Skip to content

Commit

Permalink
Implement importBrep and vtkPolyData export (#735)
Browse files Browse the repository at this point in the history
* Implement importBrep

* Implement rw to/from stream

* Implement toVtkPolyData

* Implemented VTP export

* Added normals calculation

* use VTK for rendering in jupyter

* Added orientation marker

* Assy rendering in notebooks

* Implement export to vtkjs

* Store the env in the cqgi result

* VTK-based cq directive

* Support show_object and assy

* assy vrml export via vtk

* Use vtk in the docs

* Add slot dxf file

* Add vtk.js to the static files

* Use single renderer

* Ignore cq_directive code coverage

* Ignore missing docutils stubs

* Implement select

* Disable interaction dynamically

* Mention VTP in the docs

* Add path to the test reqs
  • Loading branch information
adam-urbanczyk authored Jun 22, 2021
1 parent 7b1a99c commit e00ac83
Show file tree
Hide file tree
Showing 26 changed files with 9,395 additions and 47 deletions.
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[run]
branch = True
omit = cadquery/utils.py
omit =
cadquery/utils.py
cadquery/cq_directive.py

[report]
exclude_lines =
Expand Down
2 changes: 1 addition & 1 deletion build-docs.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/sh
sphinx-build -b html doc target/docs
(cd doc && sphinx-build -b html . ../target/docs)
9 changes: 9 additions & 0 deletions cadquery/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,12 @@ def toCompound(self) -> Compound:
shapes.extend((child.toCompound() for child in self.children))

return Compound.makeCompound(shapes).locate(self.loc)

def _repr_javascript_(self):
"""
Jupyter 3D representation support
"""

from .occ_impl.jupyter_tools import display

return display(self)._repr_javascript_()
6 changes: 4 additions & 2 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -4024,15 +4024,17 @@ def offset2D(

return self.newObject(rv)

def _repr_html_(self) -> Any:
def _repr_javascript_(self) -> Any:
"""
Special method for rendering current object in a jupyter notebook
"""

if type(self.val()) is Vector:
return "&lt {} &gt".format(self.__repr__()[1:-1])
else:
return Compound.makeCompound(_selectShapes(self.objects))._repr_html_()
return Compound.makeCompound(
_selectShapes(self.objects)
)._repr_javascript_()


# alias for backward compatibility
Expand Down
270 changes: 269 additions & 1 deletion cadquery/cq_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@
"""

import traceback
from cadquery import exporters

from pathlib import Path
from uuid import uuid1 as uuid
from textwrap import indent

from cadquery import exporters, Assembly, Compound, Color
from cadquery import cqgi
from cadquery.occ_impl.jupyter_tools import (
toJSON,
dumps,
TEMPLATE_RENDER,
DEFAULT_COLOR,
)
from docutils.parsers.rst import directives, Directive

template = """
Expand All @@ -21,6 +32,181 @@
"""
template_content_indent = " "

rendering_code = """
const RENDERERS = {};
var ID = 0;
const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance();
const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
renderWindow.addView(openglRenderWindow);
const rootContainer = document.createElement('div');
rootContainer.style.position = 'fixed';
//rootContainer.style.zIndex = -1;
rootContainer.style.left = 0;
rootContainer.style.top = 0;
rootContainer.style.pointerEvents = 'none';
rootContainer.style.width = '100%';
rootContainer.style.height = '100%';
openglRenderWindow.setContainer(rootContainer);
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.setButton(3);
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.setInteractorStyle(interact_style);
document.addEventListener('DOMContentLoaded', function () {
document.body.appendChild(rootContainer);
interactor.bindEvents(document.body);
});
function updateViewPort(element, renderer) {
const { innerHeight, innerWidth } = window;
const { x, y, width, height } = element.getBoundingClientRect();
const viewport = [
x / innerWidth,
1 - (y + height) / innerHeight,
(x + width) / innerWidth,
1 - y / innerHeight,
];
renderer.setViewport(...viewport);
}
function recomputeViewports() {
const rendererElems = document.querySelectorAll('.renderer');
for (let i = 0; i < rendererElems.length; i++) {
const elem = rendererElems[i];
const { id } = elem;
const renderer = RENDERERS[id];
updateViewPort(elem, renderer);
}
renderWindow.render();
}
function resize() {
rootContainer.style.width = `${window.innerWidth}px`;
openglRenderWindow.setSize(window.innerWidth, window.innerHeight);
recomputeViewports();
}
window.addEventListener('resize', resize);
document.addEventListener('scroll', recomputeViewports);
function enterCurrentRenderer(e) {
interact_style.setEnabled(true);
interactor.setCurrentRenderer(RENDERERS[e.target.id]);
}
function exitCurrentRenderer(e) {
interactor.setCurrentRenderer(null);
interact_style.setEnabled(false);
}
function applyStyle(element) {
element.classList.add('renderer');
element.style.width = '100%';
element.style.height = '100%';
element.style.display = 'inline-block';
element.style.boxSizing = 'border';
return element;
}
window.addEventListener('load', resize);
function render(data, parent_element, ratio){
// Initial setup
const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({ background: [1, 1, 1 ] });
// 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());
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);
};
//add the container
const container = applyStyle(document.createElement("div"));
parent_element.appendChild(container);
container.addEventListener('mouseenter', enterCurrentRenderer);
container.addEventListener('mouseleave', exitCurrentRenderer);
container.id = ID;
renderWindow.addRenderer(renderer);
updateViewPort(container, renderer);
renderer.resetCamera();
RENDERERS[ID] = renderer;
ID++;
};
"""


template_vtk = """
.. raw:: html
<div class="cq-vtk"
style="text-align:{txt_align}s;float:left;border: 1px solid #ddd; width:{width}; height:{height}"">
<script>
var parent_element = {element};
var data = {data};
render(data, parent_element);
</script>
</div>
<div style="clear:both;">
</div>
"""


class cq_directive(Directive):

Expand Down Expand Up @@ -84,9 +270,91 @@ def run(self):
return []


class cq_directive_vtk(Directive):

has_content = True
required_arguments = 0
optional_arguments = 2
option_spec = {
"height": directives.length_or_unitless,
"width": directives.length_or_percentage_or_unitless,
"align": directives.unchanged,
"select": directives.unchanged,
}

def run(self):

options = self.options
content = self.content
state_machine = self.state_machine
env = self.state.document.settings.env
build_path = Path(env.app.builder.outdir)
out_path = build_path / "_static"

# only consider inline snippets
plot_code = "\n".join(content)

# collect the result
try:
result = cqgi.parse(plot_code).build()

if result.success:
if result.first_result:
shape = result.first_result.shape
else:
shape = result.env[options.get("select", "result")]

if isinstance(shape, Assembly):
assy = shape
else:
assy = Assembly(shape, color=Color(*DEFAULT_COLOR))
else:
raise result.exception

except Exception:
traceback.print_exc()
assy = Assembly(Compound.makeText("CQGI error", 10, 5))

# save vtkjs to static
fname = Path(str(uuid()))
exporters.assembly.exportVTKJS(assy, out_path / fname)
fname = str(fname) + ".zip"

# add the output
lines = []

data = dumps(toJSON(assy))

lines.extend(
template_vtk.format(
code=indent(TEMPLATE_RENDER.format(), " "),
data=data,
ratio="null",
element="document.currentScript.parentNode",
txt_align=options.get("align", "left"),
width=options.get("width", "100%"),
height=options.get("height", "500px"),
).splitlines()
)

lines.extend(["::", ""])
lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")])
lines.append("")

if len(lines):
state_machine.insert_input(lines, state_machine.input_lines.source(0))

return []


def setup(app):
setup.app = app
setup.config = app.config
setup.confdir = app.confdir

app.add_directive("cq_plot", cq_directive)
app.add_directive("cadquery", cq_directive_vtk)

# add vtk.js
app.add_js_file("vtk.js")
app.add_js_file(None, body=rendering_code)
6 changes: 5 additions & 1 deletion cadquery/cqgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@ def build(self, build_parameters=None, build_options=None):
exec(c, env)
result.set_debug(collector.debugObjects)
result.set_success_result(collector.outputObjects)
result.env = env

except Exception as ex:
result.set_failure_result(ex)

end = time.perf_counter()
result.buildTime = end - start

return result

def set_param_values(self, params):
Expand Down Expand Up @@ -322,12 +324,14 @@ def __init__(self):
self.outputObjects = []
self.debugObjects = []

def show_object(self, shape, options={}):
def show_object(self, shape, options={}, **kwargs):
"""
return an object to the executing environment, with options
:param shape: a cadquery object
:param options: a dictionary of options that will be made available to the executing environment
"""
options.update(kwargs)

o = ShapeResult()
o.options = options
o.shape = shape
Expand Down
Loading

0 comments on commit e00ac83

Please sign in to comment.