from dataclasses import dataclass
from typing import Tuple, List
import cadquery as cq
import numpy as np
import ocl
from OCP.BRep import BRep_Tool
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.TopAbs import TopAbs_FACE
from OCP.TopExp import TopExp_Explorer
from OCP.TopLoc import TopLoc_Location
from OCP.gp import gp_Pnt
from cq_cam.commands.base_command import Unit
from cq_cam.commands.command import Rapid, Plunge, Cut
from cq_cam.job import Job
from cq_cam.operations.base_operation import FaceBaseOperation
from cq_cam.operations.strategy import ZigZagStrategy
from cq_cam.utils import utils
from cq_cam.utils.utils import flatten_list
from cq_cam.visualize import visualize_task
[docs]@dataclass
class Surface3D(FaceBaseOperation):
tool: ocl.MillingCutter = ocl.CylCutter(3.175, 15)
interpolation_step: float = 0.5
@property
def _tool_diameter(self) -> float:
return self.tool.getDiameter()
def __post_init__(self):
"""
The 3D job can work very similar to 2D pocket:
1) Collect all the faces to get the work boundary
2) Generate cut sequences
3) Split cut sequences into sufficiently small sections
3) Use OpenCAMLib to calculate the depth for each section
4) Proceed generating multiple depths taking section depths into consideration
:return:
"""
super().__post_init__()
faces = self.transform_shapes_to_global(self._faces)
compound = cq.Workplane().add(faces).combine().objects[0]
bb = compound.BoundingBox()
projected_faces = [utils.project_face(face) for face in faces]
combine_result = cq.Workplane().add(projected_faces).combine().objects[0]
if isinstance(combine_result, cq.Compound):
base_boundaries = self.break_compound_to_faces(combine_result)
else:
base_boundaries = [combine_result]
op_boundaries = flatten_list([self.offset_boundary(boundary) for boundary in base_boundaries])
for op_boundary in op_boundaries:
outer_boundary = op_boundary.outerWire()
inner_boundaries = op_boundary.innerWires() # TODO is this needed?
cut_sequences = ZigZagStrategy.process(self, [outer_boundary], [])
def interpolate_cut_sequence(cut_sequence):
interpolated = [cut_sequence[0]]
v1 = cq.Vector(cut_sequence[0])
for p2 in cut_sequence[1:]:
v2 = cq.Vector(p2)
v = v2 - v1
u = v.normalized()
l = v.Length
for step in np.arange(0, l, self.interpolation_step):
step_v = v1 + u * step
interpolated.append((step_v.x, step_v.y))
interpolated.append(p2)
v1 = v2
return interpolated
# Note, this interpolation doesn't consider depth at all
# Ideally we'd have a point at every depth boundary (TODO?)
# Should be kinda easy?
interpolated_cut_sequences = [interpolate_cut_sequence(cut_sequence) for cut_sequence in cut_sequences]
cl_points = []
for cut_sequence in interpolated_cut_sequences:
for point in cut_sequence:
cl_point = ocl.CLPoint(point[0], point[1], bb.zmin)
cl_points.append(cl_point)
triangles = self.shape_to_triangles(compound)
stl_surf = ocl.STLSurf()
for triangle in triangles:
points = (ocl.Point(*vertex) for vertex in triangle)
tri = ocl.Triangle(*points)
stl_surf.addTriangle(tri)
# TODO add post optimization for the moves (detect linear sequences)
op = ocl.BatchDropCutter()
op.setCutter(self.tool)
for cl_point in cl_points:
op.appendPoint(cl_point)
op.setSTL(stl_surf)
op.run()
# Merge bottom height data
result_points = [cl_point_to_tuple(point) for point in op.getCLPoints()]
i = 0
for cut_sequence in interpolated_cut_sequences:
for j, point in enumerate(cut_sequence):
cut_sequence[j] = (*point, result_points[i][2])
i += 1
bottom_height = bb.zmin
if self.stepdown:
depths = list(np.arange(self.top_height + self.stepdown, bottom_height, self.stepdown))
if depths[-1] != bottom_height:
depths.append(bottom_height)
else:
depths = [bottom_height]
for i, depth in enumerate(depths):
# We want to include i-2 - otherwise we get gaps between depths
last_last_depth = depths[i - 2] if i > 1 else 0
depth_cut_sequences = self._chop_sequences_by_depth(interpolated_cut_sequences, last_last_depth)
# TODO optimize order of cut sequences to minimize rapid distances
# note to self: in zigzag, I guess maintaining the order is the best bet
# TODO if there is a new cut sequence within radius of max_step then use it without retracting
for cut_sequence in depth_cut_sequences:
cut_start = cut_sequence[0]
self.commands.append(Rapid(x=None, y=None, z=self.clearance_height))
self.commands.append(Rapid(x=cut_start[0], y=cut_start[1], z=None))
self.commands.append(Rapid(x=None, y=None, z=self.top_height)) # TODO plunge or rapid?
self.commands.append(Plunge(z=cut_start[2]))
for cut in cut_sequence[1:]:
self.commands.append(Cut(x=cut[0], y=cut[1], z=max(depth, cut[2])))
# for i, base_boundary in enumerate(base_boundaries):
# show_object(base_boundary, f'base_boundary-{i}')
#
# for i, op_boundary in enumerate(op_boundaries):
# show_object(op_boundary, f'op_boundary-{i}')
#
# show_object(faces, 'depth_boundary')
@classmethod
def shape_to_triangles(cls,
shape: cq.Shape,
tolerance: float = 1e-3,
angular_tolerance: float = 0.1) -> List[Tuple[Tuple[float, float, float], ...]]:
# BRepMesh_IncrementalMesh gets mad if you try to pass a Compound to it
if isinstance(shape, cq.Compound):
faces = cls.break_compound_to_faces(shape)
results = []
for face in faces:
results.append(cls.shape_to_triangles(face, tolerance, angular_tolerance))
return flatten_list(results)
mesh = BRepMesh_IncrementalMesh(shape.wrapped, tolerance, True, angular_tolerance)
mesh.Perform()
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
triangles = []
location = TopLoc_Location()
brep_tool = BRep_Tool()
while explorer.More():
shape_face = explorer.Current()
triangulation = brep_tool.Triangulation_s(shape.wrapped, location)
for i in range(triangulation.NbTriangles()):
triangle = triangulation.Triangle(i + 1)
face_triangles = tuple(point_to_tuple(triangulation.Node(node)) for node in triangle.Get())
triangles.append(face_triangles)
explorer.Next()
return triangles
@staticmethod
def _chop_sequences_by_depth(sequences: List[List[Tuple[float, float, float]]], last_depth: float):
new_sequences = []
for sequence in sequences:
new_sequence = []
for point in sequence:
if point[2] > last_depth:
if new_sequence:
new_sequences.append(new_sequence)
new_sequence = []
else:
new_sequence.append(point)
if new_sequence:
new_sequences.append(new_sequence)
return new_sequences
def point_to_tuple(point: gp_Pnt) -> Tuple[float, float, float]:
return point.X(), point.Y(), point.Z()
def shape_to_triangles(shape: cq.Shape,
tolerance: float = 1e-3,
angular_tolerance: float = 0.1) -> List[Tuple[Tuple[float, float, float], ...]]:
mesh = BRepMesh_IncrementalMesh(shape.wrapped, tolerance, True, angular_tolerance)
mesh.Perform()
explorer = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
triangles = []
location = TopLoc_Location()
brep_tool = BRep_Tool()
while explorer.More():
shape_face = explorer.Current()
triangulation = brep_tool.Triangulation_s(shape.wrapped, location)
for i in range(triangulation.NbTriangles()):
triangle = triangulation.Triangle(i + 1)
face_triangles = tuple(point_to_tuple(triangulation.Node(node)) for node in triangle.Get())
triangles.append(face_triangles)
explorer.Next()
return triangles
def cl_point_to_tuple(point: ocl.CLPoint) -> Tuple[float, float, float]:
return point.x, point.y, point.z
def demo():
wp = cq.Workplane('XZ').lineTo(100, 0).lineTo(100, 120).lineTo(80, 120).lineTo(0, 0).close().extrude(50)
job = Job(workplane=wp.faces('>Z').workplane(),
feed=300,
plunge_feed=100,
unit=Unit.METRIC,
rapid_height=10)
faces = wp.faces('(not +X) and (not -X) and (not -Y) and (not +Y) and (not -Z)')
op = Surface3D(job=job, clearance_height=2, top_height=0, o=faces, tool=ocl.CylCutter(3.175, 10),
avoid=None, stepdown=-5)
toolpath = visualize_task(job, op)
show_object(wp, 'part')
show_object(toolpath, 'toolpath')
def demo2():
result = (
cq.Workplane('XY').rect(30, 30).extrude(20)
.faces('>Z').workplane().rect(20, 20).cutBlind(-5)
.faces('>Z[1]').workplane().rect(10, 10).extrude(3)
.faces('>Z[1]').fillet(1)
.faces('>Z[2]').fillet(1)
.faces('>Z')
)
result.objects = result.objects[0].innerWires()
result = result.fillet(1)
job = Job(workplane=result.faces('>Z').workplane(),
feed=300,
plunge_feed=100,
unit=Unit.METRIC,
rapid_height=10)
op = Surface3D(job=job, clearance_height=2, top_height=0, o=result.faces(), tool=ocl.CylCutter(3.175, 10),
interpolation_step=0.1, outer_boundary_offset=0)
toolpath = visualize_task(job, op, as_edges=False)
show_object(result)
show_object(toolpath)
if 'show_object' in locals() or __name__ == '__main__':
demo2()