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
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
[run]
|
||||
branch = True
|
||||
omit = cadquery/utils.py
|
||||
omit =
|
||||
cadquery/utils.py
|
||||
cadquery/cq_directive.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
sphinx-build -b html doc target/docs
|
||||
(cd doc && sphinx-build -b html . ../target/docs)
|
||||
|
||||
@ -519,3 +519,12 @@ class Assembly(object):
|
||||
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_()
|
||||
|
||||
@ -4024,7 +4024,7 @@ class Workplane(object):
|
||||
|
||||
return self.newObject(rv)
|
||||
|
||||
def _repr_html_(self) -> Any:
|
||||
def _repr_javascript_(self) -> Any:
|
||||
"""
|
||||
Special method for rendering current object in a jupyter notebook
|
||||
"""
|
||||
@ -4032,7 +4032,9 @@ class Workplane(object):
|
||||
if type(self.val()) is Vector:
|
||||
return "< {} >".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
|
||||
|
||||
@ -4,8 +4,19 @@ A special directive for including a cq object.
|
||||
"""
|
||||
|
||||
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 = """
|
||||
@ -21,6 +32,181 @@ template = """
|
||||
"""
|
||||
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):
|
||||
|
||||
@ -84,9 +270,91 @@ class cq_directive(Directive):
|
||||
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)
|
||||
|
||||
@ -117,12 +117,14 @@ class CQModel(object):
|
||||
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):
|
||||
@ -322,12 +324,14 @@ class ScriptCallback(object):
|
||||
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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Iterable, Tuple, Dict, overload, Optional
|
||||
from typing import Iterable, Tuple, Dict, overload, Optional, Any, List
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from OCP.TDocStd import TDocStd_Document
|
||||
@ -10,8 +10,11 @@ from OCP.TDF import TDF_Label
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.Quantity import Quantity_ColorRGBA
|
||||
|
||||
from vtk import vtkActor, vtkPolyDataMapper as vtkMapper, vtkRenderer
|
||||
|
||||
from .geom import Location
|
||||
from .shapes import Shape, Compound
|
||||
from .exporters.vtk import toString
|
||||
|
||||
|
||||
class Color(object):
|
||||
@ -60,6 +63,15 @@ class Color(object):
|
||||
else:
|
||||
raise ValueError(f"Unsupported arguments: {args}, {kwargs}")
|
||||
|
||||
def toTuple(self) -> Tuple[float, float, float, float]:
|
||||
"""
|
||||
Convert Color to RGB tuple.
|
||||
"""
|
||||
a = self.wrapped.Alpha()
|
||||
rgb = self.wrapped.GetRGB()
|
||||
|
||||
return (rgb.Red(), rgb.Green(), rgb.Blue(), a)
|
||||
|
||||
|
||||
class AssemblyProtocol(Protocol):
|
||||
@property
|
||||
@ -155,3 +167,71 @@ def toCAF(
|
||||
tool.UpdateAssemblies()
|
||||
|
||||
return top, doc
|
||||
|
||||
|
||||
def toVTK(
|
||||
assy: AssemblyProtocol,
|
||||
renderer: vtkRenderer = vtkRenderer(),
|
||||
loc: Location = Location(),
|
||||
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
||||
tolerance: float = 1e-3,
|
||||
) -> vtkRenderer:
|
||||
|
||||
loc = loc * assy.loc
|
||||
trans, rot = loc.toTuple()
|
||||
|
||||
if assy.color:
|
||||
color = assy.color.toTuple()
|
||||
|
||||
if assy.shapes:
|
||||
data = Compound.makeCompound(assy.shapes).toVtkPolyData(tolerance)
|
||||
|
||||
mapper = vtkMapper()
|
||||
mapper.SetInputData(data)
|
||||
|
||||
actor = vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.SetPosition(*trans)
|
||||
actor.SetOrientation(*rot)
|
||||
actor.GetProperty().SetColor(*color[:3])
|
||||
actor.GetProperty().SetOpacity(color[3])
|
||||
|
||||
renderer.AddActor(actor)
|
||||
|
||||
for child in assy.children:
|
||||
renderer = toVTK(child, renderer, loc, color, tolerance)
|
||||
|
||||
return renderer
|
||||
|
||||
|
||||
def toJSON(
|
||||
assy: AssemblyProtocol,
|
||||
loc: Location = Location(),
|
||||
color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
||||
tolerance: float = 1e-3,
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
loc = loc * assy.loc
|
||||
trans, rot = loc.toTuple()
|
||||
|
||||
if assy.color:
|
||||
color = assy.color.toTuple()
|
||||
|
||||
rv = []
|
||||
|
||||
if assy.shapes:
|
||||
val: Any = {}
|
||||
|
||||
data = toString(Compound.makeCompound(assy.shapes), tolerance)
|
||||
|
||||
val["shape"] = data
|
||||
val["color"] = color
|
||||
val["position"] = trans
|
||||
val["orientation"] = rot
|
||||
|
||||
rv.append(val)
|
||||
|
||||
for child in assy.children:
|
||||
rv.extend(toJSON(child, loc, color, tolerance))
|
||||
|
||||
return rv
|
||||
|
||||
@ -15,6 +15,7 @@ from .svg import getSVG
|
||||
from .json import JsonMesh
|
||||
from .amf import AmfWriter
|
||||
from .dxf import exportDXF
|
||||
from .vtk import exportVTP
|
||||
from .utils import toCompound
|
||||
|
||||
|
||||
@ -26,9 +27,10 @@ class ExportTypes:
|
||||
TJS = "TJS"
|
||||
DXF = "DXF"
|
||||
VRML = "VRML"
|
||||
VTP = "VTP"
|
||||
|
||||
|
||||
ExportLiterals = Literal["STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML"]
|
||||
ExportLiterals = Literal["STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP"]
|
||||
|
||||
|
||||
def export(
|
||||
@ -107,6 +109,9 @@ def export(
|
||||
shape.mesh(tolerance, angularTolerance)
|
||||
VrmlAPI.Write_s(shape.wrapped, fname)
|
||||
|
||||
elif exportType == ExportTypes.VTP:
|
||||
exportVTP(shape, fname, tolerance, angularTolerance)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown export type")
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import os.path
|
||||
|
||||
from tempfile import TemporaryDirectory
|
||||
from shutil import make_archive
|
||||
|
||||
from vtk import vtkJSONSceneExporter, vtkRenderer, vtkRenderWindow, vtkVRMLExporter
|
||||
|
||||
from OCP.XSControl import XSControl_WorkSession
|
||||
from OCP.STEPCAFControl import STEPCAFControl_Writer
|
||||
from OCP.STEPControl import STEPControl_StepModelType
|
||||
@ -12,10 +17,13 @@ from OCP.XmlDrivers import (
|
||||
from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString
|
||||
from OCP.PCDM import PCDM_StoreStatus
|
||||
|
||||
from ..assembly import AssemblyProtocol, toCAF
|
||||
from ..assembly import AssemblyProtocol, toCAF, toVTK
|
||||
|
||||
|
||||
def exportAssembly(assy: AssemblyProtocol, path: str) -> bool:
|
||||
"""
|
||||
Export an assembly to a step a file.
|
||||
"""
|
||||
|
||||
_, doc = toCAF(assy, True)
|
||||
|
||||
@ -32,6 +40,9 @@ def exportAssembly(assy: AssemblyProtocol, path: str) -> bool:
|
||||
|
||||
|
||||
def exportCAF(assy: AssemblyProtocol, path: str) -> bool:
|
||||
"""
|
||||
Export an assembly to a OCAF xml file (internal OCCT format).
|
||||
"""
|
||||
|
||||
folder, fname = os.path.split(path)
|
||||
name, ext = os.path.splitext(fname)
|
||||
@ -61,3 +72,46 @@ def exportCAF(assy: AssemblyProtocol, path: str) -> bool:
|
||||
app.Close(doc)
|
||||
|
||||
return status == PCDM_StoreStatus.PCDM_SS_OK
|
||||
|
||||
|
||||
def _vtkRenderWindow(assy: AssemblyProtocol) -> vtkRenderWindow:
|
||||
"""
|
||||
Convert an assembly to a vtkRenderWindow. Used by vtk based exporters.
|
||||
"""
|
||||
|
||||
renderer = vtkRenderer()
|
||||
renderWindow = vtkRenderWindow()
|
||||
renderWindow.AddRenderer(renderer)
|
||||
toVTK(assy, renderer)
|
||||
|
||||
renderer.ResetCamera()
|
||||
renderer.SetBackground(1, 1, 1)
|
||||
|
||||
return renderWindow
|
||||
|
||||
|
||||
def exportVTKJS(assy: AssemblyProtocol, path: str):
|
||||
"""
|
||||
Export an assembly to a zipped vtkjs. NB: .zip extensions is added to path.
|
||||
"""
|
||||
|
||||
renderWindow = _vtkRenderWindow(assy)
|
||||
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
|
||||
exporter = vtkJSONSceneExporter()
|
||||
exporter.SetFileName(tmpdir)
|
||||
exporter.SetRenderWindow(renderWindow)
|
||||
exporter.Write()
|
||||
make_archive(path, "zip", tmpdir)
|
||||
|
||||
|
||||
def exportVRML(assy: AssemblyProtocol, path: str):
|
||||
"""
|
||||
Export an assembly to a vrml file using vtk.
|
||||
"""
|
||||
|
||||
exporter = vtkVRMLExporter()
|
||||
exporter.SetFileName(path)
|
||||
exporter.SetRenderWindow(_vtkRenderWindow(assy))
|
||||
exporter.Write()
|
||||
|
||||
24
cadquery/occ_impl/exporters/vtk.py
Normal file
24
cadquery/occ_impl/exporters/vtk.py
Normal file
@ -0,0 +1,24 @@
|
||||
from vtk import vtkXMLPolyDataWriter
|
||||
from ..shapes import Shape
|
||||
|
||||
|
||||
def exportVTP(
|
||||
shape: Shape, fname: str, tolerance: float = 0.1, angularTolerance: float = 0.1
|
||||
):
|
||||
|
||||
writer = vtkXMLPolyDataWriter()
|
||||
writer.SetFileName(fname)
|
||||
writer.SetInputData(shape.toVtkPolyData(tolerance, angularTolerance))
|
||||
writer.Write()
|
||||
|
||||
|
||||
def toString(
|
||||
shape: Shape, tolerance: float = 1e-3, angularTolerance: float = 0.1
|
||||
) -> str:
|
||||
|
||||
writer = vtkXMLPolyDataWriter()
|
||||
writer.SetWriteToOutputString(True)
|
||||
writer.SetInputData(shape.toVtkPolyData(tolerance, angularTolerance))
|
||||
writer.Write()
|
||||
|
||||
return writer.GetOutputString()
|
||||
@ -12,6 +12,7 @@ from OCP.gp import (
|
||||
gp_Trsf,
|
||||
gp_GTrsf,
|
||||
gp_XYZ,
|
||||
gp_EulerSequence,
|
||||
gp,
|
||||
)
|
||||
from OCP.Bnd import Bnd_Box
|
||||
@ -974,3 +975,15 @@ class Location(object):
|
||||
def __mul__(self, other: "Location") -> "Location":
|
||||
|
||||
return Location(self.wrapped * other.wrapped)
|
||||
|
||||
def toTuple(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]:
|
||||
"""Convert the location to a translation, rotation tuple."""
|
||||
|
||||
T = self.wrapped.Transformation()
|
||||
trans = T.TranslationPart()
|
||||
rot = T.GetRotation()
|
||||
|
||||
rv_trans = (trans.X(), trans.Y(), trans.Z())
|
||||
rv_rot = rot.GetEulerAngles(gp_EulerSequence.gp_Extrinsic_XYZ)
|
||||
|
||||
return rv_trans, rv_rot
|
||||
|
||||
@ -1,8 +1,178 @@
|
||||
from IPython.display import SVG
|
||||
from typing import Dict, Any, List
|
||||
from json import dumps
|
||||
|
||||
from .exporters.svg import getSVG
|
||||
from IPython.display import Javascript
|
||||
|
||||
|
||||
from .exporters.vtk import toString
|
||||
from .shapes import Shape
|
||||
from ..assembly import Assembly
|
||||
from .assembly import toJSON
|
||||
|
||||
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());
|
||||
|
||||
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});
|
||||
}});
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def display(shape):
|
||||
|
||||
return SVG(getSVG(shape))
|
||||
payload: List[Dict[str, Any]] = []
|
||||
|
||||
if isinstance(shape, Shape):
|
||||
payload.append(
|
||||
dict(
|
||||
shape=toString(shape),
|
||||
color=DEFAULT_COLOR,
|
||||
position=[0, 0, 0],
|
||||
orientation=[0, 0, 0],
|
||||
)
|
||||
)
|
||||
elif isinstance(shape, Assembly):
|
||||
payload = toJSON(shape)
|
||||
else:
|
||||
raise ValueError(f"Type {type(shape)} is not supported")
|
||||
|
||||
code = TEMPLATE.format(data=dumps(payload), element="element", ratio=0.5)
|
||||
|
||||
return Javascript(code)
|
||||
|
||||
@ -15,6 +15,14 @@ from typing import (
|
||||
)
|
||||
from typing_extensions import Literal, Protocol
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from vtk import (
|
||||
vtkPolyData,
|
||||
vtkTriangleFilter,
|
||||
vtkPolyDataNormals,
|
||||
)
|
||||
|
||||
from .geom import Vector, BoundBox, Plane, Location, Matrix
|
||||
|
||||
import OCP.TopAbs as ta # Tolopolgy type enum
|
||||
@ -56,7 +64,7 @@ from OCP.BRepAdaptor import (
|
||||
BRepAdaptor_HCurve,
|
||||
BRepAdaptor_HCompCurve,
|
||||
)
|
||||
from OCP.Adaptor3d import Adaptor3d_Curve, Adaptor3d_HCurve
|
||||
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakeVertex,
|
||||
BRepBuilderAPI_MakeEdge,
|
||||
@ -76,7 +84,6 @@ from OCP.BRepBuilderAPI import (
|
||||
# properties used to store mass calculation result
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.BRepGProp import BRepGProp_Face, BRepGProp # used for mass calculation
|
||||
from OCP.BRepLProp import BRepLProp_CLProps # local curve properties
|
||||
|
||||
from OCP.BRepPrimAPI import (
|
||||
BRepPrimAPI_MakeBox,
|
||||
@ -92,7 +99,7 @@ from OCP.BRepPrimAPI import (
|
||||
from OCP.TopExp import TopExp_Explorer # Toplogy explorer
|
||||
|
||||
# used for getting underlying geoetry -- is this equvalent to brep adaptor?
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRep import BRep_Tool, BRep_Builder
|
||||
|
||||
from OCP.TopoDS import (
|
||||
TopoDS,
|
||||
@ -126,7 +133,6 @@ from OCP.BRepAlgoAPI import (
|
||||
BRepAlgoAPI_Cut,
|
||||
BRepAlgoAPI_BooleanOperation,
|
||||
BRepAlgoAPI_Splitter,
|
||||
BRepAlgoAPI_BuilderAlgo,
|
||||
)
|
||||
|
||||
from OCP.Geom import (
|
||||
@ -217,6 +223,9 @@ from OCP.GeomFill import (
|
||||
GeomFill_TrihedronLaw,
|
||||
)
|
||||
|
||||
from OCP.IVtkOCC import IVtkOCC_Shape, IVtkOCC_ShapeMesher
|
||||
from OCP.IVtkVTK import IVtkVTK_ShapeData
|
||||
|
||||
# for catching exceptions
|
||||
from OCP.Standard import Standard_NoSuchObject, Standard_Failure
|
||||
|
||||
@ -448,12 +457,29 @@ class Shape(object):
|
||||
|
||||
return writer.Write(fileName)
|
||||
|
||||
def exportBrep(self, fileName: str) -> bool:
|
||||
def exportBrep(self, f: Union[str, BytesIO]) -> bool:
|
||||
"""
|
||||
Export this shape to a BREP file
|
||||
"""
|
||||
|
||||
return BRepTools.Write_s(self.wrapped, fileName)
|
||||
rv = BRepTools.Write_s(self.wrapped, f)
|
||||
|
||||
return True if rv is None else rv
|
||||
|
||||
@classmethod
|
||||
def importBrep(cls, f: Union[str, BytesIO]) -> "Shape":
|
||||
"""
|
||||
Import shape from a BREP file
|
||||
"""
|
||||
s = TopoDS_Shape()
|
||||
builder = BRep_Builder()
|
||||
|
||||
BRepTools.Read_s(s, f, builder)
|
||||
|
||||
if s.IsNull():
|
||||
raise ValueError(f"Could not import {f}")
|
||||
|
||||
return cls.cast(s)
|
||||
|
||||
def geomType(self) -> Geoms:
|
||||
"""
|
||||
@ -1072,14 +1098,51 @@ class Shape(object):
|
||||
|
||||
return vertices, triangles
|
||||
|
||||
def _repr_html_(self):
|
||||
def toVtkPolyData(
|
||||
self, tolerance: float, angularTolerance: float = 0.1, normals: bool = True
|
||||
) -> vtkPolyData:
|
||||
"""
|
||||
Convert shape to vtkPolyData
|
||||
"""
|
||||
|
||||
vtk_shape = IVtkOCC_Shape(self.wrapped)
|
||||
shape_data = IVtkVTK_ShapeData()
|
||||
shape_mesher = IVtkOCC_ShapeMesher(
|
||||
tolerance, angularTolerance, theNbUIsos=0, theNbVIsos=0
|
||||
)
|
||||
|
||||
shape_mesher.Build(vtk_shape, shape_data)
|
||||
|
||||
rv = shape_data.getVtkPolyData()
|
||||
|
||||
# convert to traingles and split edges
|
||||
t_filter = vtkTriangleFilter()
|
||||
t_filter.SetInputData(rv)
|
||||
t_filter.Update()
|
||||
|
||||
rv = t_filter.GetOutput()
|
||||
|
||||
# compute normals
|
||||
if normals:
|
||||
n_filter = vtkPolyDataNormals()
|
||||
n_filter.SetComputePointNormals(True)
|
||||
n_filter.SetComputeCellNormals(True)
|
||||
n_filter.SetFeatureAngle(360)
|
||||
n_filter.SetInputData(rv)
|
||||
n_filter.Update()
|
||||
|
||||
rv = n_filter.GetOutput()
|
||||
|
||||
return rv
|
||||
|
||||
def _repr_javascript_(self):
|
||||
"""
|
||||
Jupyter 3D representation support
|
||||
"""
|
||||
|
||||
from .jupyter_tools import display
|
||||
|
||||
return display(self)
|
||||
return display(self)._repr_javascript_()
|
||||
|
||||
|
||||
class ShapeProtocol(Protocol):
|
||||
|
||||
@ -27,6 +27,7 @@ test:
|
||||
requires:
|
||||
- pytest
|
||||
- docutils
|
||||
- path
|
||||
source_files:
|
||||
- tests/
|
||||
commands:
|
||||
|
||||
3
doc/_static/vtk.js
vendored
Normal file
3
doc/_static/vtk.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -205,7 +205,8 @@ Final result
|
||||
|
||||
Below is the complete code including the final solve step.
|
||||
|
||||
.. code-block:: python
|
||||
.. cadquery::
|
||||
:height: 600px
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
@ -354,10 +355,6 @@ Below is the complete code including the final solve step.
|
||||
|
||||
show_object(door,name='door')
|
||||
|
||||
This code generates the following assembly.
|
||||
|
||||
.. image:: _static/door_assy.png
|
||||
|
||||
|
||||
Data export
|
||||
===========
|
||||
|
||||
@ -44,17 +44,12 @@ extensions = [
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.autosummary",
|
||||
"cadquery.cq_directive",
|
||||
"sphinxcadquery.sphinxcadquery",
|
||||
]
|
||||
|
||||
always_document_param_types = True
|
||||
|
||||
# Configure `sphinxcadquery`
|
||||
sphinxcadquery_include_source = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = ".rst"
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ Export Formats
|
||||
* AMF
|
||||
* TJS
|
||||
* VRML
|
||||
* VTP
|
||||
|
||||
Notes on the Formats
|
||||
#######################
|
||||
@ -38,6 +39,7 @@ Notes on the Formats
|
||||
* STL and AMF files are mesh-based formats which are typically used in additive manufacturing (i.e. 3D printing). AMF files support more features, but are not as universally supported as STL files.
|
||||
* TJS is short for ThreeJS, and is a JSON mesh format that is useful for displaying 3D models in web browsers. The TJS format is used to display embedded 3D examples within the CadQuery documentation.
|
||||
* VRML is a mesh-based format for representing interactive 3D objects in a web browser.
|
||||
* VTP is a mesh-based format used by the VTK library.
|
||||
|
||||
Importing DXF
|
||||
##############
|
||||
|
||||
8556
doc/vslot-2020_1.dxf
Normal file
8556
doc/vslot-2020_1.dxf
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@ dependencies:
|
||||
- typing_extensions
|
||||
- nptyping
|
||||
- scipy
|
||||
- path
|
||||
- pip
|
||||
- pip:
|
||||
- "--editable=."
|
||||
|
||||
7
mypy.ini
7
mypy.ini
@ -18,3 +18,10 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-nptyping.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-vtk.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-docutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
@ -3,8 +3,13 @@ import os
|
||||
from itertools import product
|
||||
|
||||
import cadquery as cq
|
||||
from cadquery.occ_impl.exporters.assembly import exportAssembly, exportCAF
|
||||
|
||||
from cadquery.occ_impl.exporters.assembly import (
|
||||
exportAssembly,
|
||||
exportCAF,
|
||||
exportVTKJS,
|
||||
exportVRML,
|
||||
)
|
||||
from cadquery.occ_impl.assembly import toJSON
|
||||
from OCP.gp import gp_XYZ
|
||||
|
||||
|
||||
@ -44,6 +49,17 @@ def nested_assy():
|
||||
return assy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_top_assy():
|
||||
|
||||
b1 = cq.Workplane().box(1, 1, 1)
|
||||
|
||||
assy = cq.Assembly()
|
||||
assy.add(b1, color=cq.Color("green"))
|
||||
|
||||
return assy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def box_and_vertex():
|
||||
|
||||
@ -115,6 +131,33 @@ def test_native_export(simple_assy):
|
||||
assert os.path.exists("assy.xml")
|
||||
|
||||
|
||||
def test_vtkjs_export(nested_assy):
|
||||
|
||||
exportVTKJS(nested_assy, "assy")
|
||||
|
||||
# only sanity check for now
|
||||
assert os.path.exists("assy.zip")
|
||||
|
||||
|
||||
def test_vrml_export(simple_assy):
|
||||
|
||||
exportVRML(simple_assy, "assy.wrl")
|
||||
|
||||
# only sanity check for now
|
||||
assert os.path.exists("assy.wrl")
|
||||
|
||||
|
||||
def test_toJSON(simple_assy, nested_assy, empty_top_assy):
|
||||
|
||||
r1 = toJSON(simple_assy)
|
||||
r2 = toJSON(simple_assy)
|
||||
r3 = toJSON(empty_top_assy)
|
||||
|
||||
assert len(r1) == 3
|
||||
assert len(r2) == 3
|
||||
assert len(r3) == 1
|
||||
|
||||
|
||||
def test_save(simple_assy, nested_assy):
|
||||
|
||||
simple_assy.save("simple.step")
|
||||
|
||||
@ -4571,3 +4571,27 @@ class TestCadQuery(BaseTest):
|
||||
self.assertEqual(len(edges), len(vertices) + 1)
|
||||
endpoints = [e.endPoint() for e in edges]
|
||||
self.assertTrue(all([v in endpoints for v in vecs]))
|
||||
|
||||
def testBrepImportExport(self):
|
||||
|
||||
# import/export to file
|
||||
s = Workplane().box(1, 1, 1).val()
|
||||
|
||||
s.exportBrep("test.brep")
|
||||
si = Shape.importBrep("test.brep")
|
||||
|
||||
self.assertTrue(si.isValid())
|
||||
self.assertAlmostEqual(si.Volume(), 1)
|
||||
|
||||
# import/export to BytesIO
|
||||
from io import BytesIO
|
||||
|
||||
bio = BytesIO()
|
||||
|
||||
s.exportBrep(bio)
|
||||
bio.seek(0)
|
||||
|
||||
si = Shape.importBrep("test.brep")
|
||||
|
||||
self.assertTrue(si.isValid())
|
||||
self.assertAlmostEqual(si.Volume(), 1)
|
||||
|
||||
@ -3,7 +3,9 @@ import pytest
|
||||
from glob import glob
|
||||
from itertools import chain, count
|
||||
|
||||
from docutils.parsers.rst import directives, Directive
|
||||
from path import Path
|
||||
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.core import publish_doctree
|
||||
from docutils.utils import Reporter
|
||||
|
||||
@ -12,16 +14,16 @@ from cadquery import cqgi
|
||||
from cadquery.cq_directive import cq_directive
|
||||
|
||||
|
||||
def find_examples(pattern="examples/*.py"):
|
||||
def find_examples(pattern="examples/*.py", path=Path("examples")):
|
||||
|
||||
for p in glob(pattern):
|
||||
with open(p, encoding="UTF-8") as f:
|
||||
code = f.read()
|
||||
|
||||
yield code
|
||||
yield code, path
|
||||
|
||||
|
||||
def find_examples_in_docs(pattern="doc/*.rst"):
|
||||
def find_examples_in_docs(pattern="doc/*.rst", path=Path("doc")):
|
||||
|
||||
# dummy CQ directive for code
|
||||
class dummy_cq_directive(cq_directive):
|
||||
@ -48,16 +50,17 @@ def find_examples_in_docs(pattern="doc/*.rst"):
|
||||
# yield all code snippets
|
||||
for c in dummy_cq_directive.codes:
|
||||
|
||||
yield c
|
||||
yield c, path
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code", chain(find_examples(), find_examples_in_docs()), ids=count(0)
|
||||
"code, path", chain(find_examples(), find_examples_in_docs()), ids=count(0)
|
||||
)
|
||||
def test_example(code):
|
||||
def test_example(code, path):
|
||||
|
||||
# build
|
||||
res = cqgi.parse(code).build()
|
||||
with path:
|
||||
res = cqgi.parse(code).build()
|
||||
|
||||
assert res.exception is None
|
||||
|
||||
|
||||
@ -98,6 +98,15 @@ class TestExporters(BaseTest):
|
||||
# export again to trigger all paths in the code
|
||||
exporters.export(self._box(), "out.vrml")
|
||||
|
||||
def testVTP(self):
|
||||
|
||||
exporters.export(self._box(), "out.vtp")
|
||||
|
||||
with open("out.vtp") as f:
|
||||
res = f.read(100)
|
||||
|
||||
assert res.startswith('<?xml version="1.0"?>\n<VTKFile')
|
||||
|
||||
def testDXF(self):
|
||||
|
||||
exporters.export(self._box().section(), "out.dxf")
|
||||
|
||||
@ -1,14 +1,27 @@
|
||||
from tests import BaseTest
|
||||
|
||||
import cadquery
|
||||
import cadquery as cq
|
||||
from cadquery.occ_impl.jupyter_tools import display
|
||||
|
||||
|
||||
class TestJupyter(BaseTest):
|
||||
def test_repr_html(self):
|
||||
cube = cadquery.Workplane("XY").box(1, 1, 1)
|
||||
def test_repr_javascript(self):
|
||||
cube = cq.Workplane("XY").box(1, 1, 1)
|
||||
assy = cq.Assembly().add(cube)
|
||||
shape = cube.val()
|
||||
self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid)
|
||||
|
||||
# Test no exception on rendering to html
|
||||
html = shape._repr_html_()
|
||||
# TODO: verification improvement: test for valid html
|
||||
self.assertIsInstance(shape, cq.occ_impl.shapes.Solid)
|
||||
|
||||
# Test no exception on rendering to js
|
||||
js1 = shape._repr_javascript_()
|
||||
js2 = cube._repr_javascript_()
|
||||
js3 = assy._repr_javascript_()
|
||||
|
||||
assert "function render" in js1
|
||||
assert "function render" in js2
|
||||
assert "function render" in js3
|
||||
|
||||
def test_display_error(self):
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
display(cq.Vector())
|
||||
|
||||
Reference in New Issue
Block a user