{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "md01",
   "metadata": {},
   "source": [
    "# `webgpu` + `ngsolve_webgpu` - successor to webgui\n",
    "\n",
    "GPU-native visualization for NGSolve, successor to webgui. The familiar\n",
    "`Draw(cf, mesh, ...)`, or create your custom scene."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md02",
   "metadata": {},
   "source": [
    "### webgpu\n",
    "Python API for the JavaScript **WebGPU** standard (successor to WebGL, used by `webgui`).\n",
    "\n",
    "- Basic foundations and general purpose rendering classes\n",
    "- Camera, Colormap, Clipping, Fonts, ShapeRenderer\n",
    "- No FEM-specific code (mesh, functions)\n",
    "\n",
    "### ngsolve_webgpu\n",
    "\n",
    "Renderers for FEM-specific objects:\n",
    "- Mesh elements (surface, volume)\n",
    "- Numbers (points, elements, facets, ...)\n",
    "- CoefficientFunctions on surface elements, clipping plane, isosurface\n",
    "- Isolines\n",
    "- Vectors on surface/clipping plane\n",
    "\n",
    "### Improvements vs webgui\n",
    "\n",
    "WebGPU allows compute-only shaders and more flexible data storage on GPU. This removes some limitations of webgui:\n",
    "\n",
    "- CoefficientFunctions with arbitrary order/number of components\n",
    "- Compute shaders: run once, reuse during rendering (clipping plane, isosurface)\n",
    "- Access multiple functions in one render draw (render a function at isosurface of another levelset function)\n",
    "\n",
    "### Installation\n",
    "\n",
    "```bash\n",
    "pip install ngsolve_webgpu\n",
    "````\n",
    "\n",
    "Supported browsers: Chrome, Edge, Safari\n",
    "\n",
    "Unsupported browsers: Firefox\n",
    "\n",
    "On Linux: `about://flags`, enable \"Unsafe WebGPU support\" and \"Vulkan\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md03",
   "metadata": {},
   "source": [
    "### First triangle\n",
    "The smallest possible scene: build a renderer, then `Draw` it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c04",
   "metadata": {},
   "outputs": [],
   "source": [
    "from webgpu.jupyter import Draw\n",
    "from webgpu.triangles import TriangulationRenderer\n",
    "\n",
    "tri = TriangulationRenderer([(0, 0, 0), (0, 1, 0), (1, 0, 0)], color=(1, 1, 0, 1))\n",
    "Draw(tri)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9510cdf-91db-41ea-a22c-eda8c0ec3476",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Text rendering, compose multiple Renderer objects\n",
    "from webgpu import Labels\n",
    "\n",
    "# positions in screen coordinates x,y between -1 and 1\n",
    "labels = Labels([\"WebGPU\", \"is\", \"awesome\"], positions=[(-0.7,0.8), (0.5, 0.3), (0, -0.5)], font_size=40)\n",
    "\n",
    "Draw([tri, labels])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md05",
   "metadata": {},
   "source": [
    "### Instancing & colormaps\n",
    "Draw same shape (like arrows) at different positions, this is used by `SurfaceVectors` in `ngsolve_webgpu`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c06",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from webgpu.shapes import generate_cylinder, generate_cone, ShapeRenderer\n",
    "from webgpu.colormap import Colormap, Colorbar\n",
    "\n",
    "# build an arrow from primitives (combine with +, reposition with .move())\n",
    "shaft = generate_cylinder(n=12, radius=0.015, height=0.7, bottom_face=True)\n",
    "tip   = generate_cone(n=12, radius=0.05, height=0.3, bottom_face=True).move((0, 0, 0.7))\n",
    "arrow = shaft + tip\n",
    "\n",
    "N = 48\n",
    "theta = np.linspace(0, 2 * np.pi, N, endpoint=False)\n",
    "positions  = np.c_[np.cos(theta), np.sin(theta), np.zeros(N)]      # on a circle\n",
    "directions = np.c_[-np.sin(theta), np.cos(theta), np.zeros(N)]     # tangential\n",
    "values     = np.repeat(theta, 2)                                   # 2 per object (start,end)\n",
    "\n",
    "cmap = Colormap(colormap=\"viridis\")        # plasma, matlab:jet, matplotlib:coolwarm, ...\n",
    "cmap.set_min_max(float(values.min()), float(values.max()))\n",
    "renderer = ShapeRenderer(arrow, positions=positions, directions=directions,\n",
    "                         values=values, colormap=cmap)\n",
    "Draw([renderer, Colorbar(cmap)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md09",
   "metadata": {},
   "source": [
    "### Clipping\n",
    "`Clipping` cuts geometry on the GPU; the GUI auto-adds normal/offset sliders. Look inside a 3D mesh."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c10",
   "metadata": {},
   "outputs": [],
   "source": [
    "from webgpu import Clipping, CoordinateAxes\n",
    "clipping = Clipping(Clipping.Mode.PLANE, normal=(1,1,0))\n",
    "renderer = ShapeRenderer(arrow, positions=positions, directions=directions,\n",
    "                         values=values, colormap=cmap, clipping=clipping)\n",
    "Draw([renderer, Colorbar(cmap), CoordinateAxes()])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md11",
   "metadata": {},
   "source": [
    "### Compute shaders\n",
    "Run a WGSL kernel on GPU buffers and read the result back - no canvas. Same compute path powers cuts, streamlines and isosurfaces later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c12",
   "metadata": {},
   "outputs": [],
   "source": [
    "from webgpu.utils import *   # numpy (np) imported above; webgpu runtime already initialized\n",
    "\n",
    "device = get_device()\n",
    "a = np.array([1, 2, 3], dtype=np.float32)\n",
    "b = np.array([4, 5, 6], dtype=np.float32)\n",
    "N = a.size\n",
    "mem_size = a.size * a.itemsize\n",
    "\n",
    "a_gpu = buffer_from_array(a)\n",
    "b_gpu = buffer_from_array(b)\n",
    "res_gpu = device.createBuffer(mem_size, BufferUsage.STORAGE | BufferUsage.COPY_SRC)\n",
    "uniform_N = uniform_from_array(np.array([N], dtype=np.uint32))\n",
    "\n",
    "bindings = [\n",
    "    BufferBinding(101, a_gpu),\n",
    "    BufferBinding(102, b_gpu),\n",
    "    BufferBinding(103, res_gpu, read_only=False),\n",
    "    UniformBinding(104, uniform_N),\n",
    "]\n",
    "\n",
    "shader_code = \"\"\"\n",
    "@group(0) @binding(101) var<storage> vec_a : array<f32>;\n",
    "@group(0) @binding(102) var<storage> vec_b : array<f32>;\n",
    "@group(0) @binding(103) var<storage, read_write> vec_res : array<f32>;\n",
    "@group(0) @binding(104) var<uniform> N : u32;\n",
    "\n",
    "@compute @workgroup_size(256, 1, 1)\n",
    "fn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n",
    "    let tid = gid.x;\n",
    "    if (tid < N) {\n",
    "        vec_res[tid] = vec_a[tid] + vec_b[tid];\n",
    "    }\n",
    "}\n",
    "\"\"\"\n",
    "\n",
    "run_compute_shader(shader_code, bindings, n_workgroups=((N + 255) // 256, 1, 1))\n",
    "print(read_buffer(res_gpu, np.float32))     # -> [5. 7. 9.]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af1ab4ac-4bdd-4486-a328-6b24fa7caf9a",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "md13",
   "metadata": {},
   "source": [
    "## `ngsolve_webgpu` - the FEM specific stuff\n",
    "\n",
    "The NGSolve-facing layer. Data lives on the GPU once and is shared by renderers; fields are\n",
    "true high-order, evaluated on the GPU.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md14",
   "metadata": {},
   "source": [
    "### The `Draw` you know\n",
    "Mirrors the webgui `Draw` and dispatches on its first argument (Mesh / CF / OCC). High-order CF, colorbar and GUI come for free."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c15",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.jupyter import Draw    # high-level Draw(cf, mesh) — the webgui-style one\n",
    "from ngsolve import unit_square, Mesh, sin, cos, x, y, CF\n",
    "import ngsolve as ngs\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.15))\n",
    "Draw(sin(10 * x) * cos(8 * y), mesh, order=3, deformation=CF((x, 0.3*x**2, 0)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md16",
   "metadata": {},
   "source": [
    "### `MeshData` + `FunctionData`\n",
    "Data on the GPU once; `FunctionData` stores per-element Bernstein coefficients → true high-order rendering."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c17",
   "metadata": {},
   "outputs": [],
   "source": [
    "from netgen.occ import *\n",
    "from ngsolve_webgpu import FunctionData, MeshData, CFRenderer\n",
    "from ngsolve_webgpu.jupyter import Draw          # back to the low-level renderer Draw\n",
    "\n",
    "geo = OCCGeometry(Cylinder((0, 0, 0), X, r=0.3, h=0.5))\n",
    "mesh = Mesh(geo.GenerateMesh(maxh=6))\n",
    "mesh.Curve(5)                                  # high-order curved geometry\n",
    "\n",
    "mesh_data = MeshData(mesh)\n",
    "function_data = FunctionData(mesh_data, sin(20 * x), order=5)\n",
    "cfr = CFRenderer(function_data)\n",
    "cfr.colormap.set_min_max(-1, 1)\n",
    "Draw([cfr, Colorbar(cfr.colormap)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md18",
   "metadata": {},
   "source": [
    "### Mesh visualization\n",
    "- Entity numbers (vertices, segments, segment_indices, ....)\n",
    "- 3D volume elements with a **shrink** slider and clipping; then a curved high-order surface mesh + wireframe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5093b66f-0bdd-48df-8a64-69558067c684",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Entity numbers (vertices) on the wireframe\n",
    "from ngsolve_webgpu import EntityNumbers, MeshWireframe2d\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.3))\n",
    "meshdata = MeshData(mesh)\n",
    "Draw([MeshWireframe2d(meshdata), EntityNumbers(meshdata, entity=\"vertices\", font_size=15)])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c19",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu import MeshElements3d\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.2))\n",
    "meshdata = MeshData(mesh)\n",
    "clipping = Clipping()\n",
    "clipping.center = [0.5, 0.5, 0.5]\n",
    "clipping.mode = clipping.Mode.PLANE\n",
    "\n",
    "vol = MeshElements3d(meshdata, clipping=clipping)\n",
    "vol.shrink = 0.8\n",
    "numbers = EntityNumbers(meshdata, \"segment_indices\", clipping = clipping)\n",
    "numbers.zero_based = False\n",
    "Draw([vol, numbers], width=1000, height=700)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c20",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Curved surface mesh + wireframe (high order)\n",
    "from ngsolve_webgpu import MeshElements2d, MeshWireframe2d\n",
    "\n",
    "mesh = Mesh(OCCGeometry(Sphere((0, 0, 0), 1)).GenerateMesh(maxh=0.3))\n",
    "mesh.Curve(2)\n",
    "meshdata = MeshData(mesh)\n",
    "Draw([MeshElements2d(meshdata), MeshWireframe2d(meshdata)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md21",
   "metadata": {},
   "source": [
    "### Clipping cross-sections\n",
    "`ClippingCF` evaluates the field **on the cut plane** (compute shader)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c22",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu import ClippingCF\n",
    "from ngsolve import CF, x,y,z, sin,cos\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.1))\n",
    "cf = CF((sin(10 * z) * cos(15 * x), (x - 0.5)**2 + (y - 0.5)**2 + (z - 0.5)**2 - 2))\n",
    "\n",
    "function_data = FunctionData(MeshData(mesh), cf, order=4)\n",
    "clipping = Clipping()\n",
    "clipping.mode = clipping.Mode.PLANE\n",
    "clipping.center = [0.7, 0.5, 0.5]\n",
    "clipping.normal = [1, -1, 1]\n",
    "\n",
    "colormap = Colormap()\n",
    "cfr  = CFRenderer(function_data, colormap=colormap, clipping=clipping)   # boundary surface\n",
    "clip = ClippingCF(function_data, clipping=clipping, colormap=colormap)   # interior cut\n",
    "Draw([cfr, clip, Colorbar(colormap)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md23",
   "metadata": {},
   "source": [
    "### Vector fields\n",
    "- Arrows on a surface, arrows on a clipping plane are computed **on the GPU**\n",
    "- Streamlines are computed on CPU (similar to webgui)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c24",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Arrows on the boundary surface\n",
    "from ngsolve_webgpu import SurfaceVectors\n",
    "mesh = Mesh(OCCGeometry(Sphere((0, 0, 0), 1)).GenerateMesh(maxh=0.3))\n",
    "mesh.Curve(3)\n",
    "mdata = MeshData(mesh)\n",
    "fd = FunctionData(mdata, CF((x+0.1, 1j*y, -1j*z)), order=1)\n",
    "Draw([SurfaceVectors(fd, grid_size=50)])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c25",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Arrows on a clipping-plane cross-section\n",
    "from ngsolve_webgpu import ClippingVectors, MeshSegments\n",
    "\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.2))\n",
    "mdata = MeshData(mesh)\n",
    "fd = FunctionData(mdata, CF((x, y, 1)), order=1)\n",
    "clipping = Clipping()\n",
    "clipping.center = [0.5, 0.5, 0.5]\n",
    "segments = MeshSegments(mdata)\n",
    "Draw([ClippingVectors(fd, clipping=clipping, grid_size=50), segments])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c26",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.cf import FieldLines\n",
    "\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.3))\n",
    "fieldlines = FieldLines(CF((-y, x, 0.1)), mesh,\n",
    "                        num_lines=100, length=0.5, thickness=0.0015)\n",
    "Draw([fieldlines, segments])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md27",
   "metadata": {},
   "source": [
    "### Isosurfaces & isolines\n",
    "Isosurface = zero level-set extracted by a compute shader, coloured by a second field. Isolines = contour lines in the fragment shader."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "62e39075-968d-4eba-bd79-8c7da36d8a12",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Isosurface (sphere level-set), coloured by x\n",
    "from ngsolve_webgpu.isosurface import make_isosurface_renderers, IsoSurfaceRenderer\n",
    "\n",
    "mesh = Mesh(OCCGeometry(Box((-1, -1, -1), (1, 1, 1))).GenerateMesh(maxh=0.2))\n",
    "levelset = 0.5-(x**2 + y**2 + z**2)\n",
    "\n",
    "mesh_data = MeshData(mesh)\n",
    "func_data = FunctionData(mesh_data, x+y, order=1)\n",
    "levelset_data = FunctionData(mesh_data, levelset, order=2)\n",
    "\n",
    "segments = MeshSegments(mesh_data)\n",
    "iso = IsoSurfaceRenderer(func_data, levelset_data)\n",
    "Draw([iso, Colorbar(colormap), MeshSegments(mesh_data)], width=900)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c28",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Isosurface (sphere level-set), coloured by x\n",
    "from ngsolve_webgpu.isosurface import make_isosurface_renderers\n",
    "\n",
    "mesh = Mesh(OCCGeometry(Box((-1, -1, -1), (1, 1, 1))).GenerateMesh(maxh=0.2))\n",
    "levelset = x**2 + y**2 + z**2\n",
    "\n",
    "mesh_data = MeshData(mesh)\n",
    "colormap = Colormap()\n",
    "clipping = Clipping()\n",
    "func_data = FunctionData(mesh_data, x, order=2)\n",
    "levelset_data = FunctionData(mesh_data, levelset, order=2)\n",
    "\n",
    "# One helper wires the isosurface + negative-surface + negative-clipping\n",
    "# renderers to a single shared `value` uniform -> one \"Level\" slider in the GUI.\n",
    "renderers, iso_settings = make_isosurface_renderers(func_data, levelset_data, clipping, colormap)\n",
    "iso_settings.value = 0.5**2   # drive all three at once from Python\n",
    "clipping.mode = clipping.Mode.PLANE\n",
    "Draw(renderers + [Colorbar(colormap), MeshSegments(mesh_data)], width=900)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c29",
   "metadata": {},
   "source": [
    "# Quick isolines via the high-level Draw"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc2cd30e-c048-4cdc-9b39-be4f7e9d81c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.jupyter import Draw\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.05))\n",
    "Draw(sin(10 * x) * cos(10 * y), mesh, order=4, isolines=10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c30",
   "metadata": {},
   "outputs": [],
   "source": [
    "# IsolineRenderer with the coloured field underneath\n",
    "from ngsolve_webgpu import IsolineRenderer\n",
    "from webgpu.jupyter import Draw\n",
    "from ngsolve import exp\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.05))\n",
    "function_data = FunctionData(MeshData(mesh), exp(-(10 * ((x - 0.5)**2 + (y - 0.5)**2))), order=4)\n",
    "renderer = IsolineRenderer(function_data, n_lines=12, thickness=2.0, show_field=True)\n",
    "Draw([renderer, Colorbar(renderer.colormap)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md31",
   "metadata": {},
   "source": [
    "### Complex fields & phase\n",
    "Real & imaginary parts uploaded interleaved. A **Complex Mode** dropdown + **Animate** checkbox appear."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c32",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.jupyter import Draw\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))\n",
    "fes = ngs.H1(mesh, order=3, complex=True)\n",
    "gf = ngs.GridFunction(fes)\n",
    "gf.Set(sin(3 * x) + 1j * cos(3 * y))\n",
    "scene = Draw(gf, mesh, order=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c33",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Switch the view (just a uniform write, no data re-upload) — needs the live kernel.\n",
    "# Try \"real\" | \"imag\" | \"abs\" | \"phase\".  Or call animate_phase(scene, speed=0.2).\n",
    "for ro in scene.render_objects:\n",
    "    if isinstance(ro, CFRenderer):\n",
    "        ro.set_complex_mode(\"abs\")\n",
    "        ro.animate_phase(scene, speed=0.2)   # smooth phase sweep\n",
    "        break\n",
    "scene.render()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md34",
   "metadata": {},
   "source": [
    "### Element boundaries (DG / HDG)\n",
    "Render a CF on element **facets** so inter-element jumps become visible - for discontinuous and facet-based spaces."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c35",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2D: an L2 (discontinuous) function -> jumps across every edge\n",
    "from ngsolve_webgpu import FacetFunctionData, FacetCFRenderer\n",
    "from webgpu.jupyter import Draw\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.2))\n",
    "mdata = MeshData(mesh)\n",
    "gf = ngs.GridFunction(ngs.L2(mesh, order=0))\n",
    "gf = ngs.GridFunction(ngs.FacetFESpace(mesh, order=0))\n",
    "\n",
    "gf.vec.FV().NumPy()[:] = [i / len(gf.vec) for i in range(len(gf.vec))]\n",
    "\n",
    "facet_data = FacetFunctionData(mdata, gf, order=2)\n",
    "colormap = Colormap()\n",
    "Draw([FacetCFRenderer(facet_data, colormap=colormap, thickness=0.02), MeshWireframe2d(mdata), Colorbar(colormap)])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c36",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3D: every tet face, slightly shrunk, with clipping\n",
    "from ngsolve_webgpu import FacetCFRenderer3D\n",
    "\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.3))\n",
    "mdata = MeshData(mesh)\n",
    "\n",
    "gf = ngs.GridFunction(ngs.L2(mesh, order=0))\n",
    "gf.vec.FV().NumPy()[:] = [i / len(gf.vec) for i in range(len(gf.vec))]\n",
    "\n",
    "colormap = Colormap()\n",
    "clipping = Clipping()\n",
    "clipping.mode = clipping.Mode.PLANE\n",
    "clipping.center = [0.5, 0.5, 0.5]\n",
    "clipping.normal = [1, 0, 0]\n",
    "\n",
    "renderer = FacetCFRenderer3D(mdata, gf, order=1, colormap=colormap, clipping=clipping, shrink=0.9)\n",
    "Draw([renderer, Colorbar(colormap)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md37",
   "metadata": {},
   "source": [
    "### Probe & pick\n",
    "Entity numbers on the wireframe, and **click-to-pick** an element/region (picking needs the live kernel)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c39",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Click-to-pick: prints the element / region (needs the live kernel)\n",
    "from ngsolve_webgpu import MeshPickResult\n",
    "import webgpu.jupyter as wj\n",
    "\n",
    "mesh = Mesh(unit_cube.GenerateMesh(maxh=0.3))\n",
    "mesh_data = MeshData(mesh)\n",
    "surface = MeshElements2d(mesh_data)\n",
    "wireframe = MeshWireframe2d(mesh_data)\n",
    "\n",
    "def on_pick(ev):\n",
    "    print(MeshPickResult(ev, mesh, scene.options, kind=\"surface\"))\n",
    "\n",
    "surface.on_select(on_pick)\n",
    "scene = wj.Draw([surface, wireframe])\n",
    "scene.input_handler.on_click(lambda ev: scene.select(ev[\"canvasX\"], ev[\"canvasY\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md40",
   "metadata": {},
   "source": [
    "### Geometry from OCC\n",
    "`Draw` takes an OCC shape directly: faces + edges + vertices with their real colours — no meshing required."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c41",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.jupyter import Draw\n",
    "\n",
    "shape = Box((-1, -1, -1), (1, 1, 1)) - Cylinder((0, 0, -2), Z, r=0.5, h=4)\n",
    "Draw(shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md42",
   "metadata": {},
   "source": [
    "### Animation & live updates\n",
    "`Animation` records per-frame GPU buffers; a **Frame** slider scrubs the timesteps and survives HTML export. Record frames *before* `Draw`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c43",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ngsolve_webgpu.animate import Animation\n",
    "from webgpu.jupyter import Draw\n",
    "\n",
    "mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))\n",
    "gf = ngs.GridFunction(ngs.H1(mesh, order=3))\n",
    "t = ngs.Parameter(0)\n",
    "f = sin(10 * (x - 0.1 * t))\n",
    "gf.Interpolate(f)\n",
    "\n",
    "md_ = MeshData(mesh)\n",
    "fd = FunctionData(md_, gf, order=3)\n",
    "cfr = CFRenderer(fd)\n",
    "cfr.colormap.set_min_max(-1, 1)\n",
    "\n",
    "ani = Animation(cfr)\n",
    "ani.add_time()                              # frame 0\n",
    "for tval in range(1, 21):                   # record before Draw\n",
    "    t.Set(tval * 5)\n",
    "    gf.Interpolate(f)\n",
    "    ani.add_time()\n",
    "\n",
    "Draw([ani, Colorbar(cfr.colormap)])         # drag the \"Animation/Frame\" slider"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30f20d30-e818-472c-9cc5-fcc83a9b6da2",
   "metadata": {},
   "source": [
    "### List all class names in ngsolve_webgpu"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c4c945c-9784-4ba5-8895-58e40b49becc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import ngsolve_webgpu \n",
    "print('\\n'.join([name for name in dir(ngsolve_webgpu) if name[0].isupper()]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c00a2df-5e52-4614-b0a1-c66e0ad9e1e6",
   "metadata": {},
   "source": [
    "### Export to html\n",
    "```bash\n",
    "WEBGPU_EXPORTING=1 jupyter nbconvert --to html --execute your_notebook.ipynb\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md45",
   "metadata": {},
   "source": [
    "### Custom renderer in raw WGSL\n",
    "A `Renderer` needs only `get_shader_code` + `get_bounding_box`. `#import camera` hands the vertex shader the interactive 3D camera."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c46",
   "metadata": {},
   "outputs": [],
   "source": [
    "from webgpu.renderer import Renderer    # wj (webgpu.jupyter) imported earlier\n",
    "\n",
    "shader_code = \"\"\"\n",
    "#import camera\n",
    "\n",
    "struct FragmentInput {\n",
    "    @builtin(position) p: vec4<f32>,\n",
    "    @location(0) color: vec4<f32>,\n",
    "};\n",
    "\n",
    "@vertex\n",
    "fn vertex_main(@builtin(vertex_index) vertex_index : u32) -> FragmentInput {\n",
    "    var pos = array<vec3f, 3>(\n",
    "        vec3f( 0.0,  0.5, 0.),\n",
    "        vec3f(-0.5, -0.5, 0.),\n",
    "        vec3f( 0.5, -0.5, 0.));\n",
    "    var color = array<vec4f, 3>(\n",
    "        vec4f(1., 0., 0., 1.),\n",
    "        vec4f(0., 1., 0., 1.),\n",
    "        vec4f(0., 0., 1., 1.));\n",
    "    return FragmentInput(cameraMapPoint(pos[vertex_index]), color[vertex_index]);\n",
    "}\n",
    "\n",
    "@fragment\n",
    "fn fragment_main(input: FragmentInput) -> @location(0) vec4f {\n",
    "    return input.color;\n",
    "}\n",
    "\"\"\"\n",
    "\n",
    "class TriangleCamera(Renderer):\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.n_vertices = 3\n",
    "    def get_bounding_box(self):\n",
    "        return ((-0.5, -0.5, 0), (0.5, 0.5, 0))\n",
    "    def get_shader_code(self):\n",
    "        return shader_code\n",
    "\n",
    "wj.Draw(TriangleCamera())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md49",
   "metadata": {},
   "source": [
    "## Links\n",
    "\n",
    "- `ngsolve_webgpu`: <https://cerbsim.github.io/ngsolve_webgpu/>\n",
    "- `webgpu`: <https://cerbsim.github.io/webgpu/>\n",
    "- source: <https://github.com/CERBSim/webgpu> · <https://github.com/CERBSim/ngsolve_webgpu>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ff84827-eb52-4369-8279-58299f344dae",
   "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.14.6"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {},
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
