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:
Adam Urbańczyk
2021-06-22 10:06:50 +02:00
committed by GitHub
parent 7b1a99ca8a
commit e00ac83f98
26 changed files with 9395 additions and 47 deletions

View File

@ -1,6 +1,8 @@
[run]
branch = True
omit = cadquery/utils.py
omit =
cadquery/utils.py
cadquery/cq_directive.py
[report]
exclude_lines =

View File

@ -1,2 +1,2 @@
#!/bin/sh
sphinx-build -b html doc target/docs
(cd doc && sphinx-build -b html . ../target/docs)

View File

@ -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_()

View File

@ -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 "&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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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()

View 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()

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -27,6 +27,7 @@ test:
requires:
- pytest
- docutils
- path
source_files:
- tests/
commands:

3
doc/_static/vtk.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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
===========

View File

@ -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"

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ dependencies:
- typing_extensions
- nptyping
- scipy
- path
- pip
- pip:
- "--editable=."

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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())