Skip to content

Commit e00ac83

Browse files
Implement importBrep and vtkPolyData export (#735)
* 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
1 parent 7b1a99c commit e00ac83

26 files changed

+9395
-47
lines changed

.coveragerc

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[run]
22
branch = True
3-
omit = cadquery/utils.py
3+
omit =
4+
cadquery/utils.py
5+
cadquery/cq_directive.py
46

57
[report]
68
exclude_lines =

build-docs.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/bin/sh
2-
sphinx-build -b html doc target/docs
2+
(cd doc && sphinx-build -b html . ../target/docs)

cadquery/assembly.py

+9
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,12 @@ def toCompound(self) -> Compound:
519519
shapes.extend((child.toCompound() for child in self.children))
520520

521521
return Compound.makeCompound(shapes).locate(self.loc)
522+
523+
def _repr_javascript_(self):
524+
"""
525+
Jupyter 3D representation support
526+
"""
527+
528+
from .occ_impl.jupyter_tools import display
529+
530+
return display(self)._repr_javascript_()

cadquery/cq.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -4024,15 +4024,17 @@ def offset2D(
40244024

40254025
return self.newObject(rv)
40264026

4027-
def _repr_html_(self) -> Any:
4027+
def _repr_javascript_(self) -> Any:
40284028
"""
40294029
Special method for rendering current object in a jupyter notebook
40304030
"""
40314031

40324032
if type(self.val()) is Vector:
40334033
return "&lt {} &gt".format(self.__repr__()[1:-1])
40344034
else:
4035-
return Compound.makeCompound(_selectShapes(self.objects))._repr_html_()
4035+
return Compound.makeCompound(
4036+
_selectShapes(self.objects)
4037+
)._repr_javascript_()
40364038

40374039

40384040
# alias for backward compatibility

cadquery/cq_directive.py

+269-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@
44
"""
55

66
import traceback
7-
from cadquery import exporters
7+
8+
from pathlib import Path
9+
from uuid import uuid1 as uuid
10+
from textwrap import indent
11+
12+
from cadquery import exporters, Assembly, Compound, Color
813
from cadquery import cqgi
14+
from cadquery.occ_impl.jupyter_tools import (
15+
toJSON,
16+
dumps,
17+
TEMPLATE_RENDER,
18+
DEFAULT_COLOR,
19+
)
920
from docutils.parsers.rst import directives, Directive
1021

1122
template = """
@@ -21,6 +32,181 @@
2132
"""
2233
template_content_indent = " "
2334

35+
rendering_code = """
36+
const RENDERERS = {};
37+
var ID = 0;
38+
39+
const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance();
40+
const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
41+
renderWindow.addView(openglRenderWindow);
42+
43+
const rootContainer = document.createElement('div');
44+
rootContainer.style.position = 'fixed';
45+
//rootContainer.style.zIndex = -1;
46+
rootContainer.style.left = 0;
47+
rootContainer.style.top = 0;
48+
rootContainer.style.pointerEvents = 'none';
49+
rootContainer.style.width = '100%';
50+
rootContainer.style.height = '100%';
51+
52+
openglRenderWindow.setContainer(rootContainer);
53+
54+
const interact_style = vtk.Interaction.Style.vtkInteractorStyleManipulator.newInstance();
55+
56+
const manips = {
57+
rot: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRotateManipulator.newInstance(),
58+
pan: vtk.Interaction.Manipulators.vtkMouseCameraTrackballPanManipulator.newInstance(),
59+
zoom1: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
60+
zoom2: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
61+
roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(),
62+
};
63+
64+
manips.zoom1.setControl(true);
65+
manips.zoom2.setButton(3);
66+
manips.roll.setShift(true);
67+
manips.pan.setButton(2);
68+
69+
for (var k in manips){{
70+
interact_style.addMouseManipulator(manips[k]);
71+
}};
72+
73+
const interactor = vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance();
74+
interactor.setView(openglRenderWindow);
75+
interactor.initialize();
76+
interactor.setInteractorStyle(interact_style);
77+
78+
document.addEventListener('DOMContentLoaded', function () {
79+
document.body.appendChild(rootContainer);
80+
interactor.bindEvents(document.body);
81+
});
82+
83+
function updateViewPort(element, renderer) {
84+
const { innerHeight, innerWidth } = window;
85+
const { x, y, width, height } = element.getBoundingClientRect();
86+
const viewport = [
87+
x / innerWidth,
88+
1 - (y + height) / innerHeight,
89+
(x + width) / innerWidth,
90+
1 - y / innerHeight,
91+
];
92+
renderer.setViewport(...viewport);
93+
}
94+
95+
function recomputeViewports() {
96+
const rendererElems = document.querySelectorAll('.renderer');
97+
for (let i = 0; i < rendererElems.length; i++) {
98+
const elem = rendererElems[i];
99+
const { id } = elem;
100+
const renderer = RENDERERS[id];
101+
updateViewPort(elem, renderer);
102+
}
103+
renderWindow.render();
104+
}
105+
106+
function resize() {
107+
rootContainer.style.width = `${window.innerWidth}px`;
108+
openglRenderWindow.setSize(window.innerWidth, window.innerHeight);
109+
recomputeViewports();
110+
}
111+
112+
window.addEventListener('resize', resize);
113+
document.addEventListener('scroll', recomputeViewports);
114+
115+
116+
function enterCurrentRenderer(e) {
117+
interact_style.setEnabled(true);
118+
interactor.setCurrentRenderer(RENDERERS[e.target.id]);
119+
}
120+
121+
function exitCurrentRenderer(e) {
122+
interactor.setCurrentRenderer(null);
123+
interact_style.setEnabled(false);
124+
}
125+
126+
127+
function applyStyle(element) {
128+
element.classList.add('renderer');
129+
element.style.width = '100%';
130+
element.style.height = '100%';
131+
element.style.display = 'inline-block';
132+
element.style.boxSizing = 'border';
133+
return element;
134+
}
135+
136+
window.addEventListener('load', resize);
137+
138+
function render(data, parent_element, ratio){
139+
140+
// Initial setup
141+
const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({ background: [1, 1, 1 ] });
142+
143+
// iterate over all children children
144+
for (var el of data){
145+
var trans = el.position;
146+
var rot = el.orientation;
147+
var rgba = el.color;
148+
var shape = el.shape;
149+
150+
// load the inline data
151+
var reader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance();
152+
const textEncoder = new TextEncoder();
153+
reader.parseAsArrayBuffer(textEncoder.encode(shape));
154+
155+
// setup actor,mapper and add
156+
const mapper = vtk.Rendering.Core.vtkMapper.newInstance();
157+
mapper.setInputConnection(reader.getOutputPort());
158+
159+
const actor = vtk.Rendering.Core.vtkActor.newInstance();
160+
actor.setMapper(mapper);
161+
162+
// set color and position
163+
actor.getProperty().setColor(rgba.slice(0,3));
164+
actor.getProperty().setOpacity(rgba[3]);
165+
166+
actor.rotateZ(rot[2]*180/Math.PI);
167+
actor.rotateY(rot[1]*180/Math.PI);
168+
actor.rotateX(rot[0]*180/Math.PI);
169+
170+
actor.setPosition(trans);
171+
172+
renderer.addActor(actor);
173+
174+
};
175+
176+
//add the container
177+
const container = applyStyle(document.createElement("div"));
178+
parent_element.appendChild(container);
179+
container.addEventListener('mouseenter', enterCurrentRenderer);
180+
container.addEventListener('mouseleave', exitCurrentRenderer);
181+
container.id = ID;
182+
183+
renderWindow.addRenderer(renderer);
184+
updateViewPort(container, renderer);
185+
renderer.resetCamera();
186+
187+
RENDERERS[ID] = renderer;
188+
ID++;
189+
};
190+
"""
191+
192+
193+
template_vtk = """
194+
195+
.. raw:: html
196+
197+
<div class="cq-vtk"
198+
style="text-align:{txt_align}s;float:left;border: 1px solid #ddd; width:{width}; height:{height}"">
199+
<script>
200+
var parent_element = {element};
201+
var data = {data};
202+
render(data, parent_element);
203+
</script>
204+
</div>
205+
<div style="clear:both;">
206+
</div>
207+
208+
"""
209+
24210

25211
class cq_directive(Directive):
26212

@@ -84,9 +270,91 @@ def run(self):
84270
return []
85271

86272

273+
class cq_directive_vtk(Directive):
274+
275+
has_content = True
276+
required_arguments = 0
277+
optional_arguments = 2
278+
option_spec = {
279+
"height": directives.length_or_unitless,
280+
"width": directives.length_or_percentage_or_unitless,
281+
"align": directives.unchanged,
282+
"select": directives.unchanged,
283+
}
284+
285+
def run(self):
286+
287+
options = self.options
288+
content = self.content
289+
state_machine = self.state_machine
290+
env = self.state.document.settings.env
291+
build_path = Path(env.app.builder.outdir)
292+
out_path = build_path / "_static"
293+
294+
# only consider inline snippets
295+
plot_code = "\n".join(content)
296+
297+
# collect the result
298+
try:
299+
result = cqgi.parse(plot_code).build()
300+
301+
if result.success:
302+
if result.first_result:
303+
shape = result.first_result.shape
304+
else:
305+
shape = result.env[options.get("select", "result")]
306+
307+
if isinstance(shape, Assembly):
308+
assy = shape
309+
else:
310+
assy = Assembly(shape, color=Color(*DEFAULT_COLOR))
311+
else:
312+
raise result.exception
313+
314+
except Exception:
315+
traceback.print_exc()
316+
assy = Assembly(Compound.makeText("CQGI error", 10, 5))
317+
318+
# save vtkjs to static
319+
fname = Path(str(uuid()))
320+
exporters.assembly.exportVTKJS(assy, out_path / fname)
321+
fname = str(fname) + ".zip"
322+
323+
# add the output
324+
lines = []
325+
326+
data = dumps(toJSON(assy))
327+
328+
lines.extend(
329+
template_vtk.format(
330+
code=indent(TEMPLATE_RENDER.format(), " "),
331+
data=data,
332+
ratio="null",
333+
element="document.currentScript.parentNode",
334+
txt_align=options.get("align", "left"),
335+
width=options.get("width", "100%"),
336+
height=options.get("height", "500px"),
337+
).splitlines()
338+
)
339+
340+
lines.extend(["::", ""])
341+
lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")])
342+
lines.append("")
343+
344+
if len(lines):
345+
state_machine.insert_input(lines, state_machine.input_lines.source(0))
346+
347+
return []
348+
349+
87350
def setup(app):
88351
setup.app = app
89352
setup.config = app.config
90353
setup.confdir = app.confdir
91354

92355
app.add_directive("cq_plot", cq_directive)
356+
app.add_directive("cadquery", cq_directive_vtk)
357+
358+
# add vtk.js
359+
app.add_js_file("vtk.js")
360+
app.add_js_file(None, body=rendering_code)

cadquery/cqgi.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,14 @@ def build(self, build_parameters=None, build_options=None):
117117
exec(c, env)
118118
result.set_debug(collector.debugObjects)
119119
result.set_success_result(collector.outputObjects)
120+
result.env = env
120121

121122
except Exception as ex:
122123
result.set_failure_result(ex)
123124

124125
end = time.perf_counter()
125126
result.buildTime = end - start
127+
126128
return result
127129

128130
def set_param_values(self, params):
@@ -322,12 +324,14 @@ def __init__(self):
322324
self.outputObjects = []
323325
self.debugObjects = []
324326

325-
def show_object(self, shape, options={}):
327+
def show_object(self, shape, options={}, **kwargs):
326328
"""
327329
return an object to the executing environment, with options
328330
:param shape: a cadquery object
329331
:param options: a dictionary of options that will be made available to the executing environment
330332
"""
333+
options.update(kwargs)
334+
331335
o = ShapeResult()
332336
o.options = options
333337
o.shape = shape

0 commit comments

Comments
 (0)