From c6fc398ce677b0f7b87ec6a67a7718bf64f3e521 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 5 Apr 2020 22:38:24 +0200 Subject: [PATCH 1/5] Bool op refactoring --- cadquery/occ_impl/shapes.py | 84 +++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 1a13a8b4d..94334cc41 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,13 +587,37 @@ 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.wrapped,], toCut, cut_op) + + def fuse(self, *toFuse): """ Fuse shapes together """ @@ -602,15 +626,17 @@ def fuse(self, toFuse): fuse_op.RefineEdges() fuse_op.FuseEdges() # fuse_op.SetFuzzyValue(TOLERANCE) - fuse_op.Build() - return Shape.cast(fuse_op.Shape()) + return self._bool_op([self.wrapped,], toFuse, fuse_op) - 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.wrapped,], toIntersect, intersect_op) def _repr_html_(self): """ @@ -1939,6 +1965,48 @@ 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(self.wrapped, toFuse.wrapped) + fuse_op.RefineEdges() + fuse_op.FuseEdges() + # fuse_op.SetFuzzyValue(TOLERANCE) + + return self._bool_op(self, toFuse, fuse_op) + + 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. From e1cfdf8ac397a7faaa660282c578950f21eddcbe Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 6 Apr 2020 17:01:48 +0200 Subject: [PATCH 2/5] Low level bool op refactoring --- cadquery/occ_impl/shapes.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 94334cc41..5484263f1 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -615,19 +615,22 @@ def cut(self, *toCut): cut_op = BRepAlgoAPI_Cut() - return self._bool_op([self.wrapped,], toCut, cut_op) + 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) - return self._bool_op([self.wrapped,], toFuse, fuse_op) + return rv def intersect(self, *toIntersect): """ @@ -636,7 +639,7 @@ def intersect(self, *toIntersect): intersect_op = BRepAlgoAPI_Common() - return self._bool_op([self.wrapped,], toIntersect, intersect_op) + return self._bool_op((self,), toIntersect, intersect_op) def _repr_html_(self): """ @@ -1991,12 +1994,15 @@ def fuse(self, *toFuse): Fuse shapes together """ - fuse_op = BRepAlgoAPI_Fuse(self.wrapped, toFuse.wrapped) - fuse_op.RefineEdges() - fuse_op.FuseEdges() + fuse_op = BRepAlgoAPI_Fuse() # fuse_op.SetFuzzyValue(TOLERANCE) - return self._bool_op(self, toFuse, fuse_op) + rv = self._bool_op(self, toFuse, fuse_op) + + # fuse_op.RefineEdges() + # fuse_op.FuseEdges() + + return rv def intersect(self, *toIntersect): """ From 55921d471bea12bd8918e588b320ed1562da7237 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 6 Apr 2020 17:02:27 +0200 Subject: [PATCH 3/5] Fluent API bool op refactoring --- cadquery/cq.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) 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() From 3eb345bf02546437f6d4f20823e978e60f556ee9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 6 Apr 2020 21:20:24 +0200 Subject: [PATCH 4/5] Added tests --- tests/test_cadquery.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 6ac1f8a01..9fabb7a18 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -1226,6 +1226,9 @@ def testCut(self): resS = currentS.cut(toCut.val()) self.assertEqual(10, resS.faces().size()) + + with self.assertRaises(ValueError): + currentS.cut(toCut.faces().val()) def testIntersect(self): """ @@ -1244,6 +1247,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): """ @@ -2036,6 +2048,9 @@ def testUnions(self): resS = currentS.union(toUnion) self.assertEqual(11, resS.faces().size()) + + with self.assertRaises(ValueError): + resS.union(toUnion.faces().val()) def testCombine(self): s = Workplane(Plane.XY()) @@ -3358,3 +3373,17 @@ 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()) From 0276a8f894f65521d7df428bcbce304972183b34 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 6 Apr 2020 21:26:00 +0200 Subject: [PATCH 5/5] Formatting fixes --- tests/test_cadquery.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 9fabb7a18..b3ee1f88a 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -1226,7 +1226,7 @@ def testCut(self): resS = currentS.cut(toCut.val()) self.assertEqual(10, resS.faces().size()) - + with self.assertRaises(ValueError): currentS.cut(toCut.faces().val()) @@ -1247,13 +1247,13 @@ 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) + + 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()) @@ -2048,7 +2048,7 @@ def testUnions(self): resS = currentS.union(toUnion) self.assertEqual(11, resS.faces().size()) - + with self.assertRaises(ValueError): resS.union(toUnion.faces().val()) @@ -3376,14 +3376,16 @@ def testMakeHelix(self): 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)) - + 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()) + 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()