diff --git a/cadquery/cq.py b/cadquery/cq.py index 21249ded8..a97f82140 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2700,8 +2700,9 @@ def combine(self, clean=True): """ items = list(self.objects) s = items.pop(0) - for ss in items: - s = s.fuse(ss) + + if items: + s.fuse(*items) if clean: s = s.clean() @@ -2722,16 +2723,13 @@ def union(self, toUnion=None, clean=True): # first collect all of the items together if type(toUnion) == CQ or type(toUnion) == Workplane: - solids = toUnion.solids().vals() - if len(solids) < 1: + newS = toUnion.solids().vals() + if len(newS) < 1: raise ValueError( "CQ object must have at least one solid on the stack to union!" ) - newS = solids.pop(0) - for s in solids: - newS = newS.fuse(s) elif type(toUnion) in (Solid, Compound): - newS = toUnion + newS = (toUnion,) else: raise ValueError("Cannot union type '{}'".format(type(toUnion))) @@ -2739,9 +2737,11 @@ def union(self, toUnion=None, clean=True): # look for parents to cut from solidRef = self.findSolid(searchStack=True, searchParents=True) if solidRef is not None: - r = solidRef.fuse(newS) + r = solidRef.fuse(*newS) + elif len(newS) > 1: + r = newS.pop(0).fuse(*newS) else: - r = newS + r = newS[0] if clean: r = r.clean() @@ -2766,13 +2766,13 @@ def cut(self, toCut, clean=True): raise ValueError("Cannot find solid to cut from") solidToCut = None if type(toCut) == CQ or type(toCut) == Workplane: - solidToCut = toCut.val() + solidToCut = toCut.vals() elif type(toCut) in (Solid, Compound): - solidToCut = toCut + solidToCut = (toCut,) else: raise ValueError("Cannot cut type '{}'".format(type(toCut))) - newS = solidRef.cut(solidToCut) + newS = solidRef.cut(*solidToCut) if clean: newS = newS.clean() @@ -2798,13 +2798,13 @@ def intersect(self, toIntersect, clean=True): solidToIntersect = None if isinstance(toIntersect, CQ): - solidToIntersect = toIntersect.val() + solidToIntersect = toIntersect.vals() elif isinstance(toIntersect, Solid) or isinstance(toIntersect, Compound): - solidToIntersect = toIntersect + solidToIntersect = (toIntersect,) else: raise ValueError("Cannot intersect type '{}'".format(type(toIntersect))) - newS = solidRef.intersect(solidToIntersect) + newS = solidRef.intersect(*solidToIntersect) if clean: newS = newS.clean() diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 1a13a8b4d..5484263f1 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -75,7 +75,7 @@ topods_Solid, ) -from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder +from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder, TopoDS_Iterator from OCC.Core.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse # geometry construction from OCC.Core.GCE2d import GCE2d_MakeSegment @@ -587,30 +587,59 @@ def transformGeometry(self, tMatrix): def __hash__(self): return self.hashCode() - def cut(self, toCut): + def _bool_op(self, args, tools, op): + """ + Generic boolean operation + """ + + arg = TopTools_ListOfShape() + for obj in args: + arg.Append(obj.wrapped) + + tool = TopTools_ListOfShape() + for obj in tools: + tool.Append(obj.wrapped) + + op.SetArguments(arg) + op.SetTools(tool) + + op.SetRunParallel(True) + op.Build() + + return Shape.cast(op.Shape()) + + def cut(self, *toCut): """ Remove a shape from another one """ - return Shape.cast(BRepAlgoAPI_Cut(self.wrapped, toCut.wrapped).Shape()) - def fuse(self, toFuse): + cut_op = BRepAlgoAPI_Cut() + + return self._bool_op((self,), toCut, cut_op) + + def fuse(self, *toFuse): """ Fuse shapes together """ - fuse_op = BRepAlgoAPI_Fuse(self.wrapped, toFuse.wrapped) + fuse_op = BRepAlgoAPI_Fuse() + # fuse_op.SetFuzzyValue(TOLERANCE) + + rv = self._bool_op((self,), toFuse, fuse_op) + fuse_op.RefineEdges() fuse_op.FuseEdges() - # fuse_op.SetFuzzyValue(TOLERANCE) - fuse_op.Build() - return Shape.cast(fuse_op.Shape()) + return rv - def intersect(self, toIntersect): + def intersect(self, *toIntersect): """ Construct shape intersection """ - return Shape.cast(BRepAlgoAPI_Common(self.wrapped, toIntersect.wrapped).Shape()) + + intersect_op = BRepAlgoAPI_Common() + + return self._bool_op((self,), toIntersect, intersect_op) def _repr_html_(self): """ @@ -1939,6 +1968,51 @@ def makeText( return cls(text_3d.Shape()).transformShape(position.rG) + def __iter__(self): + """ + Iterate over subshapes. + + """ + + it = TopoDS_Iterator(self.wrapped) + + while it.More(): + yield Shape.cast(it.Value()) + it.Next() + + def cut(self, *toCut): + """ + Remove a shape from another one + """ + + cut_op = BRepAlgoAPI_Cut() + + return self._bool_op(self, toCut, cut_op) + + def fuse(self, *toFuse): + """ + Fuse shapes together + """ + + fuse_op = BRepAlgoAPI_Fuse() + # fuse_op.SetFuzzyValue(TOLERANCE) + + rv = self._bool_op(self, toFuse, fuse_op) + + # fuse_op.RefineEdges() + # fuse_op.FuseEdges() + + return rv + + def intersect(self, *toIntersect): + """ + Construct shape intersection + """ + + intersect_op = BRepAlgoAPI_Common() + + return self._bool_op(self, toIntersect, intersect_op) + def sortWiresByBuildOrder(wireList, result={}): """Tries to determine how wires should be combined into faces. diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 6ac1f8a01..b3ee1f88a 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -1227,6 +1227,9 @@ def testCut(self): self.assertEqual(10, resS.faces().size()) + with self.assertRaises(ValueError): + currentS.cut(toCut.faces().val()) + def testIntersect(self): """ Tests the intersect function. @@ -1245,6 +1248,15 @@ def testIntersect(self): self.assertEqual(6, resS.faces().size()) self.assertAlmostEqual(resS.val().Volume(), 0.5) + b1 = Workplane("XY").box(1, 1, 1) + b2 = Workplane("XY", origin=(0, 0, 0.5)).box(1, 1, 1) + resS = b1.intersect(b2) + + self.assertAlmostEqual(resS.val().Volume(), 0.5) + + with self.assertRaises(ValueError): + b1.intersect(b2.faces().val()) + def testBoundingBox(self): """ Tests the boudingbox center of a model @@ -2037,6 +2049,9 @@ def testUnions(self): self.assertEqual(11, resS.faces().size()) + with self.assertRaises(ValueError): + resS.union(toUnion.faces().val()) + def testCombine(self): s = Workplane(Plane.XY()) objects1 = s.rect(2.0, 2.0).extrude(0.5).faces(">Z").rect(1.0, 1.0).extrude(0.5) @@ -3358,3 +3373,19 @@ def testMakeHelix(self): bb = obj.BoundingBox() self.assertAlmostEqual(bb.zlen, h, 1) + + def testUnionCompound(self): + + box1 = Workplane("XY").box(10, 20, 30) + box2 = Workplane("YZ").box(10, 20, 30) + shape_to_cut = Workplane("XY").box(15, 15, 15).translate((8, 8, 8)) + + list_of_shapes = [] + for o in box1.all(): + list_of_shapes.extend(o.vals()) + for o in box2.all(): + list_of_shapes.extend(o.vals()) + + obj = Workplane("XY").newObject(list_of_shapes).cut(shape_to_cut) + + assert obj.val().isValid()