From d2d3580fc8d9eab01757891d2bd3a9d6514d2ec7 Mon Sep 17 00:00:00 2001 From: Victor Poughon Date: Thu, 19 Dec 2024 11:58:54 +0100 Subject: [PATCH 1/4] jupyter_tools: move template to separate file --- src/build123d/jupyter_tools.py | 151 ++----------------------------- src/build123d/template_render.js | 137 ++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 145 deletions(-) create mode 100644 src/build123d/template_render.js diff --git a/src/build123d/jupyter_tools.py b/src/build123d/jupyter_tools.py index 0fd9f649..830f6039 100644 --- a/src/build123d/jupyter_tools.py +++ b/src/build123d/jupyter_tools.py @@ -25,156 +25,17 @@ # pylint: disable=no-name-in-module from json import dumps +import os +from string import Template from typing import Any, Dict, List from IPython.display import Javascript 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( @@ -229,6 +90,6 @@ def display(shape: Any) -> Javascript: "orientation": [0, 0, 0], } ) - code = TEMPLATE.format(data=dumps(payload), element="element", ratio=0.5) + code = Template(TEMPLATE_JS).substitute(data=dumps(payload), element="element", ratio=0.5) return Javascript(code) diff --git a/src/build123d/template_render.js b/src/build123d/template_render.js new file mode 100644 index 00000000..8d287f5f --- /dev/null +++ b/src/build123d/template_render.js @@ -0,0 +1,137 @@ +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); + +}; + + +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(() => { + // element, data and ratio are templated by python + var parent_element = $element; + var data = $data; + render(data, parent_element, $ratio); +}); From 5204b763eab65363eb6640f896d5fc4d1a1afa20 Mon Sep 17 00:00:00 2001 From: Victor Poughon Date: Sun, 19 Jan 2025 12:49:55 +0100 Subject: [PATCH 2/4] jupyter_tools: fix async render issue using a unique id div --- src/build123d/jupyter_tools.py | 16 ++++++++++------ src/build123d/template_render.js | 27 ++++++++++----------------- src/build123d/topology/shape_core.py | 6 +++--- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/build123d/jupyter_tools.py b/src/build123d/jupyter_tools.py index 830f6039..0b3884c4 100644 --- a/src/build123d/jupyter_tools.py +++ b/src/build123d/jupyter_tools.py @@ -28,7 +28,7 @@ import os 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] @@ -65,8 +65,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 @@ -75,7 +75,7 @@ def display(shape: Any) -> Javascript: ValueError: not a valid Shape Returns: - Javascript: code + HTML: html code """ payload: list[dict[str, Any]] = [] @@ -90,6 +90,10 @@ def display(shape: Any) -> Javascript: "orientation": [0, 0, 0], } ) - code = Template(TEMPLATE_JS).substitute(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-" + str(id(shape)) + 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 index 8d287f5f..afc7bd3b 100644 --- a/src/build123d/template_render.js +++ b/src/build123d/template_render.js @@ -1,4 +1,4 @@ -function render(data, parent_element, ratio){ +function render(data, div_id, ratio){ // Initial setup const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance(); @@ -45,18 +45,9 @@ function render(data, parent_element, ratio){ 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(); - }; + // Get the div container + const container = document.getElementById(div_id); + const dims = container.parentElement.getBoundingClientRect(); openglRenderWindow.setContainer(container); @@ -130,8 +121,10 @@ new Promise( } else { resolve() }; } ).then(() => { - // element, data and ratio are templated by python - var parent_element = $element; - var data = $data; - render(data, parent_element, $ratio); + // 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): From f8d86e172269de1014029d053e9e37a051d7bbe2 Mon Sep 17 00:00:00 2001 From: Victor Poughon Date: Fri, 24 Jan 2025 19:04:35 +0100 Subject: [PATCH 3/4] test: update test_jupyter.py --- tests/test_direct_api/test_jupyter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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__": From edf1dbdaa1fdfa0fcf7ae811a8b55c023fa65a2f Mon Sep 17 00:00:00 2001 From: Victor Poughon Date: Tue, 21 Jan 2025 18:39:40 +0100 Subject: [PATCH 4/4] jupyter_tools: use uuid for unique shape id --- src/build123d/jupyter_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build123d/jupyter_tools.py b/src/build123d/jupyter_tools.py index 0b3884c4..1299a73f 100644 --- a/src/build123d/jupyter_tools.py +++ b/src/build123d/jupyter_tools.py @@ -26,6 +26,7 @@ # 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 HTML @@ -92,7 +93,7 @@ def shape_to_html(shape: Any) -> HTML: ) # A new div with a unique id, plus the JS code templated with the id - div_id = "shape-" + str(id(shape)) + 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"
")