%reload_ext importnb
from importnb import Notebook
import pandas as pd
import cadquery as cq
import traitlets as T
import ipywidgets as W
import pythreejs as TJ
import numpy as np
from IPython.display import SVG
jupyter labextension install jupyter-threejs
Under the hood, cadquery
'sFreeCad
dependency chain is actually quite deep, while the widget frontend engine, ThreeJS may make your JupyterLab frontend quite large.
Using BufferGeometry
and BufferAttribute
is very attractive, but needs to wait for the next release. FreeCAD will also offer some no-copy numpy arrays... in the next release. We'll also take a look at some other options like vtk.js
in the future.
import __Revisiting_cadquery_and_ipywidgets_part_1 as CAD
if __name__ == "__main__":
part = CAD.make_a_box_with_a_hole()
One of the important features of a CAD kernel is meshing, taking geometric primitives and turning them into the facets that can be rendered efficiently.
def get_vertices_and_faces_of_a_part(part, tolerance=0.001):
vertices, faces = part.findSolid().wrapped.tessellate(tolerance)
return np.array(vertices, dtype=np.float32), np.array(faces, dtype=np.int32)
if __name__ == "__main__":
%timeit get_vertices_and_faces_of_a_part(part)
Even the simple part above makes a lot of geometry.
if __name__ == "__main__":
display(*list(map(lambda x: x.shape, get_vertices_and_faces_of_a_part(part))))
Here's a widget that will render a single cadquery
Part in a pythreejs renderer.
class THREECAD(W.HBox):
part = T.Instance(cq.CQ, allow_none=True)
mesh = T.Instance(TJ.Mesh)
lights = T.Tuple()
camera = T.Instance(TJ.Camera)
scene = T.Instance(TJ.Scene)
renderer = T.Instance(TJ.Renderer)
tolerance = T.Float(0.1)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.part = cq.Workplane("XY").box(*([1] * 3))
vertices, faces = get_vertices_and_faces_of_a_part(self.part, self.tolerance)
self.mesh = TJ.Mesh(
geometry=TJ.Geometry(
vertices=vertices.tolist(),
faces=faces.tolist()
),
material=TJ.MeshLambertMaterial(
wireframe=True,
wireframeLinewidth=2,
opacity=0.1,
transparent=True,
),
)
self.lights = [
TJ.PointLight(
color='white',
position=[20] * 3,
intensity=0.75,
target=self.mesh,
),
TJ.AmbientLight(color='#000000'),
]
self.camera = TJ.PerspectiveCamera(position=[30] * 3)
self.scene = TJ.Scene(children=[
*self.lights,
self.camera,
self.mesh,
])
self.renderer = TJ.Renderer(
scene=self.scene,
camera=self.camera,
width=400,
height=400,
controls=[TJ.OrbitControls(controlling=self.camera)],
)
self.children = [self.renderer]
self.observe(self._on_part_changed, "part")
def _on_part_changed(self, change=None):
vertices, faces = get_vertices_and_faces_of_a_part(self.part, self.tolerance)
self.mesh.geometry = TJ.Geometry(vertices=vertices.tolist(), faces=faces.tolist())
if __name__ == "__main__":
try: del cad
except: pass
cad = THREECAD()
cad.part = part
display(cad)
Let's try the classic OCC Bottle in the 3d renderer.
if __name__ == "__main__":
def interactively_make_a_bottle_in_3d(
length=(0.1, 20.0),
width=(0.1, 10.0),
extent=(0.1, 6.0),
height=(0.1, 40.0),
neck_width=(0.1, 3.0),
neck_height=(0.1, 6.0),
shell=(0.01, 1.0),
):
print("mesh")
#cad.part = CAD.make_a_box_with_a_hole()
cad.part = CAD.make_a_bottle_with_a_hole(
length, width, extent, height, neck_width, neck_height, shell
)
bottle = W.interact(interactively_make_a_bottle_in_3d)
if __name__ == "__main__":
cad.renderer.width = cad.renderer.height = 800
display(cad)