diff --git a/nbs/mesh_viewer.ipynb b/nbs/mesh_viewer.ipynb new file mode 100644 index 00000000..bf36d3eb --- /dev/null +++ b/nbs/mesh_viewer.ipynb @@ -0,0 +1,112 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9331b3ed", + "metadata": {}, + "source": "# Inline Mesh Viewer\n\n`plot_mesh_threejs` — interactive Three.js `.msh` viewer, zero Python dependencies, runs entirely in the browser." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da885bce", + "metadata": {}, + "outputs": [], + "source": [ + "import gdsfactory as gf\n", + "from ihp import LAYER, PDK\n", + "\n", + "PDK.activate()\n", + "\n", + "\n", + "@gf.cell\n", + "def gsg_electrode(\n", + " length: float = 800,\n", + " s_width: float = 20,\n", + " g_width: float = 40,\n", + " gap_width: float = 15,\n", + " layer=LAYER.TopMetal2drawing,\n", + ") -> gf.Component:\n", + " c = gf.Component()\n", + " r1 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)\n", + " r1.move((0, (g_width + s_width) / 2 + gap_width))\n", + " _r2 = c << gf.c.rectangle((length, s_width), centered=True, layer=layer)\n", + " r3 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)\n", + " r3.move((0, -(g_width + s_width) / 2 - gap_width))\n", + " c.add_port(\n", + " name=\"o1\",\n", + " center=(-length / 2, 0),\n", + " width=s_width,\n", + " orientation=0,\n", + " port_type=\"electrical\",\n", + " layer=layer,\n", + " )\n", + " c.add_port(\n", + " name=\"o2\",\n", + " center=(length / 2, 0),\n", + " width=s_width,\n", + " orientation=180,\n", + " port_type=\"electrical\",\n", + " layer=layer,\n", + " )\n", + " return c\n", + "\n", + "\n", + "c = gsg_electrode()\n", + "c" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29d77946", + "metadata": {}, + "outputs": [], + "source": [ + "from gsim.palace import DrivenSim\n", + "\n", + "sim = DrivenSim()\n", + "sim.set_output_dir(\"./mesh-viewer-demo\")\n", + "sim.set_geometry(c)\n", + "sim.set_stack(substrate_thickness=2.0, air_above=300.0)\n", + "sim.add_cpw_port(\"o1\", layer=\"topmetal2\", s_width=20, gap_width=15)\n", + "sim.add_cpw_port(\"o2\", layer=\"topmetal2\", s_width=20, gap_width=15)\n", + "sim.set_driven(fmin=1e9, fmax=100e9, num_points=300)\n", + "sim.mesh(preset=\"default\", planar_conductors=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "280ba4ea", + "metadata": {}, + "outputs": [], + "source": [ + "from gsim.viz import plot_mesh_threejs\n", + "\n", + "plot_mesh_threejs(\"mesh-viewer-demo/palace.msh\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gsim", + "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.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 68f5f562..8519968f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -235,6 +235,9 @@ select = ["ALL"] "SLF001", # private-member-access "T201", # print ] +"src/gsim/common/viz/render3d_threejs.py" = [ + "E501", # line-too-long (embedded HTML/JS template) +] "docs/hooks.py" = [ "ARG001", # unused-function-argument (MkDocs hooks require specific signatures) "INP001", # implicit-namespace-package diff --git a/src/gsim/common/viz/__init__.py b/src/gsim/common/viz/__init__.py index 030b67d3..7abdfc21 100644 --- a/src/gsim/common/viz/__init__.py +++ b/src/gsim/common/viz/__init__.py @@ -6,6 +6,7 @@ 3D backends: - PyVista (desktop, ``plot_prisms_3d``) - Open3D + Plotly (Jupyter, ``plot_prisms_3d_open3d``) + - Three.js (Jupyter, ``plot_mesh_threejs`` — zero-dep inline HTML) 2D backends: - matplotlib (``plot_prism_slices``) @@ -18,10 +19,12 @@ plot_prisms_3d, plot_prisms_3d_open3d, ) +from gsim.common.viz.render3d_threejs import plot_mesh_threejs __all__ = [ "create_web_export", "export_3d_mesh", + "plot_mesh_threejs", "plot_prism_slices", "plot_prisms_3d", "plot_prisms_3d_open3d", diff --git a/src/gsim/common/viz/render3d_threejs.py b/src/gsim/common/viz/render3d_threejs.py new file mode 100644 index 00000000..0c9479d0 --- /dev/null +++ b/src/gsim/common/viz/render3d_threejs.py @@ -0,0 +1,520 @@ +"""Three.js-based 3D mesh viewer for Jupyter notebooks. + +Embeds an interactive WebGL viewer directly in notebook output cells using +Three.js (loaded from CDN). Zero Python dependencies beyond the standard +library and IPython — no VTK, no trame, no meshio required. + +Features: + - Parses Gmsh ``.msh`` files (v2.x and v4.x) client-side in JavaScript + - Tetrahedral → surface extraction with correct outward normals + - BVH spatial chunking with per-frame frustum & screen-size culling + - Per-physical-group toggle buttons + - CAD-standard camera presets (Iso, Top, Bottom, Front, Back, Right, Left) + +Based on a custom standalone Three.js mesh viewer. +""" + +from __future__ import annotations + +import json +import uuid +from pathlib import Path + +__all__ = ["plot_mesh_threejs"] + + +def plot_mesh_threejs( + msh_path: str | Path, + *, + height: int = 600, +) -> object: + """Display a ``.msh`` file in an interactive Three.js viewer. + + The viewer is rendered inline as an HTML widget — works in JupyterLab, + VS Code notebooks, and Google Colab with zero server-side dependencies. + + Args: + msh_path: Path to a Gmsh ``.msh`` file (ASCII, v2.x or v4.x). + height: Pixel height of the viewer widget. + + Returns: + An ``IPython.display.HTML`` object. In a notebook the viewer is + displayed automatically; in a script call ``display(result)``. + """ + from IPython.display import HTML + + msh_text = Path(msh_path).read_text() + msh_json = json.dumps(msh_text) + cid = f"gsim-mesh-{uuid.uuid4().hex[:8]}" + + html = _TEMPLATE.format(cid=cid, height=height, msh_json=msh_json) + return HTML(html) + + +# --------------------------------------------------------------------------- +# HTML / JS template +# --------------------------------------------------------------------------- +# Uses {{/}} for literal braces inside the f-string, and {cid}/{height}/{msh_json} +# for Python-interpolated values. + +_TEMPLATE = """\ +