diff --git a/src/build123d/jupyter_tools.py b/src/build123d/jupyter_tools.py index 0fd9f649..1299a73f 100644 --- a/src/build123d/jupyter_tools.py +++ b/src/build123d/jupyter_tools.py @@ -25,156 +25,18 @@ # pylint: disable=no-name-in-module from json import dumps +import os +import uuid +from string import Template from typing import Any, Dict, List -from IPython.display import Javascript +from IPython.display import HTML from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter DEFAULT_COLOR = [1, 0.8, 0, 1] -TEMPLATE_RENDER = """ - -function render(data, parent_element, ratio){{ - - // Initial setup - const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance(); - const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({{ background: [1, 1, 1 ] }}); - renderWindow.addRenderer(renderer); - - // iterate over all children children - for (var el of data){{ - var trans = el.position; - var rot = el.orientation; - var rgba = el.color; - var shape = el.shape; - - // load the inline data - var reader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance(); - const textEncoder = new TextEncoder(); - reader.parseAsArrayBuffer(textEncoder.encode(shape)); - - // setup actor,mapper and add - const mapper = vtk.Rendering.Core.vtkMapper.newInstance(); - mapper.setInputConnection(reader.getOutputPort()); - mapper.setResolveCoincidentTopologyToPolygonOffset(); - mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.5,100); - - const actor = vtk.Rendering.Core.vtkActor.newInstance(); - actor.setMapper(mapper); - - // set color and position - actor.getProperty().setColor(rgba.slice(0,3)); - actor.getProperty().setOpacity(rgba[3]); - - actor.rotateZ(rot[2]*180/Math.PI); - actor.rotateY(rot[1]*180/Math.PI); - actor.rotateX(rot[0]*180/Math.PI); - - actor.setPosition(trans); - - renderer.addActor(actor); - - }}; - - renderer.resetCamera(); - - const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance(); - renderWindow.addView(openglRenderWindow); - - // Add output to the "parent element" - var container; - var dims; - - if(typeof(parent_element.appendChild) !== "undefined"){{ - container = document.createElement("div"); - parent_element.appendChild(container); - dims = parent_element.getBoundingClientRect(); - }}else{{ - container = parent_element.append("
").children("div:last-child").get(0); - dims = parent_element.get(0).getBoundingClientRect(); - }}; - - openglRenderWindow.setContainer(container); - - // handle size - if (ratio){{ - openglRenderWindow.setSize(dims.width, dims.width*ratio); - }}else{{ - openglRenderWindow.setSize(dims.width, dims.height); - }}; - - // Interaction setup - const interact_style = vtk.Interaction.Style.vtkInteractorStyleManipulator.newInstance(); - - const manips = {{ - rot: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRotateManipulator.newInstance(), - pan: vtk.Interaction.Manipulators.vtkMouseCameraTrackballPanManipulator.newInstance(), - zoom1: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(), - zoom2: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(), - roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(), - }}; - - manips.zoom1.setControl(true); - manips.zoom2.setScrollEnabled(true); - manips.roll.setShift(true); - manips.pan.setButton(2); - - for (var k in manips){{ - interact_style.addMouseManipulator(manips[k]); - }}; - - const interactor = vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance(); - interactor.setView(openglRenderWindow); - interactor.initialize(); - interactor.bindEvents(container); - interactor.setInteractorStyle(interact_style); - - // Orientation marker - - const axes = vtk.Rendering.Core.vtkAnnotatedCubeActor.newInstance(); - axes.setXPlusFaceProperty({{text: '+X'}}); - axes.setXMinusFaceProperty({{text: '-X'}}); - axes.setYPlusFaceProperty({{text: '+Y'}}); - axes.setYMinusFaceProperty({{text: '-Y'}}); - axes.setZPlusFaceProperty({{text: '+Z'}}); - axes.setZMinusFaceProperty({{text: '-Z'}}); - - const orientationWidget = vtk.Interaction.Widgets.vtkOrientationMarkerWidget.newInstance({{ - actor: axes, - interactor: interactor }}); - orientationWidget.setEnabled(true); - orientationWidget.setViewportCorner(vtk.Interaction.Widgets.vtkOrientationMarkerWidget.Corners.BOTTOM_LEFT); - orientationWidget.setViewportSize(0.2); - -}}; -""" - -TEMPLATE = ( - TEMPLATE_RENDER - + """ - -new Promise( - function(resolve, reject) - {{ - if (typeof(require) !== "undefined" ){{ - require.config({{ - "paths": {{"vtk": "https://unpkg.com/vtk"}}, - }}); - require(["vtk"], resolve, reject); - }} else if ( typeof(vtk) === "undefined" ){{ - var script = document.createElement("script"); - script.onload = resolve; - script.onerror = reject; - script.src = "https://unpkg.com/vtk.js"; - document.head.appendChild(script); - }} else {{ resolve() }}; - }} -).then(() => {{ - var parent_element = {element}; - var data = {data}; - render(data, parent_element, {ratio}); -}}); -""" -) +dir_path = os.path.dirname(os.path.realpath(__file__)) +with open(os.path.join(dir_path, "template_render.js"), encoding="utf-8") as f: + TEMPLATE_JS = f.read() def to_vtkpoly_string( @@ -204,8 +66,8 @@ def to_vtkpoly_string( return writer.GetOutputString() -def display(shape: Any) -> Javascript: - """display +def shape_to_html(shape: Any) -> HTML: + """shape_to_html Args: shape (Shape): object to display @@ -214,7 +76,7 @@ def display(shape: Any) -> Javascript: ValueError: not a valid Shape Returns: - Javascript: code + HTML: html code """ payload: list[dict[str, Any]] = [] @@ -229,6 +91,10 @@ def display(shape: Any) -> Javascript: "orientation": [0, 0, 0], } ) - code = TEMPLATE.format(data=dumps(payload), element="element", ratio=0.5) - return Javascript(code) + # A new div with a unique id, plus the JS code templated with the id + div_id = 'shape-' + uuid.uuid4().hex[:8] + code = Template(TEMPLATE_JS).substitute(data=dumps(payload), div_id=div_id, ratio=0.5) + html = HTML(f"") + + return html diff --git a/src/build123d/template_render.js b/src/build123d/template_render.js new file mode 100644 index 00000000..afc7bd3b --- /dev/null +++ b/src/build123d/template_render.js @@ -0,0 +1,130 @@ +function render(data, div_id, ratio){ + + // Initial setup + const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance(); + const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({ background: [1, 1, 1 ] }); + renderWindow.addRenderer(renderer); + + // iterate over all children children + for (var el of data){ + var trans = el.position; + var rot = el.orientation; + var rgba = el.color; + var shape = el.shape; + + // load the inline data + var reader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance(); + const textEncoder = new TextEncoder(); + reader.parseAsArrayBuffer(textEncoder.encode(shape)); + + // setup actor,mapper and add + const mapper = vtk.Rendering.Core.vtkMapper.newInstance(); + mapper.setInputConnection(reader.getOutputPort()); + mapper.setResolveCoincidentTopologyToPolygonOffset(); + mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.5,100); + + const actor = vtk.Rendering.Core.vtkActor.newInstance(); + actor.setMapper(mapper); + + // set color and position + actor.getProperty().setColor(rgba.slice(0,3)); + actor.getProperty().setOpacity(rgba[3]); + + actor.rotateZ(rot[2]*180/Math.PI); + actor.rotateY(rot[1]*180/Math.PI); + actor.rotateX(rot[0]*180/Math.PI); + + actor.setPosition(trans); + + renderer.addActor(actor); + + }; + + renderer.resetCamera(); + + const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance(); + renderWindow.addView(openglRenderWindow); + + // Get the div container + const container = document.getElementById(div_id); + const dims = container.parentElement.getBoundingClientRect(); + + openglRenderWindow.setContainer(container); + + // handle size + if (ratio){ + openglRenderWindow.setSize(dims.width, dims.width*ratio); + }else{ + openglRenderWindow.setSize(dims.width, dims.height); + }; + + // Interaction setup + const interact_style = vtk.Interaction.Style.vtkInteractorStyleManipulator.newInstance(); + + const manips = { + rot: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRotateManipulator.newInstance(), + pan: vtk.Interaction.Manipulators.vtkMouseCameraTrackballPanManipulator.newInstance(), + zoom1: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(), + zoom2: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(), + roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(), + }; + + manips.zoom1.setControl(true); + manips.zoom2.setScrollEnabled(true); + manips.roll.setShift(true); + manips.pan.setButton(2); + + for (var k in manips){ + interact_style.addMouseManipulator(manips[k]); + }; + + const interactor = vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance(); + interactor.setView(openglRenderWindow); + interactor.initialize(); + interactor.bindEvents(container); + interactor.setInteractorStyle(interact_style); + + // Orientation marker + + const axes = vtk.Rendering.Core.vtkAnnotatedCubeActor.newInstance(); + axes.setXPlusFaceProperty({text: '+X'}); + axes.setXMinusFaceProperty({text: '-X'}); + axes.setYPlusFaceProperty({text: '+Y'}); + axes.setYMinusFaceProperty({text: '-Y'}); + axes.setZPlusFaceProperty({text: '+Z'}); + axes.setZMinusFaceProperty({text: '-Z'}); + + const orientationWidget = vtk.Interaction.Widgets.vtkOrientationMarkerWidget.newInstance({ + actor: axes, + interactor: interactor }); + orientationWidget.setEnabled(true); + orientationWidget.setViewportCorner(vtk.Interaction.Widgets.vtkOrientationMarkerWidget.Corners.BOTTOM_LEFT); + orientationWidget.setViewportSize(0.2); + +}; + + +new Promise( + function(resolve, reject) + { + if (typeof(require) !== "undefined" ){ + require.config({ + "paths": {"vtk": "https://unpkg.com/vtk"}, + }); + require(["vtk"], resolve, reject); + } else if ( typeof(vtk) === "undefined" ){ + var script = document.createElement("script"); + script.onload = resolve; + script.onerror = reject; + script.src = "https://unpkg.com/vtk.js"; + document.head.appendChild(script); + } else { resolve() }; + } +).then(() => { + // data, div_id and ratio are templated by python + const div_id = "$div_id"; + const data = $data; + const ratio = $ratio; + + render(data, div_id, ratio); +}); diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py index d266f019..7ebeaae3 100644 --- a/src/build123d/topology/shape_core.py +++ b/src/build123d/topology/shape_core.py @@ -2117,12 +2117,12 @@ def _ocp_section( return (vertices, edges) - def _repr_javascript_(self): + def _repr_html_(self): """Jupyter 3D representation support""" - from build123d.jupyter_tools import display + from build123d.jupyter_tools import shape_to_html - return display(self)._repr_javascript_() + return shape_to_html(self)._repr_html_() class Comparable(ABC): diff --git a/tests/test_direct_api/test_jupyter.py b/tests/test_direct_api/test_jupyter.py index 51053a45..7ae4074d 100644 --- a/tests/test_direct_api/test_jupyter.py +++ b/tests/test_direct_api/test_jupyter.py @@ -29,28 +29,28 @@ import unittest from build123d.geometry import Vector -from build123d.jupyter_tools import to_vtkpoly_string, display +from build123d.jupyter_tools import to_vtkpoly_string, shape_to_html from build123d.topology import Solid class TestJupyter(unittest.TestCase): - def test_repr_javascript(self): + def test_repr_html(self): shape = Solid.make_box(1, 1, 1) - # Test no exception on rendering to js - js1 = shape._repr_javascript_() + # Test no exception on rendering to html + html1 = shape._repr_html_() - assert "function render" in js1 + assert "function render" in html1 def test_display_error(self): with self.assertRaises(AttributeError): - display(Vector()) + shape_to_html(Vector()) with self.assertRaises(ValueError): to_vtkpoly_string("invalid") with self.assertRaises(ValueError): - display("invalid") + shape_to_html("invalid") if __name__ == "__main__":