Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jupyter_tools: fix out of order display of multiple shapes in static html #829

Merged
merged 4 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 16 additions & 150 deletions src/build123d/jupyter_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<div/>").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(
Expand Down Expand Up @@ -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
Expand All @@ -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]] = []

Expand All @@ -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"<div id={div_id}></div><script>{code}</script>")

return html
130 changes: 130 additions & 0 deletions src/build123d/template_render.js
Original file line number Diff line number Diff line change
@@ -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);
});
6 changes: 3 additions & 3 deletions src/build123d/topology/shape_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 7 additions & 7 deletions tests/test_direct_api/test_jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
Loading