Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/gumyr/build123d into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
gumyr committed Jan 25, 2025
2 parents 9f5b4ea + 13535be commit 45dc04c
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 160 deletions.
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

0 comments on commit 45dc04c

Please sign in to comment.