From 39bf4ed2bdacd030d466ad5434f0b1a3a3e82c22 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Dec 2018 18:09:25 +0100 Subject: [PATCH 01/28] Added missing functions to Workplane --- cadquery/cq.py | 165 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/cadquery/cq.py b/cadquery/cq.py index 33bb4d9b..be3ef15a 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1065,6 +1065,45 @@ class Workplane(CQ): lpoints = list(cpoints) return self.pushPoints(lpoints) + + def polarArray(self, radius, startAngle, angle, count, fill=True): + """ + Creates an polar array of points and pushes them onto the stack. + The 0 degree reference angle is located along the local X-axis. + + :param radius: Radius of the array. + :param startAngle: Starting angle (degrees) of array. 0 degrees is + situated along local X-axis. + :param angle: The angle (degrees) to fill with elements. A positive + value will fill in the counter-clockwise direction. If fill is + false, angle is the angle between elements. + :param count: Number of elements in array. ( > 0 ) + """ + + if count <= 0: + raise ValueError("No elements in array") + + # First element at start angle, convert to cartesian coords + x = radius * math.cos(math.radians(startAngle)) + y = radius * math.sin(math.radians(startAngle)) + points = [(x, y)] + + # Calculate angle between elements + if fill: + if angle % 360 == 0: + angle = angle / count + elif count > 1: + # Inclusive start and end + angle = angle / (count - 1) + + # Add additional elements + for i in range(1, count): + phi = math.radians(startAngle + (angle * i)) + x = radius * math.cos(phi) + y = radius * math.sin(phi) + points.append((x, y)) + + return self.pushPoints(points) def pushPoints(self, pntList): """ @@ -1203,7 +1242,36 @@ class Workplane(CQ): """ p = self._findFromPoint(True) return self.lineTo(xCoord, p.y, forConstruction) + + def polarLine(self, distance, angle, forConstruction=False): + """ + Make a line of the given length, at the given angle from the current point + :param float distance: distance of the end of the line from the current point + :param float angle: angle of the vector to the end of the line with the x-axis + :return: the Workplane object with the current point at the end of the new line + """ + x = math.cos(math.radians(angle)) * distance + y = math.sin(math.radians(angle)) * distance + + return self.line(x, y, forConstruction) + + def polarLineTo(self, distance, angle, forConstruction=False): + """ + Make a line from the current point to the given polar co-ordinates + + Useful if it is more convenient to specify the end location rather than + the distance and angle from the current point + + :param float distance: distance of the end of the line from the origin + :param float angle: angle of the vector to the end of the line with the x-axis + :return: the Workplane object with the current point at the end of the new line + """ + x = math.cos(math.radians(angle)) * distance + y = math.sin(math.radians(angle)) * distance + + return self.lineTo(x, y, forConstruction) + # absolute move in current plane, not drawing def moveTo(self, x=0, y=0): """ @@ -1314,6 +1382,66 @@ class Workplane(CQ): self._addPendingEdge(arc) return self.newObject([arc]) + + def sagittaArc(self, endPoint, sag, forConstruction=False): + """ + Draw an arc from the current point to endPoint with an arc defined by the sag (sagitta). + + :param endPoint: end point for the arc + :type endPoint: 2-tuple, in workplane coordinates + :param sag: the sagitta of the arc + :type sag: float, perpendicular distance from arc center to arc baseline. + :return: a workplane with the current point at the end of the arc + + The sagitta is the distance from the center of the arc to the arc base. + Given that a closed contour is drawn clockwise; + A positive sagitta means convex arc and negative sagitta means concave arc. + See "https://en.wikipedia.org/wiki/Sagitta_(geometry)" for more information. + """ + + startPoint = self._findFromPoint(useLocalCoords=True) + endPoint = Vector(endPoint) + midPoint = endPoint.add(startPoint).multiply(0.5) + + sagVector = endPoint.sub(startPoint).normalized().multiply(abs(sag)) + if(sag > 0): + sagVector.x, sagVector.y = -sagVector.y, sagVector.x # Rotate sagVector +90 deg + else: + sagVector.x, sagVector.y = sagVector.y, -sagVector.x # Rotate sagVector -90 deg + + sagPoint = midPoint.add(sagVector) + + return self.threePointArc(sagPoint, endPoint, forConstruction) + + def radiusArc(self, endPoint, radius, forConstruction=False): + """ + Draw an arc from the current point to endPoint with an arc defined by the sag (sagitta). + + :param endPoint: end point for the arc + :type endPoint: 2-tuple, in workplane coordinates + :param radius: the radius of the arc + :type radius: float, the radius of the arc between start point and end point. + :return: a workplane with the current point at the end of the arc + + Given that a closed contour is drawn clockwise; + A positive radius means convex arc and negative radius means concave arc. + """ + + startPoint = self._findFromPoint(useLocalCoords=True) + endPoint = Vector(endPoint) + + # Calculate the sagitta from the radius + length = endPoint.sub(startPoint).Length / 2.0 + try: + sag = abs(radius) - math.sqrt(radius**2 - length**2) + except ValueError: + raise ValueError("Arc radius is not large enough to reach the end point.") + + # Return a sagittaArc + if radius > 0: + return self.sagittaArc(endPoint, sag, forConstruction) + else: + return self.sagittaArc(endPoint, -sag, forConstruction) def rotateAndCopy(self, matrix): """ @@ -2222,6 +2350,43 @@ class Workplane(CQ): solidRef.wrapped = newS.wrapped return self.newObject([newS]) + + def intersect(self, toIntersect, combine=True, clean=True): + """ + Intersects the provided solid from the current solid. + + if combine=True, the result and the original are updated to point to the new object + if combine=False, the result will be on the stack, but the original is unmodified + + :param toIntersect: object to intersect + :type toIntersect: a solid object, or a CQ object having a solid, + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there is no solid to intersect with in the chain + :return: a CQ object with the resulting object selected + """ + + # look for parents to intersect with + solidRef = self.findSolid(searchStack=True, searchParents=True) + + if solidRef is None: + raise ValueError("Cannot find solid to intersect with") + solidToIntersect = None + + if isinstance(toIntersect, CQ): + solidToIntersect = toIntersect.val() + elif isinstance(toIntersect, Solid): + solidToIntersect = toIntersect + else: + raise ValueError("Cannot intersect type '{}'".format(type(toIntersect))) + + newS = solidRef.intersect(solidToIntersect) + + if clean: newS = newS.clean() + + if combine: + solidRef.wrapped = newS.wrapped + + return self.newObject([newS]) def cutBlind(self, distanceToCut, clean=True): """ From 1866d5133015098417fb3a2a421161925e09c222 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Dec 2018 22:18:47 +0100 Subject: [PATCH 02/28] Merged changes in close() --- cadquery/cq.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index be3ef15a..099c37ff 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1874,7 +1874,14 @@ class Workplane(CQ): s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2) """ - self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) + endPoint = self._findFromPoint(True) + startPoint = self.ctx.firstPoint + + # Check if there is a distance between startPoint and endPoint + # that is larger than what is considered a numerical error. + # If so; add a line segment between endPoint and startPoint + if endPoint.sub(startPoint).Length > 1e-6: + self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) # Need to reset the first point after closing a wire self.ctx.firstPoint = None From e2e045df4214d7978cb2f0f3b1b21f86eb778156 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Dec 2018 22:20:02 +0100 Subject: [PATCH 03/28] Merged-in missing tests from the original CQ version --- tests/TestCadQuery.py | 186 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 8a7f3d7c..c6552156 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -418,6 +418,54 @@ class TestCadQuery(BaseTest): self.assertEqual(3, result.faces().size()) self.assertEqual(3, result.edges().size()) + def testSweepAlongListOfWires(self): + """ + Tests the operation of sweeping along a list of wire(s) along a path + """ + + # X axis line length 20.0 + path = Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) + + # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 + defaultSweep = Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ + workplane(offset=10.0).circle(1.0). \ + workplane(offset=10.0).circle(2.0).sweep(path, sweepAlongWires=True) + + # We can sweep thrue different shapes + recttocircleSweep = Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ + workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ + workplane(offset=8.0).rect(2.0, 2.0).sweep(path, sweepAlongWires=True) + + circletorectSweep = Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ + workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ + workplane(offset=7.0).circle(1.0).sweep(path, sweepAlongWires=True) + + # Placement of the Shape is important otherwise could produce unexpected shape + specialSweep = Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ + sweep(path, sweepAlongWires=True) + + # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 + path = Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ + threePointArc((4, 0), (0, -4)).lineTo(-5, -4) + + # Placement of different shapes should follow the path + # cylinder r=1.5 along first line + # then sweep allong arc from r=1.5 to r=1.0 + # then cylinder r=1.0 along last line + arcSweep = Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ + workplane(offset=5).circle(1.5). \ + moveTo(0, -8).circle(1.0). \ + workplane(offset=-5).circle(1.0). \ + sweep(path, sweepAlongWires=True) + + # Test and saveModel + self.assertEqual(1, defaultSweep.solids().size()) + self.assertEqual(1, circletorectSweep.solids().size()) + self.assertEqual(1, recttocircleSweep.solids().size()) + self.assertEqual(1, specialSweep.solids().size()) + self.assertEqual(1, arcSweep.solids().size()) + self.saveModel(defaultSweep) + def testTwistExtrude(self): """ Tests extrusion while twisting through an angle. @@ -803,6 +851,37 @@ class TestCadQuery(BaseTest): selectors.NearestToPointSelector((0.0, 0.0, 0.0))) .first().val().Y)) + # Test the sagittaArc and radiusArc functions + a1 = Workplane(Plane.YZ()).threePointArc((5, 1), (10, 0)) + a2 = Workplane(Plane.YZ()).sagittaArc((10, 0), -1) + a3 = Workplane(Plane.YZ()).threePointArc((6, 2), (12, 0)) + a4 = Workplane(Plane.YZ()).radiusArc((12, 0), -10) + + assert(a1.edges().first().val().geomType() == "CIRCLE") + assert(a2.edges().first().val().geomType() == "CIRCLE") + assert(a3.edges().first().val().geomType() == "CIRCLE") + assert(a4.edges().first().val().geomType() == "CIRCLE") + + assert(a1.edges().first().val().Length() == a2.edges().first().val().Length()) + assert(a3.edges().first().val().Length() == a4.edges().first().val().Length()) + + def testPolarLines(self): + """ + Draw some polar lines and check expected results + """ + + # Test the PolarLine* functions + s = Workplane(Plane.XY()) + r = s.polarLine(10, 45) \ + .polarLineTo(10, -45) \ + .polarLine(10, -180) \ + .polarLine(-10, -90) \ + .close() + + # a single wire, 5 edges + self.assertEqual(1, r.wires().size()) + self.assertEqual(5, r.wires().edges().size()) + def testLargestDimension(self): """ Tests the largestDimension function when no solids are on the stack and when there are @@ -1326,6 +1405,80 @@ class TestCadQuery(BaseTest): line(-10, 0).close().extrude(10, clean=False).clean() self.assertEqual(6, s.faces().size()) + def testPlanes(self): + """ + Test other planes other than the normal ones (XY, YZ) + """ + # ZX plane + s = Workplane(Plane.ZX()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # YX plane + s = Workplane(Plane.YX()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # YX plane + s = Workplane(Plane.YX()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # ZY plane + s = Workplane(Plane.ZY()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # front plane + s = Workplane(Plane.front()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # back plane + s = Workplane(Plane.back()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # left plane + s = Workplane(Plane.left()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # right plane + s = Workplane(Plane.right()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # top plane + s = Workplane(Plane.top()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + # bottom plane + s = Workplane(Plane.bottom()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + def testIsIndide(self): + """ + Testing if one box is inside of another. + """ + box1 = Workplane(Plane.XY()).box(10, 10, 10) + box2 = Workplane(Plane.XY()).box(5, 5, 5) + + self.assertFalse(box2.val().BoundingBox().isInside(box1.val().BoundingBox())) + self.assertTrue(box1.val().BoundingBox().isInside(box2.val().BoundingBox())) + def testCup(self): """ UOM = "mm" @@ -1486,3 +1639,36 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(delta.toTuple(), (0., 0., 2. * h), decimal_places) + + def testClose(self): + # Close without endPoint and startPoint coincide. + # Create a half-circle + a = Workplane(Plane.XY()).sagittaArc((10, 0), 2).close().extrude(2) + + # Close when endPoint and startPoint coincide. + # Create a double half-circle + b = Workplane(Plane.XY()).sagittaArc((10, 0), 2).sagittaArc((0, 0), 2).close().extrude(2) + + # The b shape shall have twice the volume of the a shape. + self.assertAlmostEqual(a.val().Volume() * 2.0, b.val().Volume()) + + # Testcase 3 from issue #238 + thickness = 3.0 + length = 10.0 + width = 5.0 + + obj1 = Workplane('XY', origin=(0, 0, -thickness / 2)) \ + .moveTo(length / 2, 0).threePointArc((0, width / 2), (-length / 2, 0)) \ + .threePointArc((0, -width / 2), (length / 2, 0)) \ + .close().extrude(thickness) + + os_x = 8.0 # Offset in X + os_y = -19.5 # Offset in Y + + obj2 = Workplane('YZ', origin=(os_x, os_y, -thickness / 2)) \ + .moveTo(os_x + length / 2, os_y).sagittaArc((os_x -length / 2, os_y), width / 2) \ + .sagittaArc((os_x + length / 2, os_y), width / 2) \ + .close().extrude(thickness) + + # The obj1 shape shall have the same volume as the obj2 shape. + self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) From a099caf25ef5bc05dca94627de53bb83c4293ab9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Dec 2018 22:21:22 +0100 Subject: [PATCH 04/28] Amended Vector implementation to make it compatibile with original CQ --- cadquery/occ_impl/geom.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index a84462fb..c384200a 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -19,17 +19,25 @@ class Vector(object): * a gp_Vec * a vector ( in which case it is copied ) * a 3-tuple - * three float values, x, y, and z + * a 2-tuple (z assumed to be 0) + * three float values: x, y, and z + * two float values: x,y """ def __init__(self, *args): if len(args) == 3: fV = gp_Vec(*args) + elif len(args) == 2: + fV = gp_Vec(*args,0) elif len(args) == 1: if isinstance(args[0], Vector): fV = gp_Vec(args[0].wrapped.XYZ()) elif isinstance(args[0], (tuple, list)): - fV = gp_Vec(*args[0]) + arg = args[0] + if len(arg)==3: + fV = gp_Vec(*arg) + elif len(arg)==2: + fV = gp_Vec(*arg,0) elif isinstance(args[0], gp_Vec): fV = gp_Vec(args[0].XYZ()) elif isinstance(args[0], gp_Pnt): @@ -50,14 +58,26 @@ class Vector(object): @property def x(self): return self.wrapped.X() + + @x.setter + def x(self,value): + self.wrapped.SetX(value) @property def y(self): return self.wrapped.Y() + + @y.setter + def y(self,value): + self.wrapped.SetY(value) @property def z(self): return self.wrapped.Z() + + @z.setter + def z(self,value): + self.wrapped.SetZ(value) @property def Length(self): From 4c797e7e1bba9f4f05f4177b2ac62db4527d937b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Dec 2018 22:49:26 +0100 Subject: [PATCH 05/28] Merge changes regarding sweep along path --- cadquery/cq.py | 439 +++++++++++++++++++++++-------------------------- 1 file changed, 208 insertions(+), 231 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 099c37ff..67226a04 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -23,6 +23,8 @@ from cadquery import * from cadquery import selectors from cadquery import exporters +from copy import copy, deepcopy + class CQContext(object): """ @@ -31,11 +33,9 @@ class CQContext(object): All objects in the same CQ chain share a reference to this same object instance which allows for shared state when needed, """ - def __init__(self): self.pendingWires = [] # a list of wires that have been created and need to be extruded - # a list of created pending edges that need to be joined into wires - self.pendingEdges = [] + self.pendingEdges = [] # a list of created pending edges that need to be joined into wires # a reference to the first point for a set of edges. # Used to determine how to behave when close() is called self.firstPoint = None @@ -77,7 +77,7 @@ class CQ(object): Custom plugins and subclasses should use this method to create new CQ objects correctly. """ - r = CQ(None) # create a completely blank one + r = type(self)(None) # create a completely blank one r.parent = self r.ctx = self.ctx # context solid remains the same r.objects = list(objlist) @@ -169,8 +169,8 @@ class CQ(object): Most of the time, both objects will contain a single solid, which is combined and returned on the stack of the new object. """ - # loop through current stack objects, and combine them - # TODO: combine other types of objects as well, like edges and wires + #loop through current stack objects, and combine them + #TODO: combine other types of objects as well, like edges and wires toCombine = self.solids().vals() if otherCQToCombine: @@ -180,17 +180,18 @@ class CQ(object): if len(toCombine) < 1: raise ValueError("Cannot Combine: at least one solid required!") - # get context solid and we don't want to find our own objects + #get context solid and we don't want to find our own objects ctxSolid = self.findSolid(searchStack=False, searchParents=True) if ctxSolid is None: ctxSolid = toCombine.pop(0) - # now combine them all. make sure to save a reference to the ctxSolid pointer! + #now combine them all. make sure to save a reference to the ctxSolid pointer! s = ctxSolid for tc in toCombine: s = s.fuse(tc) + ctxSolid.wrapped = s.wrapped return self.newObject([s]) def all(self): @@ -238,7 +239,7 @@ class CQ(object): """ if type(obj) == list: self.objects.extend(obj) - elif type(obj) == CQ or type(obj) == Workplane: + elif isinstance(obj, CQ): self.objects.extend(obj.objects) else: self.objects.append(obj) @@ -253,7 +254,7 @@ class CQ(object): """ return self.objects[0] - def toOCC(self): + def toFreecad(self): """ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code needed when rendering a model in FreeCAD's 3D view. @@ -312,9 +313,9 @@ class CQ(object): n1 = f1.normalAt() # test normals (direction of planes) - if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or - (abs(n0.y - n1.y) < self.ctx.tolerance) or - (abs(n0.z - n1.z) < self.ctx.tolerance)): + if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or + (abs(n0.y-n1.y) < self.ctx.tolerance) or + (abs(n0.z-n1.z) < self.ctx.tolerance)): return False # test if p1 is on the plane of f0 (offset of planes) @@ -329,15 +330,14 @@ class CQ(object): """ xd = Vector(0, 0, 1).cross(normal) if xd.Length < self.ctx.tolerance: - # this face is parallel with the x-y plane, so choose x to be in global coordinates + #this face is parallel with the x-y plane, so choose x to be in global coordinates xd = Vector(1, 0, 0) return xd if len(self.objects) > 1: # are all objects 'PLANE'? - if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects): - raise ValueError( - "If multiple objects selected, they all must be planar faces.") + if not all(o.geomType() == 'PLANE' for o in self.objects): + raise ValueError("If multiple objects selected, they all must be planar faces.") # are all faces co-planar with each other? if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): @@ -356,9 +356,9 @@ class CQ(object): if isinstance(obj, Face): if centerOption == 'CenterOfMass': - center = obj.Center() + center = obj.Center() elif centerOption == 'CenterOfBoundBox': - center = obj.CenterOfBoundBox() + center = obj.CenterOfBoundBox() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: @@ -370,24 +370,23 @@ class CQ(object): normal = self.plane.zDir xDir = self.plane.xDir else: - raise ValueError( - "Needs a face or a vertex or point on a work plane") + raise ValueError("Needs a face or a vertex or point on a work plane") - # invert if requested + #invert if requested if invert: normal = normal.multiply(-1.0) - # offset origin if desired + #offset origin if desired offsetVector = normal.normalized().multiply(offset) offsetCenter = center.add(offsetVector) - # make the new workplane + #make the new workplane plane = Plane(offsetCenter, xDir, normal) s = Workplane(plane) s.parent = self s.ctx = self.ctx - # a new workplane has the center of the workplane on the stack + #a new workplane has the center of the workplane on the stack return s def first(self): @@ -459,7 +458,7 @@ class CQ(object): if isinstance(s, Solid): return s elif isinstance(s, Compound): - return s + return s.Solids() if searchParents and self.parent is not None: return self.parent.findSolid(searchStack=True, searchParents=searchParents) @@ -482,9 +481,10 @@ class CQ(object): toReturn = self._collectProperty(objType) if selector is not None: - if isinstance(selector, str) or isinstance(selector, str): + # if isinstance(selector, str) or isinstance(selector, str): + try: selectorObj = selectors.StringSyntaxSelector(selector) - else: + except: selectorObj = selector toReturn = selectorObj.filter(toReturn) @@ -671,7 +671,7 @@ class CQ(object): """ return self._selectObjects('Compounds', selector) - def toSvg(self, opts=None): + def toSvg(self, opts=None, view_vector=(-1.75,1.1,5)): """ Returns svg text that represents the first item on the stack. @@ -679,20 +679,26 @@ class CQ(object): :param opts: svg formatting options :type opts: dictionary, width and height + + :param view_vector: camera's view direction vector + :type view_vector: tuple, (x, y, z) :return: a string that contains SVG that represents this item. """ - return exporters.getSVG(self.val(), opts) + return exporters.getSVG(self.val().wrapped, opts=opts, view_vector=view_vector) - def exportSvg(self, fileName): + def exportSvg(self, fileName, view_vector=(-1.75,1.1,5)): """ Exports the first item on the stack as an SVG file For testing purposes mainly. :param fileName: the filename to export + + :param view_vector: camera's view direction vector + :type view_vector: tuple, (x, y, z) :type fileName: String, absolute path to the file """ - exporters.exportSVG(self, fileName) + exporters.exportSVG(self, fileName, view_vector) def rotateAboutCenter(self, axisEndPoint, angleDegrees): """ @@ -713,13 +719,13 @@ class CQ(object): Future Enhancements: * A version of this method that returns a transformed copy, rather than modifying the originals - * This method doesnt expose a very good interface, because the axis of rotation + * This method doesn't expose a very good interface, because the axis of rotation could be inconsistent between multiple objects. This is because the beginning of the axis is variable, while the end is fixed. This is fine when operating on one object, but is not cool for multiple. """ - # center point is the first point in the vector + #center point is the first point in the vector endVec = Vector(axisEndPoint) def _rot(obj): @@ -754,10 +760,10 @@ class CQ(object): :param basePointVector: the base point to mirror about :type basePointVector: tuple """ - newS = self.newObject( - [self.objects[0].mirror(mirrorPlane, basePointVector)]) + newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)]) return newS.first() + def translate(self, vec): """ Returns a copy of all of the items on the stack moved by the specified translation vector. @@ -768,6 +774,7 @@ class CQ(object): """ return self.newObject([o.translate(vec) for o in self.objects]) + def shell(self, thickness): """ Remove the selected faces to create a shell of the specified thickness. @@ -811,6 +818,7 @@ class CQ(object): raise ValueError("Shelling requires that faces be selected") s = solidRef.shell(self.objects, thickness) + solidRef.wrapped = s.wrapped return self.newObject([s]) def fillet(self, radius): @@ -841,6 +849,7 @@ class CQ(object): raise ValueError("Fillets requires that edges be selected") s = solid.fillet(radius, edgeList) + solid.wrapped = s.wrapped return self.newObject([s]) def chamfer(self, length, length2=None): @@ -879,8 +888,15 @@ class CQ(object): s = solid.chamfer(length, length2, edgeList) + solid.wrapped = s.wrapped return self.newObject([s]) + def __copy__(self): + return self.newObject(copy(self.objects)) + + def __deepcopy__(self, memo): + return self.newObject(deepcopy(self.objects, memo)) + class Workplane(CQ): """ @@ -934,10 +950,11 @@ class Workplane(CQ): if inPlane.__class__.__name__ == 'Plane': tmpPlane = inPlane - elif isinstance(inPlane, str) or isinstance(inPlane, str): - tmpPlane = Plane.named(inPlane, origin) else: - tmpPlane = None + try: + tmpPlane = Plane.named(inPlane, origin) + except ValueError: + tmpPlane = None if tmpPlane is None: raise ValueError( @@ -963,7 +980,7 @@ class Workplane(CQ): :return: a new work plane, transformed as requested """ - # old api accepted a vector, so we'll check for that. + #old api accepted a vector, so we'll check for that. if rotate.__class__.__name__ == 'Vector': rotate = rotate.toTuple() @@ -989,8 +1006,8 @@ class Workplane(CQ): :return: a new Workplane object with the current workplane as a parent. """ - # copy the current state to the new object - ns = Workplane("XY") + #copy the current state to the new object + ns = type(self)("XY") ns.plane = self.plane ns.parent = self ns.objects = list(objlist) @@ -1025,8 +1042,7 @@ class Workplane(CQ): elif isinstance(obj, Vector): p = obj else: - raise RuntimeError( - "Cannot convert object type '%s' to vector " % type(obj)) + raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) if useLocalCoords: return self.plane.toLocalCoords(p) @@ -1047,7 +1063,7 @@ class Workplane(CQ): false, the lower left corner will be at the center of the work plane """ - if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1: + if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1: raise ValueError("Spacing and count must be > 0 ") lpoints = [] # coordinates relative to bottom left point @@ -1055,17 +1071,17 @@ class Workplane(CQ): for y in range(yCount): lpoints.append((xSpacing * x, ySpacing * y)) - # shift points down and left relative to origin if requested + #shift points down and left relative to origin if requested if center: - xc = xSpacing * (xCount - 1) * 0.5 - yc = ySpacing * (yCount - 1) * 0.5 + xc = xSpacing*(xCount-1) * 0.5 + yc = ySpacing*(yCount-1) * 0.5 cpoints = [] for p in lpoints: cpoints.append((p[0] - xc, p[1] - yc)) lpoints = list(cpoints) return self.pushPoints(lpoints) - + def polarArray(self, radius, startAngle, angle, count, fill=True): """ Creates an polar array of points and pushes them onto the stack. @@ -1242,7 +1258,7 @@ class Workplane(CQ): """ p = self._findFromPoint(True) return self.lineTo(xCoord, p.y, forConstruction) - + def polarLine(self, distance, angle, forConstruction=False): """ Make a line of the given length, at the given angle from the current point @@ -1271,8 +1287,8 @@ class Workplane(CQ): y = math.sin(math.radians(angle)) * distance return self.lineTo(x, y, forConstruction) - - # absolute move in current plane, not drawing + + #absolute move in current plane, not drawing def moveTo(self, x=0, y=0): """ Move to the specified point, without drawing. @@ -1291,7 +1307,7 @@ class Workplane(CQ): newCenter = Vector(x, y, 0) return self.newObject([self.plane.toWorldCoords(newCenter)]) - # relative move in current plane, not drawing + #relative move in current plane, not drawing def move(self, xDist=0, yDist=0): """ Move the specified distance from the current point, without drawing. @@ -1372,17 +1388,17 @@ class Workplane(CQ): provide tangent arcs """ - gstartPoint = self._findFromPoint(False) - gpoint1 = self.plane.toWorldCoords(point1) - gpoint2 = self.plane.toWorldCoords(point2) + startPoint = self._findFromPoint(False) + point1 = self.plane.toWorldCoords(point1) + point2 = self.plane.toWorldCoords(point2) - arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2) + arc = Edge.makeThreePointArc(startPoint, point1, point2) if not forConstruction: self._addPendingEdge(arc) return self.newObject([arc]) - + def sagittaArc(self, endPoint, sag, forConstruction=False): """ Draw an arc from the current point to endPoint with an arc defined by the sag (sagitta). @@ -1462,20 +1478,19 @@ class Workplane(CQ): faster implementation: this one transforms 3 times to accomplish the result """ - # convert edges to a wire, if there are pending edges + #convert edges to a wire, if there are pending edges n = self.wire(forConstruction=False) - # attempt to consolidate wires together. + #attempt to consolidate wires together. consolidated = n.consolidateWires() - rotatedWires = self.plane.rotateShapes( - consolidated.wires().vals(), matrix) + rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) for w in rotatedWires: consolidated.objects.append(w) consolidated._addPendingWire(w) - # attempt again to consolidate all of the wires + #attempt again to consolidate all of the wires c = consolidated.consolidateWires() return c @@ -1496,23 +1511,11 @@ class Workplane(CQ): Produces a flat, heart shaped object Future Enhancements: - mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + mirrorX().mirrorY() should work but doesn't, due to some FreeCAD weirdness """ - # convert edges to a wire, if there are pending edges - n = self.wire(forConstruction=False) - - # attempt to consolidate wires together. - consolidated = n.consolidateWires() - - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'Y') - - for w in mirroredWires: - consolidated.objects.append(w) - consolidated._addPendingWire(w) - - # attempt again to consolidate all of the wires - return consolidated.consolidateWires() + tm = Matrix() + tm.rotateY(math.pi) + return self.rotateAndCopy(tm) def mirrorX(self): """ @@ -1526,23 +1529,11 @@ class Workplane(CQ): Typically used to make creating wires with symmetry easier. Future Enhancements: - mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + mirrorX().mirrorY() should work but doesn't, due to some FreeCAD weirdness """ - # convert edges to a wire, if there are pending edges - n = self.wire(forConstruction=False) - - # attempt to consolidate wires together. - consolidated = n.consolidateWires() - - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'X') - - for w in mirroredWires: - consolidated.objects.append(w) - consolidated._addPendingWire(w) - - # attempt again to consolidate all of the wires - return consolidated.consolidateWires() + tm = Matrix() + tm.rotateX(math.pi) + return self.rotateAndCopy(tm) def _addPendingEdge(self, edge): """ @@ -1587,15 +1578,15 @@ class Workplane(CQ): if len(wires) < 2: return self - # TODO: this makes the assumption that either all wires could be combined, or none. - # in reality trying each combination of wires is probably not reasonable anyway + #TODO: this makes the assumption that either all wires could be combined, or none. + #in reality trying each combination of wires is probably not reasonable anyway w = Wire.combine(wires) - # ok this is a little tricky. if we consolidate wires, we have to actually - # modify the pendingWires collection to remove the original ones, and replace them - # with the consolidate done - # since we are already assuming that all wires could be consolidated, its easy, we just - # clear the pending wire list + #ok this is a little tricky. if we consolidate wires, we have to actually + #modify the pendingWires collection to remove the original ones, and replace them + #with the consolidate done + #since we are already assuming that all wires could be consolidated, its easy, we just + #clear the pending wire list r = self.newObject([w]) r.ctx.pendingWires = [] r._addPendingWire(w) @@ -1624,7 +1615,7 @@ class Workplane(CQ): edges = self.ctx.pendingEdges - # do not consolidate if there are no free edges + #do not consolidate if there are no free edges if len(edges) == 0: return self @@ -1635,6 +1626,7 @@ class Workplane(CQ): if type(e) != Edge: others.append(e) + w = Wire.assembleEdges(edges) if not forConstruction: self._addPendingWire(w) @@ -1677,7 +1669,7 @@ class Workplane(CQ): for obj in self.objects: if useLocalCoordinates: - # TODO: this needs to work for all types of objects, not just vectors! + #TODO: this needs to work for all types of objects, not just vectors! r = callBackFunction(self.plane.toLocalCoords(obj)) r = r.transformShape(self.plane.rG) else: @@ -1708,11 +1700,11 @@ class Workplane(CQ): If the stack has zero length, a single point is returned, which is the center of the current workplane/coordinate system """ - # convert stack to a list of points + #convert stack to a list of points pnts = [] if len(self.objects) == 0: - # nothing on the stack. here, we'll assume we should operate with the - # origin as the context point + #nothing on the stack. here, we'll assume we should operate with the + #origin as the context point pnts.append(self.plane.origin) else: @@ -1751,10 +1743,10 @@ class Workplane(CQ): # Here pnt is in local coordinates due to useLocalCoords=True # (xc,yc,zc) = pnt.toTuple() if centered: - p1 = pnt.add(Vector(xLen / -2.0, yLen / -2.0, 0)) - p2 = pnt.add(Vector(xLen / 2.0, yLen / -2.0, 0)) - p3 = pnt.add(Vector(xLen / 2.0, yLen / 2.0, 0)) - p4 = pnt.add(Vector(xLen / -2.0, yLen / 2.0, 0)) + p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0)) + p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0, 0)) + p3 = pnt.add(Vector(xLen/2.0, yLen/2.0, 0)) + p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0)) else: p1 = pnt p2 = pnt.add(Vector(xLen, 0, 0)) @@ -1763,11 +1755,11 @@ class Workplane(CQ): w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction) return w - # return Part.makePolygon([p1,p2,p3,p4,p1]) + #return Part.makePolygon([p1,p2,p3,p4,p1]) return self.eachpoint(makeRectangleWire, True) - # circle from current point + #circle from current point def circle(self, radius, forConstruction=False): """ Make a circle for each item on the stack. @@ -1816,12 +1808,12 @@ class Workplane(CQ): :return: a polygon wire """ def _makePolygon(center): - # pnt is a vector in local coordinates + #pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] - for i in range(nSides + 1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), - (diameter / 2.0 * math.sin(angle * i)), 0)) + for i in range(nSides+1): + pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)), + (diameter / 2.0 * math.sin(angle*i)), 0)) return Wire.makePolygon(pnts, forConstruction) return self.eachpoint(_makePolygon, True) @@ -1884,7 +1876,7 @@ class Workplane(CQ): self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) # Need to reset the first point after closing a wire - self.ctx.firstPoint = None + self.ctx.firstPoint=None return self.wire() @@ -1895,8 +1887,8 @@ class Workplane(CQ): how long or wide a feature must be to make sure to cut through all of the material :return: A value representing the largest dimension of the first solid on the stack """ - # TODO: this implementation is naive and returns the dims of the first solid... most of - # TODO: the time this works. but a stronger implementation would be to search all solids. + #TODO: this implementation is naive and returns the dims of the first solid... most of + #TODO: the time this works. but a stronger implementation would be to search all solids. s = self.findSolid() if s: return s.BoundingBox().DiagonalLength * 5.0 @@ -1917,18 +1909,18 @@ class Workplane(CQ): if ctxSolid is None: raise ValueError("Must have a solid in the chain to cut from!") - # will contain all of the counterbores as a single compound + #will contain all of the counterbores as a single compound results = self.eachpoint(fcn, useLocalCoords).vals() s = ctxSolid for cb in results: s = s.cut(cb) - if clean: - s = s.clean() + if clean: s = s.clean() + ctxSolid.wrapped = s.wrapped return self.newObject([s]) - # but parameter list is different so a simple function pointer wont work + #but parameter list is different so a simple function pointer won't work def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True): """ Makes a counterbored hole for each item on the stack. @@ -1969,20 +1961,18 @@ class Workplane(CQ): pnt is in local coordinates """ boreDir = Vector(0, 0, -1) - # first make the hole - hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordianates! + #first make the hole + hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates! - # add the counter bore - cbore = Solid.makeCylinder( - cboreDiameter / 2.0, cboreDepth, center, boreDir) + #add the counter bore + cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) r = hole.fuse(cbore) return r return self.cutEach(_makeCbore, True, clean) - # TODO: almost all code duplicated! - # but parameter list is different so a simple function pointer wont work + #TODO: almost all code duplicated! + #but parameter list is different so a simple function pointer won't work def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True): """ Makes a countersunk hole for each item on the stack. @@ -2018,13 +2008,12 @@ class Workplane(CQ): depth = self.largestDimension() def _makeCsk(center): - # center is in local coordinates + #center is in local coordinates boreDir = Vector(0, 0, -1) - # first make the hole - hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coords! + #first make the hole + hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) csk = Solid.makeCone(r, 0.0, h, center, boreDir) @@ -2033,8 +2022,8 @@ class Workplane(CQ): return self.cutEach(_makeCsk, True, clean) - # TODO: almost all code duplicated! - # but parameter list is different so a simple function pointer wont work + #TODO: almost all code duplicated! + #but parameter list is different so a simple function pointer won't work def hole(self, diameter, depth=None, clean=True): """ Makes a hole for each item on the stack. @@ -2071,14 +2060,13 @@ class Workplane(CQ): pnt is in local coordinates """ boreDir = Vector(0, 0, -1) - # first make the hole - hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordinates! + #first make the hole + hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates! return hole return self.cutEach(_makeHole, True, clean) - # TODO: duplicated code with _extrude and extrude + #TODO: duplicated code with _extrude and extrude def twistExtrude(self, distance, angleDegrees, combine=True, clean=True): """ Extrudes a wire in the direction normal to the plane, but also twists by the specified @@ -2098,23 +2086,21 @@ class Workplane(CQ): :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape :return: a CQ object with the resulting solid selected. """ - # group wires together into faces based on which ones are inside the others - # result is a list of lists - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), self.plane, []) + #group wires together into faces based on which ones are inside the others + #result is a list of lists + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) - # now all of the wires have been used to create an extrusion - self.ctx.pendingWires = [] + self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion - # compute extrusion vector and extrude + #compute extrusion vector and extrude eDir = self.plane.zDir.multiply(distance) - # one would think that fusing faces into a compound and then extruding would work, - # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) - # but then cutting it from the main solid fails with BRep_NotDone. - # the work around is to extrude each and then join the resulting solids, which seems to work + #one would think that fusing faces into a compound and then extruding would work, + #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + #but then cutting it from the main solid fails with BRep_NotDone. + #the work around is to extrude each and then join the resulting solids, which seems to work - # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there + #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there # are multiple sets r = None for ws in wireSets: @@ -2129,8 +2115,7 @@ class Workplane(CQ): newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: - newS = newS.clean() + if clean: newS = newS.clean() return newS def extrude(self, distance, combine=True, clean=True, both=False): @@ -2158,15 +2143,13 @@ class Workplane(CQ): perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar """ - r = self._extrude( - distance, both=both) # returns a Solid (or a compound if there were multiple) + r = self._extrude(distance,both=both) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: - newS = newS.clean() + if clean: newS = newS.clean() return newS def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): @@ -2191,10 +2174,10 @@ class Workplane(CQ): * if combine is true, the value is combined with the context solid if it exists, and the resulting solid becomes the new context solid. """ - # Make sure we account for users specifying angles larger than 360 degrees + #Make sure we account for users specifying angles larger than 360 degrees angleDegrees %= 360.0 - # Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve + #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees # The default start point of the vector defining the axis of rotation will be the origin @@ -2222,28 +2205,28 @@ class Workplane(CQ): newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: - newS = newS.clean() + if clean: newS = newS.clean() return newS - def sweep(self, path, makeSolid=True, isFrenet=False, combine=True, clean=True): + def sweep(self, path, sweepAlongWires=False, makeSolid=True, isFrenet=False, combine=True, clean=True): """ Use all un-extruded wires in the parent chain to create a swept solid. :param path: A wire along which the pending wires will be swept + :param boolean sweepAlongWires: + False to create multiple swept from wires on the chain along path + True to create only one solid swept along path with shape following the list of wires on the chain :param boolean combine: True to combine the resulting solid with parent solids if found. :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape :return: a CQ object with the resulting solid selected. """ - # returns a Solid (or a compound if there were multiple) - r = self._sweep(path.wire(), makeSolid, isFrenet) + r = self._sweep(path.wire(), sweepAlongWires, makeSolid, isFrenet) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: - newS = newS.clean() + if clean: newS = newS.clean() return newS def _combineWithBase(self, obj): @@ -2257,6 +2240,7 @@ class Workplane(CQ): r = obj if baseSolid is not None: r = baseSolid.fuse(obj) + baseSolid.wrapped = r.wrapped return self.newObject([r]) @@ -2274,8 +2258,7 @@ class Workplane(CQ): for ss in items: s = s.fuse(ss) - if clean: - s = s.clean() + if clean: s = s.clean() return self.newObject([s]) @@ -2293,21 +2276,20 @@ class Workplane(CQ): :return: a CQ object with the resulting object selected """ - # first collect all of the items together - if type(toUnion) == CQ or type(toUnion) == Workplane: + #first collect all of the items together + if isinstance(toUnion, CQ): solids = toUnion.solids().vals() if len(solids) < 1: - raise ValueError( - "CQ object must have at least one solid on the stack to union!") + 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): + elif type(toUnion) == Solid: newS = toUnion else: raise ValueError("Cannot union type '{}'".format(type(toUnion))) - # now combine with existing solid, if there is one + #now combine with existing solid, if there is one # look for parents to cut from solidRef = self.findSolid(searchStack=True, searchParents=True) if combine and solidRef is not None: @@ -2316,8 +2298,7 @@ class Workplane(CQ): else: r = newS - if clean: - r = r.clean() + if clean: r = r.clean() return self.newObject([r]) @@ -2343,21 +2324,20 @@ class Workplane(CQ): solidToCut = None if type(toCut) == CQ or type(toCut) == Workplane: solidToCut = toCut.val() - elif type(toCut) in (Solid, Compound): + elif type(toCut) == Solid: solidToCut = toCut else: raise ValueError("Cannot cut type '{}'".format(type(toCut))) newS = solidRef.cut(solidToCut) - if clean: - newS = newS.clean() + if clean: newS = newS.clean() if combine: solidRef.wrapped = newS.wrapped return self.newObject([newS]) - + def intersect(self, toIntersect, combine=True, clean=True): """ Intersects the provided solid from the current solid. @@ -2395,6 +2375,7 @@ class Workplane(CQ): return self.newObject([newS]) + def cutBlind(self, distanceToCut, clean=True): """ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. @@ -2414,18 +2395,18 @@ class Workplane(CQ): Future Enhancements: Cut Up to Surface """ - # first, make the object + #first, make the object toCut = self._extrude(distanceToCut) - # now find a solid in the chain + #now find a solid in the chain solidRef = self.findSolid() s = solidRef.cut(toCut) - if clean: - s = s.clean() + if clean: s = s.clean() + solidRef.wrapped = s.wrapped return self.newObject([s]) def cutThruAll(self, positive=False, clean=True): @@ -2482,22 +2463,21 @@ class Workplane(CQ): extrude along a profile (sweep) """ - # group wires together into faces based on which ones are inside the others - # result is a list of lists + #group wires together into faces based on which ones are inside the others + #result is a list of lists s = time.time() - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), self.plane, []) - # print "sorted wires in %d sec" % ( time.time() - s ) - # now all of the wires have been used to create an extrusion - self.ctx.pendingWires = [] + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + #print "sorted wires in %d sec" % ( time.time() - s ) + self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion - # compute extrusion vector and extrude + #compute extrusion vector and extrude eDir = self.plane.zDir.multiply(distance) - # one would think that fusing faces into a compound and then extruding would work, - # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) - # but then cutting it from the main solid fails with BRep_NotDone. - # the work around is to extrude each and then join the resulting solids, which seems to work + + #one would think that fusing faces into a compound and then extruding would work, + #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + #but then cutting it from the main solid fails with BRep_NotDone. + #the work around is to extrude each and then join the resulting solids, which seems to work # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are # multiple sets @@ -2522,8 +2502,7 @@ class Workplane(CQ): toFuse.append(thisObj) if both: - thisObj = Solid.extrudeLinear( - ws[0], ws[1:], eDir.multiply(-1.)) + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.)) toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2542,43 +2521,53 @@ class Workplane(CQ): This method is a utility method, primarily for plugin and internal use. """ - # We have to gather the wires to be revolved - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), self.plane, []) + #We have to gather the wires to be revolved + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) - # Mark that all of the wires have been used to create a revolution + #Mark that all of the wires have been used to create a revolution self.ctx.pendingWires = [] - # Revolve the wires, make a compound out of them and then fuse them + #Revolve the wires, make a compound out of them and then fuse them toFuse = [] for ws in wireSets: - thisObj = Solid.revolve( - ws[0], ws[1:], angleDegrees, axisStart, axisEnd) + thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) toFuse.append(thisObj) return Compound.makeCompound(toFuse) - def _sweep(self, path, makeSolid=True, isFrenet=False): + def _sweep(self, path, sweepAlongWires=False, makeSolid=True, isFrenet=False): """ Makes a swept solid from an existing set of pending wires. :param path: A wire along which the pending wires will be swept + :param boolean sweepAlongWires: + False to create multiple swept from wires on the chain along path + True to create only one solid swept along path with shape following the list of wires on the chain :return:a FreeCAD solid, suitable for boolean operations """ # group wires together into faces based on which ones are inside the others # result is a list of lists s = time.time() - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), self.plane, []) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) # print "sorted wires in %d sec" % ( time.time() - s ) - # now all of the wires have been used to create an extrusion - self.ctx.pendingWires = [] + self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion toFuse = [] - for ws in wireSets: - thisObj = Solid.sweep( - ws[0], ws[1:], path.val(), makeSolid, isFrenet) + if not sweepAlongWires: + for ws in wireSets: + thisObj = Solid.sweep(ws[0], ws[1:], path.val(), makeSolid, isFrenet) + toFuse.append(thisObj) + else: + section = [] + for ws in wireSets: + for i in range(0, len(ws)): + section.append(ws[i]) + + # implementation + outW = Wire(section[0].wrapped) + inW = section[1:] + thisObj = Solid.sweep(outW, inW, path.val(), makeSolid, isFrenet) toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2641,11 +2630,11 @@ class Workplane(CQ): boxes = self.eachpoint(_makebox, True) - # if combination is not desired, just return the created boxes + #if combination is not desired, just return the created boxes if not combine: return boxes else: - # combine everything + #combine everything return self.union(boxes, clean=clean) def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, @@ -2741,17 +2730,5 @@ class Workplane(CQ): try: cleanObjects = [obj.clean() for obj in self.objects] except AttributeError: - raise AttributeError( - "%s object doesn't support `clean()` method!" % obj.ShapeType()) + raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType()) return self.newObject(cleanObjects) - - def _repr_html_(self): - """ - Special method for rendering current object in a jupyter notebook - """ - - if type(self.objects[0]) is Vector: - return '< {} >'.format(self.__repr__()[1:-1]) - else: - return Compound.makeCompound(self.objects)._repr_html_() - From 3bd51dc12ee8c323a0b10e79b92685dd825133cb Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 19:51:12 +0100 Subject: [PATCH 06/28] Fixed bounding box --- cadquery/occ_impl/geom.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index c384200a..db5b4f8c 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -796,7 +796,7 @@ class BoundBox(object): @classmethod def _fromTopoDS(cls, shape, tol=TOL, optimal=False): ''' - Constructs a bounnding box from a TopoDS_Shape + Constructs a bounding box from a TopoDS_Shape ''' bbox = Bnd_Box() bbox.SetGap(tol) @@ -811,6 +811,14 @@ class BoundBox(object): return cls(bbox) - def isInside(self, anotherBox): + def isInside(self, b2): """Is the provided bounding box inside this one?""" - return not anotherBox.wrapped.IsOut(self.wrapped) + if (b2.xmin > self.xmin and + b2.ymin > self.ymin and + b2.zmin > self.zmin and + b2.xmax < self.xmax and + b2.ymax < self.ymax and + b2.zmax < self.zmax): + return True + else: + return False From e24d83ab3a5b3703df08f9febe260552a307a8a0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 20:10:11 +0100 Subject: [PATCH 07/28] Refactored sweep methods --- cadquery/occ_impl/shapes.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 86688b68..dc225097 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1370,15 +1370,39 @@ class Solid(Shape, Mixin3D): if makeSolid: face = Face.makeFromWires(outerWire, innerWires) - builder = BRepOffsetAPI_MakePipe(path.wrapped, face.wrapped) - + rv = cls(builder.Shape()) else: - builder = BRepOffsetAPI_MakePipeShell(path.wrapped) - builder.Add(outerWire.wrapped) - for w in innerWires: + shapes = [] + for w in [outerWire]+innerWires: + builder = BRepOffsetAPI_MakePipeShell(path.wrapped) + builder.SetMode(isFrenet) builder.Add(w.wrapped) + shapes.append(cls(builder.Shape())) + + rv = Compound.makeCompound(shapes) + return rv + + @classmethod + def sweep_multi(cls, profiles, path, makeSolid=True, isFrenet=False): + """ + Multi section sweep. Only single outer profile per section is allowed. + + :param profiles: list of profiles + :param path: The wire to sweep the face resulting from the wires over + :return: a Solid object + """ + + builder = BRepOffsetAPI_MakePipeShell(path.wrapped) + + for p in profiles: + builder.add(p.wrapped) + + if makeSolid: + builder.MakeSolid() + + builder.SetMode(isFrenet) builder.Build() return cls(builder.Shape()) From 2adf22cb65435ed78cfa7386cf8b826abfd3b7da Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 20:12:30 +0100 Subject: [PATCH 08/28] Restored some changes on OCC branch --- cadquery/cq.py | 400 +++++++++++++++++++++++++++---------------------- 1 file changed, 218 insertions(+), 182 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 67226a04..417999bc 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -23,8 +23,6 @@ from cadquery import * from cadquery import selectors from cadquery import exporters -from copy import copy, deepcopy - class CQContext(object): """ @@ -33,9 +31,11 @@ class CQContext(object): All objects in the same CQ chain share a reference to this same object instance which allows for shared state when needed, """ + def __init__(self): self.pendingWires = [] # a list of wires that have been created and need to be extruded - self.pendingEdges = [] # a list of created pending edges that need to be joined into wires + # a list of created pending edges that need to be joined into wires + self.pendingEdges = [] # a reference to the first point for a set of edges. # Used to determine how to behave when close() is called self.firstPoint = None @@ -77,7 +77,7 @@ class CQ(object): Custom plugins and subclasses should use this method to create new CQ objects correctly. """ - r = type(self)(None) # create a completely blank one + r = CQ(None) # create a completely blank one r.parent = self r.ctx = self.ctx # context solid remains the same r.objects = list(objlist) @@ -169,8 +169,8 @@ class CQ(object): Most of the time, both objects will contain a single solid, which is combined and returned on the stack of the new object. """ - #loop through current stack objects, and combine them - #TODO: combine other types of objects as well, like edges and wires + # loop through current stack objects, and combine them + # TODO: combine other types of objects as well, like edges and wires toCombine = self.solids().vals() if otherCQToCombine: @@ -180,18 +180,17 @@ class CQ(object): if len(toCombine) < 1: raise ValueError("Cannot Combine: at least one solid required!") - #get context solid and we don't want to find our own objects + # get context solid and we don't want to find our own objects ctxSolid = self.findSolid(searchStack=False, searchParents=True) if ctxSolid is None: ctxSolid = toCombine.pop(0) - #now combine them all. make sure to save a reference to the ctxSolid pointer! + # now combine them all. make sure to save a reference to the ctxSolid pointer! s = ctxSolid for tc in toCombine: s = s.fuse(tc) - ctxSolid.wrapped = s.wrapped return self.newObject([s]) def all(self): @@ -239,7 +238,7 @@ class CQ(object): """ if type(obj) == list: self.objects.extend(obj) - elif isinstance(obj, CQ): + elif type(obj) == CQ or type(obj) == Workplane: self.objects.extend(obj.objects) else: self.objects.append(obj) @@ -254,7 +253,7 @@ class CQ(object): """ return self.objects[0] - def toFreecad(self): + def toOCC(self): """ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code needed when rendering a model in FreeCAD's 3D view. @@ -313,9 +312,9 @@ class CQ(object): n1 = f1.normalAt() # test normals (direction of planes) - if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or - (abs(n0.y-n1.y) < self.ctx.tolerance) or - (abs(n0.z-n1.z) < self.ctx.tolerance)): + if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or + (abs(n0.y - n1.y) < self.ctx.tolerance) or + (abs(n0.z - n1.z) < self.ctx.tolerance)): return False # test if p1 is on the plane of f0 (offset of planes) @@ -330,14 +329,15 @@ class CQ(object): """ xd = Vector(0, 0, 1).cross(normal) if xd.Length < self.ctx.tolerance: - #this face is parallel with the x-y plane, so choose x to be in global coordinates + # this face is parallel with the x-y plane, so choose x to be in global coordinates xd = Vector(1, 0, 0) return xd if len(self.objects) > 1: # are all objects 'PLANE'? - if not all(o.geomType() == 'PLANE' for o in self.objects): - raise ValueError("If multiple objects selected, they all must be planar faces.") + if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects): + raise ValueError( + "If multiple objects selected, they all must be planar faces.") # are all faces co-planar with each other? if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): @@ -356,9 +356,9 @@ class CQ(object): if isinstance(obj, Face): if centerOption == 'CenterOfMass': - center = obj.Center() + center = obj.Center() elif centerOption == 'CenterOfBoundBox': - center = obj.CenterOfBoundBox() + center = obj.CenterOfBoundBox() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: @@ -370,23 +370,24 @@ class CQ(object): normal = self.plane.zDir xDir = self.plane.xDir else: - raise ValueError("Needs a face or a vertex or point on a work plane") + raise ValueError( + "Needs a face or a vertex or point on a work plane") - #invert if requested + # invert if requested if invert: normal = normal.multiply(-1.0) - #offset origin if desired + # offset origin if desired offsetVector = normal.normalized().multiply(offset) offsetCenter = center.add(offsetVector) - #make the new workplane + # make the new workplane plane = Plane(offsetCenter, xDir, normal) s = Workplane(plane) s.parent = self s.ctx = self.ctx - #a new workplane has the center of the workplane on the stack + # a new workplane has the center of the workplane on the stack return s def first(self): @@ -458,7 +459,7 @@ class CQ(object): if isinstance(s, Solid): return s elif isinstance(s, Compound): - return s.Solids() + return s if searchParents and self.parent is not None: return self.parent.findSolid(searchStack=True, searchParents=searchParents) @@ -481,10 +482,9 @@ class CQ(object): toReturn = self._collectProperty(objType) if selector is not None: - # if isinstance(selector, str) or isinstance(selector, str): - try: + if isinstance(selector, str) or isinstance(selector, str): selectorObj = selectors.StringSyntaxSelector(selector) - except: + else: selectorObj = selector toReturn = selectorObj.filter(toReturn) @@ -671,7 +671,7 @@ class CQ(object): """ return self._selectObjects('Compounds', selector) - def toSvg(self, opts=None, view_vector=(-1.75,1.1,5)): + def toSvg(self, opts=None): """ Returns svg text that represents the first item on the stack. @@ -679,26 +679,20 @@ class CQ(object): :param opts: svg formatting options :type opts: dictionary, width and height - - :param view_vector: camera's view direction vector - :type view_vector: tuple, (x, y, z) :return: a string that contains SVG that represents this item. """ - return exporters.getSVG(self.val().wrapped, opts=opts, view_vector=view_vector) + return exporters.getSVG(self.val(), opts) - def exportSvg(self, fileName, view_vector=(-1.75,1.1,5)): + def exportSvg(self, fileName): """ Exports the first item on the stack as an SVG file For testing purposes mainly. :param fileName: the filename to export - - :param view_vector: camera's view direction vector - :type view_vector: tuple, (x, y, z) :type fileName: String, absolute path to the file """ - exporters.exportSVG(self, fileName, view_vector) + exporters.exportSVG(self, fileName) def rotateAboutCenter(self, axisEndPoint, angleDegrees): """ @@ -719,13 +713,13 @@ class CQ(object): Future Enhancements: * A version of this method that returns a transformed copy, rather than modifying the originals - * This method doesn't expose a very good interface, because the axis of rotation + * This method doesnt expose a very good interface, because the axis of rotation could be inconsistent between multiple objects. This is because the beginning of the axis is variable, while the end is fixed. This is fine when operating on one object, but is not cool for multiple. """ - #center point is the first point in the vector + # center point is the first point in the vector endVec = Vector(axisEndPoint) def _rot(obj): @@ -760,10 +754,10 @@ class CQ(object): :param basePointVector: the base point to mirror about :type basePointVector: tuple """ - newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)]) + newS = self.newObject( + [self.objects[0].mirror(mirrorPlane, basePointVector)]) return newS.first() - def translate(self, vec): """ Returns a copy of all of the items on the stack moved by the specified translation vector. @@ -774,7 +768,6 @@ class CQ(object): """ return self.newObject([o.translate(vec) for o in self.objects]) - def shell(self, thickness): """ Remove the selected faces to create a shell of the specified thickness. @@ -818,7 +811,6 @@ class CQ(object): raise ValueError("Shelling requires that faces be selected") s = solidRef.shell(self.objects, thickness) - solidRef.wrapped = s.wrapped return self.newObject([s]) def fillet(self, radius): @@ -849,7 +841,6 @@ class CQ(object): raise ValueError("Fillets requires that edges be selected") s = solid.fillet(radius, edgeList) - solid.wrapped = s.wrapped return self.newObject([s]) def chamfer(self, length, length2=None): @@ -888,15 +879,8 @@ class CQ(object): s = solid.chamfer(length, length2, edgeList) - solid.wrapped = s.wrapped return self.newObject([s]) - def __copy__(self): - return self.newObject(copy(self.objects)) - - def __deepcopy__(self, memo): - return self.newObject(deepcopy(self.objects, memo)) - class Workplane(CQ): """ @@ -950,11 +934,10 @@ class Workplane(CQ): if inPlane.__class__.__name__ == 'Plane': tmpPlane = inPlane + elif isinstance(inPlane, str) or isinstance(inPlane, str): + tmpPlane = Plane.named(inPlane, origin) else: - try: - tmpPlane = Plane.named(inPlane, origin) - except ValueError: - tmpPlane = None + tmpPlane = None if tmpPlane is None: raise ValueError( @@ -980,7 +963,7 @@ class Workplane(CQ): :return: a new work plane, transformed as requested """ - #old api accepted a vector, so we'll check for that. + # old api accepted a vector, so we'll check for that. if rotate.__class__.__name__ == 'Vector': rotate = rotate.toTuple() @@ -1006,8 +989,8 @@ class Workplane(CQ): :return: a new Workplane object with the current workplane as a parent. """ - #copy the current state to the new object - ns = type(self)("XY") + # copy the current state to the new object + ns = Workplane("XY") ns.plane = self.plane ns.parent = self ns.objects = list(objlist) @@ -1042,7 +1025,8 @@ class Workplane(CQ): elif isinstance(obj, Vector): p = obj else: - raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) + raise RuntimeError( + "Cannot convert object type '%s' to vector " % type(obj)) if useLocalCoords: return self.plane.toLocalCoords(p) @@ -1063,7 +1047,7 @@ class Workplane(CQ): false, the lower left corner will be at the center of the work plane """ - if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1: + if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1: raise ValueError("Spacing and count must be > 0 ") lpoints = [] # coordinates relative to bottom left point @@ -1071,17 +1055,17 @@ class Workplane(CQ): for y in range(yCount): lpoints.append((xSpacing * x, ySpacing * y)) - #shift points down and left relative to origin if requested + # shift points down and left relative to origin if requested if center: - xc = xSpacing*(xCount-1) * 0.5 - yc = ySpacing*(yCount-1) * 0.5 + xc = xSpacing * (xCount - 1) * 0.5 + yc = ySpacing * (yCount - 1) * 0.5 cpoints = [] for p in lpoints: cpoints.append((p[0] - xc, p[1] - yc)) lpoints = list(cpoints) return self.pushPoints(lpoints) - + def polarArray(self, radius, startAngle, angle, count, fill=True): """ Creates an polar array of points and pushes them onto the stack. @@ -1258,7 +1242,7 @@ class Workplane(CQ): """ p = self._findFromPoint(True) return self.lineTo(xCoord, p.y, forConstruction) - + def polarLine(self, distance, angle, forConstruction=False): """ Make a line of the given length, at the given angle from the current point @@ -1287,8 +1271,8 @@ class Workplane(CQ): y = math.sin(math.radians(angle)) * distance return self.lineTo(x, y, forConstruction) - - #absolute move in current plane, not drawing + + # absolute move in current plane, not drawing def moveTo(self, x=0, y=0): """ Move to the specified point, without drawing. @@ -1307,7 +1291,7 @@ class Workplane(CQ): newCenter = Vector(x, y, 0) return self.newObject([self.plane.toWorldCoords(newCenter)]) - #relative move in current plane, not drawing + # relative move in current plane, not drawing def move(self, xDist=0, yDist=0): """ Move the specified distance from the current point, without drawing. @@ -1388,17 +1372,17 @@ class Workplane(CQ): provide tangent arcs """ - startPoint = self._findFromPoint(False) - point1 = self.plane.toWorldCoords(point1) - point2 = self.plane.toWorldCoords(point2) + gstartPoint = self._findFromPoint(False) + gpoint1 = self.plane.toWorldCoords(point1) + gpoint2 = self.plane.toWorldCoords(point2) - arc = Edge.makeThreePointArc(startPoint, point1, point2) + arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2) if not forConstruction: self._addPendingEdge(arc) return self.newObject([arc]) - + def sagittaArc(self, endPoint, sag, forConstruction=False): """ Draw an arc from the current point to endPoint with an arc defined by the sag (sagitta). @@ -1478,19 +1462,20 @@ class Workplane(CQ): faster implementation: this one transforms 3 times to accomplish the result """ - #convert edges to a wire, if there are pending edges + # convert edges to a wire, if there are pending edges n = self.wire(forConstruction=False) - #attempt to consolidate wires together. + # attempt to consolidate wires together. consolidated = n.consolidateWires() - rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) + rotatedWires = self.plane.rotateShapes( + consolidated.wires().vals(), matrix) for w in rotatedWires: consolidated.objects.append(w) consolidated._addPendingWire(w) - #attempt again to consolidate all of the wires + # attempt again to consolidate all of the wires c = consolidated.consolidateWires() return c @@ -1511,11 +1496,23 @@ class Workplane(CQ): Produces a flat, heart shaped object Future Enhancements: - mirrorX().mirrorY() should work but doesn't, due to some FreeCAD weirdness + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ - tm = Matrix() - tm.rotateY(math.pi) - return self.rotateAndCopy(tm) + # convert edges to a wire, if there are pending edges + n = self.wire(forConstruction=False) + + # attempt to consolidate wires together. + consolidated = n.consolidateWires() + + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), + 'Y') + + for w in mirroredWires: + consolidated.objects.append(w) + consolidated._addPendingWire(w) + + # attempt again to consolidate all of the wires + return consolidated.consolidateWires() def mirrorX(self): """ @@ -1529,11 +1526,23 @@ class Workplane(CQ): Typically used to make creating wires with symmetry easier. Future Enhancements: - mirrorX().mirrorY() should work but doesn't, due to some FreeCAD weirdness + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ - tm = Matrix() - tm.rotateX(math.pi) - return self.rotateAndCopy(tm) + # convert edges to a wire, if there are pending edges + n = self.wire(forConstruction=False) + + # attempt to consolidate wires together. + consolidated = n.consolidateWires() + + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), + 'X') + + for w in mirroredWires: + consolidated.objects.append(w) + consolidated._addPendingWire(w) + + # attempt again to consolidate all of the wires + return consolidated.consolidateWires() def _addPendingEdge(self, edge): """ @@ -1578,15 +1587,15 @@ class Workplane(CQ): if len(wires) < 2: return self - #TODO: this makes the assumption that either all wires could be combined, or none. - #in reality trying each combination of wires is probably not reasonable anyway + # TODO: this makes the assumption that either all wires could be combined, or none. + # in reality trying each combination of wires is probably not reasonable anyway w = Wire.combine(wires) - #ok this is a little tricky. if we consolidate wires, we have to actually - #modify the pendingWires collection to remove the original ones, and replace them - #with the consolidate done - #since we are already assuming that all wires could be consolidated, its easy, we just - #clear the pending wire list + # ok this is a little tricky. if we consolidate wires, we have to actually + # modify the pendingWires collection to remove the original ones, and replace them + # with the consolidate done + # since we are already assuming that all wires could be consolidated, its easy, we just + # clear the pending wire list r = self.newObject([w]) r.ctx.pendingWires = [] r._addPendingWire(w) @@ -1615,7 +1624,7 @@ class Workplane(CQ): edges = self.ctx.pendingEdges - #do not consolidate if there are no free edges + # do not consolidate if there are no free edges if len(edges) == 0: return self @@ -1626,7 +1635,6 @@ class Workplane(CQ): if type(e) != Edge: others.append(e) - w = Wire.assembleEdges(edges) if not forConstruction: self._addPendingWire(w) @@ -1669,7 +1677,7 @@ class Workplane(CQ): for obj in self.objects: if useLocalCoordinates: - #TODO: this needs to work for all types of objects, not just vectors! + # TODO: this needs to work for all types of objects, not just vectors! r = callBackFunction(self.plane.toLocalCoords(obj)) r = r.transformShape(self.plane.rG) else: @@ -1700,11 +1708,11 @@ class Workplane(CQ): If the stack has zero length, a single point is returned, which is the center of the current workplane/coordinate system """ - #convert stack to a list of points + # convert stack to a list of points pnts = [] if len(self.objects) == 0: - #nothing on the stack. here, we'll assume we should operate with the - #origin as the context point + # nothing on the stack. here, we'll assume we should operate with the + # origin as the context point pnts.append(self.plane.origin) else: @@ -1743,10 +1751,10 @@ class Workplane(CQ): # Here pnt is in local coordinates due to useLocalCoords=True # (xc,yc,zc) = pnt.toTuple() if centered: - p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0)) - p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0, 0)) - p3 = pnt.add(Vector(xLen/2.0, yLen/2.0, 0)) - p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0)) + p1 = pnt.add(Vector(xLen / -2.0, yLen / -2.0, 0)) + p2 = pnt.add(Vector(xLen / 2.0, yLen / -2.0, 0)) + p3 = pnt.add(Vector(xLen / 2.0, yLen / 2.0, 0)) + p4 = pnt.add(Vector(xLen / -2.0, yLen / 2.0, 0)) else: p1 = pnt p2 = pnt.add(Vector(xLen, 0, 0)) @@ -1755,11 +1763,11 @@ class Workplane(CQ): w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction) return w - #return Part.makePolygon([p1,p2,p3,p4,p1]) + # return Part.makePolygon([p1,p2,p3,p4,p1]) return self.eachpoint(makeRectangleWire, True) - #circle from current point + # circle from current point def circle(self, radius, forConstruction=False): """ Make a circle for each item on the stack. @@ -1808,12 +1816,12 @@ class Workplane(CQ): :return: a polygon wire """ def _makePolygon(center): - #pnt is a vector in local coordinates + # pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] - for i in range(nSides+1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)), - (diameter / 2.0 * math.sin(angle*i)), 0)) + for i in range(nSides + 1): + pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), + (diameter / 2.0 * math.sin(angle * i)), 0)) return Wire.makePolygon(pnts, forConstruction) return self.eachpoint(_makePolygon, True) @@ -1876,7 +1884,7 @@ class Workplane(CQ): self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) # Need to reset the first point after closing a wire - self.ctx.firstPoint=None + self.ctx.firstPoint = None return self.wire() @@ -1887,8 +1895,8 @@ class Workplane(CQ): how long or wide a feature must be to make sure to cut through all of the material :return: A value representing the largest dimension of the first solid on the stack """ - #TODO: this implementation is naive and returns the dims of the first solid... most of - #TODO: the time this works. but a stronger implementation would be to search all solids. + # TODO: this implementation is naive and returns the dims of the first solid... most of + # TODO: the time this works. but a stronger implementation would be to search all solids. s = self.findSolid() if s: return s.BoundingBox().DiagonalLength * 5.0 @@ -1909,18 +1917,18 @@ class Workplane(CQ): if ctxSolid is None: raise ValueError("Must have a solid in the chain to cut from!") - #will contain all of the counterbores as a single compound + # will contain all of the counterbores as a single compound results = self.eachpoint(fcn, useLocalCoords).vals() s = ctxSolid for cb in results: s = s.cut(cb) - if clean: s = s.clean() + if clean: + s = s.clean() - ctxSolid.wrapped = s.wrapped return self.newObject([s]) - #but parameter list is different so a simple function pointer won't work + # but parameter list is different so a simple function pointer wont work def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True): """ Makes a counterbored hole for each item on the stack. @@ -1961,18 +1969,20 @@ class Workplane(CQ): pnt is in local coordinates """ boreDir = Vector(0, 0, -1) - #first make the hole - hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates! + # first make the hole + hole = Solid.makeCylinder( + diameter / 2.0, depth, center, boreDir) # local coordianates! - #add the counter bore - cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) + # add the counter bore + cbore = Solid.makeCylinder( + cboreDiameter / 2.0, cboreDepth, center, boreDir) r = hole.fuse(cbore) return r return self.cutEach(_makeCbore, True, clean) - #TODO: almost all code duplicated! - #but parameter list is different so a simple function pointer won't work + # TODO: almost all code duplicated! + # but parameter list is different so a simple function pointer wont work def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True): """ Makes a countersunk hole for each item on the stack. @@ -2008,12 +2018,13 @@ class Workplane(CQ): depth = self.largestDimension() def _makeCsk(center): - #center is in local coordinates + # center is in local coordinates boreDir = Vector(0, 0, -1) - #first make the hole - hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords! + # first make the hole + hole = Solid.makeCylinder( + diameter / 2.0, depth, center, boreDir) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) csk = Solid.makeCone(r, 0.0, h, center, boreDir) @@ -2022,8 +2033,8 @@ class Workplane(CQ): return self.cutEach(_makeCsk, True, clean) - #TODO: almost all code duplicated! - #but parameter list is different so a simple function pointer won't work + # TODO: almost all code duplicated! + # but parameter list is different so a simple function pointer wont work def hole(self, diameter, depth=None, clean=True): """ Makes a hole for each item on the stack. @@ -2060,13 +2071,14 @@ class Workplane(CQ): pnt is in local coordinates """ boreDir = Vector(0, 0, -1) - #first make the hole - hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates! + # first make the hole + hole = Solid.makeCylinder( + diameter / 2.0, depth, center, boreDir) # local coordinates! return hole return self.cutEach(_makeHole, True, clean) - #TODO: duplicated code with _extrude and extrude + # TODO: duplicated code with _extrude and extrude def twistExtrude(self, distance, angleDegrees, combine=True, clean=True): """ Extrudes a wire in the direction normal to the plane, but also twists by the specified @@ -2086,21 +2098,23 @@ class Workplane(CQ): :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape :return: a CQ object with the resulting solid selected. """ - #group wires together into faces based on which ones are inside the others - #result is a list of lists - wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + # group wires together into faces based on which ones are inside the others + # result is a list of lists + wireSets = sortWiresByBuildOrder( + list(self.ctx.pendingWires), self.plane, []) - self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + # now all of the wires have been used to create an extrusion + self.ctx.pendingWires = [] - #compute extrusion vector and extrude + # compute extrusion vector and extrude eDir = self.plane.zDir.multiply(distance) - #one would think that fusing faces into a compound and then extruding would work, - #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) - #but then cutting it from the main solid fails with BRep_NotDone. - #the work around is to extrude each and then join the resulting solids, which seems to work + # one would think that fusing faces into a compound and then extruding would work, + # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + # but then cutting it from the main solid fails with BRep_NotDone. + # the work around is to extrude each and then join the resulting solids, which seems to work - #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there + # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there # are multiple sets r = None for ws in wireSets: @@ -2115,7 +2129,8 @@ class Workplane(CQ): newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() return newS def extrude(self, distance, combine=True, clean=True, both=False): @@ -2143,13 +2158,15 @@ class Workplane(CQ): perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar """ - r = self._extrude(distance,both=both) # returns a Solid (or a compound if there were multiple) + r = self._extrude( + distance, both=both) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() return newS def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): @@ -2174,10 +2191,10 @@ class Workplane(CQ): * if combine is true, the value is combined with the context solid if it exists, and the resulting solid becomes the new context solid. """ - #Make sure we account for users specifying angles larger than 360 degrees + # Make sure we account for users specifying angles larger than 360 degrees angleDegrees %= 360.0 - #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve + # Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees # The default start point of the vector defining the axis of rotation will be the origin @@ -2205,7 +2222,8 @@ class Workplane(CQ): newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() return newS def sweep(self, path, sweepAlongWires=False, makeSolid=True, isFrenet=False, combine=True, clean=True): @@ -2240,7 +2258,6 @@ class Workplane(CQ): r = obj if baseSolid is not None: r = baseSolid.fuse(obj) - baseSolid.wrapped = r.wrapped return self.newObject([r]) @@ -2258,7 +2275,8 @@ class Workplane(CQ): for ss in items: s = s.fuse(ss) - if clean: s = s.clean() + if clean: + s = s.clean() return self.newObject([s]) @@ -2276,20 +2294,21 @@ class Workplane(CQ): :return: a CQ object with the resulting object selected """ - #first collect all of the items together - if isinstance(toUnion, CQ): + # first collect all of the items together + if type(toUnion) == CQ or type(toUnion) == Workplane: solids = toUnion.solids().vals() if len(solids) < 1: - raise ValueError("CQ object must have at least one solid on the stack to union!") + 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) == Solid: + elif type(toUnion) in (Solid, Compound): newS = toUnion else: raise ValueError("Cannot union type '{}'".format(type(toUnion))) - #now combine with existing solid, if there is one + # now combine with existing solid, if there is one # look for parents to cut from solidRef = self.findSolid(searchStack=True, searchParents=True) if combine and solidRef is not None: @@ -2298,7 +2317,8 @@ class Workplane(CQ): else: r = newS - if clean: r = r.clean() + if clean: + r = r.clean() return self.newObject([r]) @@ -2324,20 +2344,21 @@ class Workplane(CQ): solidToCut = None if type(toCut) == CQ or type(toCut) == Workplane: solidToCut = toCut.val() - elif type(toCut) == Solid: + elif type(toCut) in (Solid, Compound): solidToCut = toCut else: raise ValueError("Cannot cut type '{}'".format(type(toCut))) newS = solidRef.cut(solidToCut) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() if combine: solidRef.wrapped = newS.wrapped return self.newObject([newS]) - + def intersect(self, toIntersect, combine=True, clean=True): """ Intersects the provided solid from the current solid. @@ -2375,7 +2396,6 @@ class Workplane(CQ): return self.newObject([newS]) - def cutBlind(self, distanceToCut, clean=True): """ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. @@ -2395,18 +2415,18 @@ class Workplane(CQ): Future Enhancements: Cut Up to Surface """ - #first, make the object + # first, make the object toCut = self._extrude(distanceToCut) - #now find a solid in the chain + # now find a solid in the chain solidRef = self.findSolid() s = solidRef.cut(toCut) - if clean: s = s.clean() + if clean: + s = s.clean() - solidRef.wrapped = s.wrapped return self.newObject([s]) def cutThruAll(self, positive=False, clean=True): @@ -2463,21 +2483,22 @@ class Workplane(CQ): extrude along a profile (sweep) """ - #group wires together into faces based on which ones are inside the others - #result is a list of lists + # group wires together into faces based on which ones are inside the others + # result is a list of lists s = time.time() - wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) - #print "sorted wires in %d sec" % ( time.time() - s ) - self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + wireSets = sortWiresByBuildOrder( + list(self.ctx.pendingWires), self.plane, []) + # print "sorted wires in %d sec" % ( time.time() - s ) + # now all of the wires have been used to create an extrusion + self.ctx.pendingWires = [] - #compute extrusion vector and extrude + # compute extrusion vector and extrude eDir = self.plane.zDir.multiply(distance) - - #one would think that fusing faces into a compound and then extruding would work, - #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) - #but then cutting it from the main solid fails with BRep_NotDone. - #the work around is to extrude each and then join the resulting solids, which seems to work + # one would think that fusing faces into a compound and then extruding would work, + # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + # but then cutting it from the main solid fails with BRep_NotDone. + # the work around is to extrude each and then join the resulting solids, which seems to work # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are # multiple sets @@ -2502,7 +2523,8 @@ class Workplane(CQ): toFuse.append(thisObj) if both: - thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.)) + thisObj = Solid.extrudeLinear( + ws[0], ws[1:], eDir.multiply(-1.)) toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2521,16 +2543,18 @@ class Workplane(CQ): This method is a utility method, primarily for plugin and internal use. """ - #We have to gather the wires to be revolved - wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + # We have to gather the wires to be revolved + wireSets = sortWiresByBuildOrder( + list(self.ctx.pendingWires), self.plane, []) - #Mark that all of the wires have been used to create a revolution + # Mark that all of the wires have been used to create a revolution self.ctx.pendingWires = [] - #Revolve the wires, make a compound out of them and then fuse them + # Revolve the wires, make a compound out of them and then fuse them toFuse = [] for ws in wireSets: - thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) + thisObj = Solid.revolve( + ws[0], ws[1:], angleDegrees, axisStart, axisEnd) toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2630,11 +2654,11 @@ class Workplane(CQ): boxes = self.eachpoint(_makebox, True) - #if combination is not desired, just return the created boxes + # if combination is not desired, just return the created boxes if not combine: return boxes else: - #combine everything + # combine everything return self.union(boxes, clean=clean) def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, @@ -2730,5 +2754,17 @@ class Workplane(CQ): try: cleanObjects = [obj.clean() for obj in self.objects] except AttributeError: - raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType()) + raise AttributeError( + "%s object doesn't support `clean()` method!" % obj.ShapeType()) return self.newObject(cleanObjects) + + def _repr_html_(self): + """ + Special method for rendering current object in a jupyter notebook + """ + + if type(self.objects[0]) is Vector: + return '< {} >'.format(self.__repr__()[1:-1]) + else: + return Compound.makeCompound(self.objects)._repr_html_() + From 10be1be57714c0511deb22900facfb6b0e700002 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 20:44:03 +0100 Subject: [PATCH 09/28] Additional refactoring of sweep_multi --- cadquery/occ_impl/shapes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index dc225097..d14a42be 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1364,7 +1364,6 @@ class Solid(Shape, Mixin3D): :param path: The wire to sweep the face resulting from the wires over :return: a Solid object """ - if path.ShapeType() == 'Edge': path = Wire.assembleEdges([path, ]) @@ -1393,17 +1392,21 @@ class Solid(Shape, Mixin3D): :param path: The wire to sweep the face resulting from the wires over :return: a Solid object """ + if path.ShapeType() == 'Edge': + path = Wire.assembleEdges([path, ]) builder = BRepOffsetAPI_MakePipeShell(path.wrapped) for p in profiles: - builder.add(p.wrapped) + builder.Add(p.wrapped) + + builder.SetMode(isFrenet) + builder.Build() if makeSolid: builder.MakeSolid() - builder.SetMode(isFrenet) - builder.Build() + return cls(builder.Shape()) From 7c3359689e005983e003b7c1d61728f9affd008e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 20:44:39 +0100 Subject: [PATCH 10/28] Use sweep_multi for sweep with sweepAlongWires option --- cadquery/cq.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 417999bc..1af0e869 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2589,9 +2589,7 @@ class Workplane(CQ): section.append(ws[i]) # implementation - outW = Wire(section[0].wrapped) - inW = section[1:] - thisObj = Solid.sweep(outW, inW, path.val(), makeSolid, isFrenet) + thisObj = Solid.sweep_multi(section, path.val(), makeSolid, isFrenet) toFuse.append(thisObj) return Compound.makeCompound(toFuse) From bcee21289f2c6ca3b92db1f961330102947d017a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 23:30:53 +0100 Subject: [PATCH 11/28] Update tests --- tests/TestCadQuery.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index c6552156..64bea83d 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -494,6 +494,35 @@ class TestCadQuery(BaseTest): # 6 faces for the box, 2 faces for each cylinder self.assertEqual(6 + NUMX * NUMY * 2, s.faces().size()) + def testPolarArray(self): + radius = 10 + + # Test for proper number of elements + s = Workplane("XY").polarArray(radius, 0, 180, 1) + self.assertEqual(1, s.size()) + s = Workplane("XY").polarArray(radius, 0, 180, 6) + self.assertEqual(6, s.size()) + + # Test for proper placement when fill == True + s = Workplane("XY").polarArray(radius, 0, 180, 3) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper placement when angle to fill is multiple of 360 deg + s = Workplane("XY").polarArray(radius, 0, 360, 4) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper placement when fill == False + s = Workplane("XY").polarArray(radius, 0, 90, 3, fill=False) + self.assertAlmostEqual(0, s.objects[1].x) + self.assertAlmostEqual(radius, s.objects[1].y) + + # Test for proper operation of startAngle + s = Workplane("XY").polarArray(radius, 90, 180, 3) + self.assertAlmostEqual(0, s.objects[0].x) + self.assertAlmostEqual(radius, s.objects[0].y) + def testNestedCircle(self): s = Workplane("XY").box(40, 40, 5).pushPoints( [(10, 0), (0, 10)]).circle(4).circle(2).extrude(4) @@ -690,6 +719,20 @@ class TestCadQuery(BaseTest): self.assertEqual(10, currentS.faces().size()) + def testIntersect(self): + """ + Tests the intersect function. + """ + s = Workplane(Plane.XY()) + currentS = s.rect(2.0, 2.0).extrude(0.5) + toIntersect = s.rect(1.0, 1.0).extrude(1) + + currentS.intersect(toIntersect.val()) + + self.assertEqual(6, currentS.faces().size()) + bb = currentS.val().BoundingBox() + self.assertListEqual([bb.xlen, bb.ylen, bb.zlen], [1, 1, 0.5]) + def testBoundingBox(self): """ Tests the boudingbox center of a model From 4c4ec5eb98b907b1fb028407513ac806fecffa8b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Dec 2018 23:39:41 +0100 Subject: [PATCH 12/28] Fixed failing tests --- cadquery/cq.py | 2 +- tests/TestCadQuery.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 1af0e869..ed544364 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2382,7 +2382,7 @@ class Workplane(CQ): if isinstance(toIntersect, CQ): solidToIntersect = toIntersect.val() - elif isinstance(toIntersect, Solid): + elif isinstance(toIntersect, Solid) or isinstance(toIntersect, Compound): solidToIntersect = toIntersect else: raise ValueError("Cannot intersect type '{}'".format(type(toIntersect))) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 64bea83d..62883711 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -730,8 +730,7 @@ class TestCadQuery(BaseTest): currentS.intersect(toIntersect.val()) self.assertEqual(6, currentS.faces().size()) - bb = currentS.val().BoundingBox() - self.assertListEqual([bb.xlen, bb.ylen, bb.zlen], [1, 1, 0.5]) + self.assertAlmostEqual(currentS.val().Volume(),0.5) def testBoundingBox(self): """ From 56ad889f57b71c940111e92407262158ca857211 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 26 Dec 2018 00:07:15 +0100 Subject: [PATCH 13/28] Increase coverage --- tests/TestCadQuery.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 62883711..db98d6f7 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -731,6 +731,11 @@ class TestCadQuery(BaseTest): self.assertEqual(6, currentS.faces().size()) self.assertAlmostEqual(currentS.val().Volume(),0.5) + + currentS.intersect(toIntersect) + + self.assertEqual(6, currentS.faces().size()) + self.assertAlmostEqual(currentS.val().Volume(),0.5) def testBoundingBox(self): """ From 9583160ad1d446eacfd881d04fcb598876c78d72 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 26 Dec 2018 00:19:34 +0100 Subject: [PATCH 14/28] Refectored Vector and added more tests --- cadquery/occ_impl/geom.py | 6 +----- tests/TestCadObjects.py | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index db5b4f8c..9592f103 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -38,11 +38,7 @@ class Vector(object): fV = gp_Vec(*arg) elif len(arg)==2: fV = gp_Vec(*arg,0) - elif isinstance(args[0], gp_Vec): - fV = gp_Vec(args[0].XYZ()) - elif isinstance(args[0], gp_Pnt): - fV = gp_Vec(args[0].XYZ()) - elif isinstance(args[0], gp_Dir): + elif isinstance(args[0], (gp_Vec, gp_Pnt, gp_Dir)): fV = gp_Vec(args[0].XYZ()) elif isinstance(args[0], gp_XYZ): fV = gp_Vec(args[0]) diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py index 2b4fd139..aa5c4ad4 100644 --- a/tests/TestCadObjects.py +++ b/tests/TestCadObjects.py @@ -2,7 +2,7 @@ import sys import unittest from tests import BaseTest -from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ +from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ, gp_XYZ from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace) @@ -24,9 +24,26 @@ class TestCadObjects(BaseTest): v1 = Vector(1, 2, 3) v2 = Vector((1, 2, 3)) v3 = Vector(gp_Vec(1, 2, 3)) + v4 = Vector([1,2,3]) + v5 = Vector(gp_XYZ(1,2,3)) - for v in [v1, v2, v3]: + for v in [v1, v2, v3, v4, v5]: self.assertTupleAlmostEquals((1, 2, 3), v.toTuple(), 4) + + v6 = Vector((1,2)) + v7 = Vector([1,2]) + v8 = Vector(1,2) + + for v in [v6, v7, v8]: + self.assertTupleAlmostEquals((1, 2, 0), v.toTuple(), 4) + + v9 = Vector() + self.assertTupleAlmostEquals((0, 0, 0), v9.toTuple(), 4) + + v9.x = 1. + v9.y = 2. + v9.z = 3. + self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4) def testVertex(self): """ From b59e277c83576b7ab96c1d930667b7630d0a6920 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 27 Dec 2018 21:08:30 +0100 Subject: [PATCH 15/28] Typo fix --- tests/TestCadQuery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index db98d6f7..d34d155f 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -1516,7 +1516,7 @@ class TestCadQuery(BaseTest): .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) self.saveModel(result) - def testIsIndide(self): + def testIsInside(self): """ Testing if one box is inside of another. """ From 786c1a2c0d13cbf081cff834000e3a750ef24a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 29 Dec 2018 23:14:17 +0100 Subject: [PATCH 16/28] Remove import STEP form URL This code is not compatible with py3 and not covered by any tests. Any thoughts? --- cadquery/occ_impl/importers.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/cadquery/occ_impl/importers.py b/cadquery/occ_impl/importers.py index 6ae08d27..e04772f4 100644 --- a/cadquery/occ_impl/importers.py +++ b/cadquery/occ_impl/importers.py @@ -4,7 +4,6 @@ from .shapes import Shape import sys import os -import urllib as urlreader import tempfile from OCC.STEPControl import STEPControl_Reader @@ -58,18 +57,3 @@ def importStep(fileName): solids.append(Shape.cast(shape)) return cadquery.Workplane("XY").newObject(solids) - -# Loads a STEP file from an URL into a CQ.Workplane object -def importStepFromURL(url): - # Now read and return the shape - try: - webFile = urlreader.urlopen(url) - tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) - tempFile.write(webFile.read()) - webFile.close() - tempFile.close() - - return importStep(tempFile.name) - except: - raise ValueError("STEP File from the URL: " + - url + " Could not be loaded") From f1cf831701e2cafb0cb79e016b2a8f8b4429abbe Mon Sep 17 00:00:00 2001 From: Jeremy Mack Wright Date: Sat, 29 Dec 2018 18:44:08 -0500 Subject: [PATCH 17/28] Reworked the core examples to exclude contributed examples. --- examples/Ex001_Simple_Block.py | 19 ++++ .../Ex002_Block_With_Bored_Center_Hole.py | 20 ++++ ...03_Pillow_Block_With_Counterbored_Holes.py | 34 ++++++ examples/Ex004_Extruded_Cylindrical_Plate.py | 29 +++++ examples/Ex005_Extruded_Lines_and_Arcs.py | 45 ++++++++ .../Ex006_Moving_the_Current_Working_Point.py | 35 ++++++ examples/Ex007_Using_Point_Lists.py | 32 ++++++ examples/Ex008_Polygon_Creation.py | 39 +++++++ examples/Ex009_Polylines.py | 39 +++++++ .../Ex010_Defining_an_Edge_with_a_Spline.py | 27 +++++ .../Ex011_Mirroring_Symmetric_Geometry.py | 20 ++++ .../Ex012_Creating_Workplanes_on_Faces.py | 16 +++ .../Ex013_Locating_a_Workplane_on_a_Vertex.py | 21 ++++ examples/Ex014_Offset_Workplanes.py | 20 ++++ examples/Ex015_Rotated_Workplanes.py | 22 ++++ examples/Ex016_Using_Construction_Geometry.py | 21 ++++ .../Ex017_Shelling_to_Create_Thin_Features.py | 14 +++ examples/Ex018_Making_Lofts.py | 20 ++++ examples/Ex019_Counter_Sunk_Holes.py | 19 ++++ .../Ex020_Rounding_Corners_with_Fillets.py | 13 +++ examples/Ex021_Splitting_an_Object.py | 25 +++++ examples/Ex022_Revolution.py | 21 ++++ examples/Ex023_Sweep.py | 40 +++++++ examples/Ex024_Sweep_Along_List_Of_Wires.py | 47 ++++++++ examples/FreeCAD/Ex001_Simple_Block.py | 32 ------ .../Ex002_Block_With_Bored_Center_Hole.py | 33 ------ ...03_Pillow_Block_With_Counterbored_Holes.py | 40 ------- .../Ex004_Extruded_Cylindrical_Plate.py | 34 ------ .../FreeCAD/Ex005_Extruded_Lines_and_Arcs.py | 33 ------ .../Ex006_Moving_the_Current_Working_Point.py | 38 ------- examples/FreeCAD/Ex007_Using_Point_Lists.py | 36 ------- examples/FreeCAD/Ex008_Polygon_Creation.py | 36 ------- examples/FreeCAD/Ex009_Polylines.py | 44 -------- .../Ex010_Defining_an_Edge_with_a_Spline.py | 45 -------- .../Ex011_Mirroring_Symmetric_Geometry.py | 34 ------ .../Ex012_Creating_Workplanes_on_Faces.py | 31 ------ .../Ex013_Locating_a_Workplane_on_a_Vertex.py | 34 ------ examples/FreeCAD/Ex014_Offset_Workplanes.py | 34 ------ examples/FreeCAD/Ex015_Rotated_Workplanes.py | 32 ------ .../Ex016_Using_Construction_Geometry.py | 29 ----- .../Ex017_Shelling_to_Create_Thin_Features.py | 28 ----- examples/FreeCAD/Ex018_Making_Lofts.py | 29 ----- examples/FreeCAD/Ex019_Counter_Sunk_Holes.py | 30 ------ .../Ex020_Rounding_Corners_with_Fillets.py | 28 ----- examples/FreeCAD/Ex021_Splitting_an_Object.py | 31 ------ examples/FreeCAD/Ex022_Classic_OCC_Bottle.py | 40 ------- .../FreeCAD/Ex023_Parametric_Enclosure.py | 102 ------------------ ...x024_Using_FreeCAD_Solids_as_CQ_Objects.py | 41 ------- examples/FreeCAD/Ex025_Revolution.py | 41 ------- 49 files changed, 638 insertions(+), 935 deletions(-) create mode 100644 examples/Ex001_Simple_Block.py create mode 100644 examples/Ex002_Block_With_Bored_Center_Hole.py create mode 100644 examples/Ex003_Pillow_Block_With_Counterbored_Holes.py create mode 100644 examples/Ex004_Extruded_Cylindrical_Plate.py create mode 100644 examples/Ex005_Extruded_Lines_and_Arcs.py create mode 100644 examples/Ex006_Moving_the_Current_Working_Point.py create mode 100644 examples/Ex007_Using_Point_Lists.py create mode 100644 examples/Ex008_Polygon_Creation.py create mode 100644 examples/Ex009_Polylines.py create mode 100644 examples/Ex010_Defining_an_Edge_with_a_Spline.py create mode 100644 examples/Ex011_Mirroring_Symmetric_Geometry.py create mode 100644 examples/Ex012_Creating_Workplanes_on_Faces.py create mode 100644 examples/Ex013_Locating_a_Workplane_on_a_Vertex.py create mode 100644 examples/Ex014_Offset_Workplanes.py create mode 100644 examples/Ex015_Rotated_Workplanes.py create mode 100644 examples/Ex016_Using_Construction_Geometry.py create mode 100644 examples/Ex017_Shelling_to_Create_Thin_Features.py create mode 100644 examples/Ex018_Making_Lofts.py create mode 100644 examples/Ex019_Counter_Sunk_Holes.py create mode 100644 examples/Ex020_Rounding_Corners_with_Fillets.py create mode 100644 examples/Ex021_Splitting_an_Object.py create mode 100644 examples/Ex022_Revolution.py create mode 100644 examples/Ex023_Sweep.py create mode 100644 examples/Ex024_Sweep_Along_List_Of_Wires.py delete mode 100644 examples/FreeCAD/Ex001_Simple_Block.py delete mode 100644 examples/FreeCAD/Ex002_Block_With_Bored_Center_Hole.py delete mode 100644 examples/FreeCAD/Ex003_Pillow_Block_With_Counterbored_Holes.py delete mode 100644 examples/FreeCAD/Ex004_Extruded_Cylindrical_Plate.py delete mode 100644 examples/FreeCAD/Ex005_Extruded_Lines_and_Arcs.py delete mode 100644 examples/FreeCAD/Ex006_Moving_the_Current_Working_Point.py delete mode 100644 examples/FreeCAD/Ex007_Using_Point_Lists.py delete mode 100644 examples/FreeCAD/Ex008_Polygon_Creation.py delete mode 100644 examples/FreeCAD/Ex009_Polylines.py delete mode 100644 examples/FreeCAD/Ex010_Defining_an_Edge_with_a_Spline.py delete mode 100644 examples/FreeCAD/Ex011_Mirroring_Symmetric_Geometry.py delete mode 100644 examples/FreeCAD/Ex012_Creating_Workplanes_on_Faces.py delete mode 100644 examples/FreeCAD/Ex013_Locating_a_Workplane_on_a_Vertex.py delete mode 100644 examples/FreeCAD/Ex014_Offset_Workplanes.py delete mode 100644 examples/FreeCAD/Ex015_Rotated_Workplanes.py delete mode 100644 examples/FreeCAD/Ex016_Using_Construction_Geometry.py delete mode 100644 examples/FreeCAD/Ex017_Shelling_to_Create_Thin_Features.py delete mode 100644 examples/FreeCAD/Ex018_Making_Lofts.py delete mode 100644 examples/FreeCAD/Ex019_Counter_Sunk_Holes.py delete mode 100644 examples/FreeCAD/Ex020_Rounding_Corners_with_Fillets.py delete mode 100644 examples/FreeCAD/Ex021_Splitting_an_Object.py delete mode 100644 examples/FreeCAD/Ex022_Classic_OCC_Bottle.py delete mode 100644 examples/FreeCAD/Ex023_Parametric_Enclosure.py delete mode 100644 examples/FreeCAD/Ex024_Using_FreeCAD_Solids_as_CQ_Objects.py delete mode 100644 examples/FreeCAD/Ex025_Revolution.py diff --git a/examples/Ex001_Simple_Block.py b/examples/Ex001_Simple_Block.py new file mode 100644 index 00000000..f72445f4 --- /dev/null +++ b/examples/Ex001_Simple_Block.py @@ -0,0 +1,19 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block + +# Create a 3D block based on the dimension variables above. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +result = cq.Workplane("XY").box(length, height, thickness) + +# The following method is now outdated, but can still be used to display the +# results of the script if you want +# from Helpers import show +# show(result) # Render the result of this script + +show_object(result) diff --git a/examples/Ex002_Block_With_Bored_Center_Hole.py b/examples/Ex002_Block_With_Bored_Center_Hole.py new file mode 100644 index 00000000..a825f98e --- /dev/null +++ b/examples/Ex002_Block_With_Bored_Center_Hole.py @@ -0,0 +1,20 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block + +# Create a block based on the dimensions above and add a 22mm center hole. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +# 2. The highest (max) Z face is selected and a new workplane is created on it. +# 3. The new workplane is used to drill a hole through the block. +# 3a. The hole is automatically centered in the workplane. +result = cq.Workplane("XY").box(length, height, thickness) \ + .faces(">Z").workplane().hole(center_hole_dia) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py new file mode 100644 index 00000000..50ad422a --- /dev/null +++ b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py @@ -0,0 +1,34 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block +cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter +cbore_diameter = 4.4 # Bolt head pocket hole diameter +cbore_depth = 2.1 # Bolt head pocket hole depth + +# Create a 3D block based on the dimensions above and add a 22mm center hold +# and 4 counterbored holes for bolts +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +# 2. The highest(max) Z face is selected and a new workplane is created on it. +# 3. The new workplane is used to drill a hole through the block. +# 3a. The hole is automatically centered in the workplane. +# 4. The highest(max) Z face is selected and a new workplane is created on it. +# 5. A for-construction rectangle is created on the workplane based on the +# block's overall dimensions. +# 5a. For-construction objects are used only to place other geometry, they +# do not show up in the final displayed geometry. +# 6. The vertices of the rectangle (corners) are selected, and a counter-bored +# hole is placed at each of the vertices (all 4 of them at once). +result = cq.Workplane("XY").box(length, height, thickness) \ + .faces(">Z").workplane().hole(center_hole_dia) \ + .faces(">Z").workplane() \ + .rect(length - 8.0, height - 8.0, forConstruction=True) \ + .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex004_Extruded_Cylindrical_Plate.py b/examples/Ex004_Extruded_Cylindrical_Plate.py new file mode 100644 index 00000000..6ccb2eca --- /dev/null +++ b/examples/Ex004_Extruded_Cylindrical_Plate.py @@ -0,0 +1,29 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +circle_radius = 50.0 # Radius of the plate +thickness = 13.0 # Thickness of the plate +rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate +rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate + +# Extrude a cylindrical plate with a rectangular hole in the middle of it. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. The 2D geometry for the outer circle is created at the same time as the +# rectangle that will create the hole in the center. +# 2a. The circle and the rectangle will be automatically centered on the +# workplane. +# 2b. Unlike some other functions like the hole(), circle() takes +# a radius and not a diameter. +# 3. The circle and rectangle are extruded together, creating a cylindrical +# plate with a rectangular hole in the center. +# 3a. circle() and rect() could be changed to any other shape to completely +# change the resulting plate and/or the hole in it. +result = cq.Workplane("front").circle(circle_radius) \ + .rect(rectangle_width, rectangle_length) \ + .extrude(thickness) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex005_Extruded_Lines_and_Arcs.py b/examples/Ex005_Extruded_Lines_and_Arcs.py new file mode 100644 index 00000000..fa93a893 --- /dev/null +++ b/examples/Ex005_Extruded_Lines_and_Arcs.py @@ -0,0 +1,45 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +width = 2.0 # Overall width of the plate +thickness = 0.25 # Thickness of the plate + +# Extrude a plate outline made of lines and an arc +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Draws a line from the origin to an X position of the plate's width. +# 2a. The starting point of a 2D drawing like this will be at the center of the +# workplane (0, 0) unless the moveTo() function moves the starting point. +# 3. A line is drawn from the last position straight up in the Y direction +# 1.0 millimeters. +# 4. An arc is drawn from the last point, through point (1.0, 1.5) which is +# half-way back to the origin in the X direction and 0.5 mm above where +# the last line ended at. The arc then ends at (0.0, 1.0), which is 1.0 mm +# above (in the Y direction) where our first line started from. +# 5. An arc is drawn from the last point that ends on (-0.5, 1.0), the sag of +# the curve 0.2 determines that the curve is concave with the midpoint 0.1 mm +# from the arc baseline. If the sag was -0.2 the arc would be convex. +# This convention is valid when the profile is drawn counterclockwise. +# The reverse is true if the profile is drawn clockwise. +# Clockwise: +sag => convex, -sag => concave +# Counterclockwise: +sag => concave, -sag => convex +# 6. An arc is drawn from the last point that ends on (-0.7, -0.2), the arc is +# determined by the radius of -1.5 mm. +# Clockwise: +radius => convex, -radius => concave +# Counterclockwise: +radius => concave, -radius => convex +# 7. close() is called to automatically draw the last line for us and close +# the sketch so that it can be extruded. +# 7a. Without the close(), the 2D sketch will be left open and the extrude +# operation will provide unpredictable results. +# 8. The 2D sketch is extruded into a solid object of the specified thickness. +result = cq.Workplane("front").lineTo(width, 0) \ + .lineTo(width, 1.0) \ + .threePointArc((1.0, 1.5), (0.0, 1.0)) \ + .sagittaArc((-0.5, 1.0), 0.2) \ + .radiusArc((-0.7, -0.2), -1.5) \ + .close().extrude(thickness) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex006_Moving_the_Current_Working_Point.py b/examples/Ex006_Moving_the_Current_Working_Point.py new file mode 100644 index 00000000..3c26121a --- /dev/null +++ b/examples/Ex006_Moving_the_Current_Working_Point.py @@ -0,0 +1,35 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +circle_radius = 3.0 # The outside radius of the plate +thickness = 0.25 # The thickness of the plate + +# Make a plate with two cutouts in it by moving the workplane center point +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 1b. The initial workplane center point is the center of the circle, at (0,0). +# 2. A circle is created at the center of the workplane +# 2a. Notice that circle() takes a radius and not a diameter +result = cq.Workplane("front").circle(circle_radius) + +# 3. The work center is movide to (1.5, 0.0) by calling center(). +# 3a. The new center is specified relative to the previous center,not +# relative to global coordinates. +# 4. A 0.5mm x 0.5mm 2D square is drawn inside the circle. +# 4a. The plate has not been extruded yet, only 2D geometry is being created. +result = result.center(1.5, 0.0).rect(0.5, 0.5) + +# 5. The work center is moved again, this time to (-1.5, 1.5). +# 6. A 2D circle is created at that new center with a radius of 0.25mm. +result = result.center(-1.5, 1.5).circle(0.25) + +# 7. All 2D geometry is extruded to the specified thickness of the plate. +# 7a. The small circle and the square are enclosed in the outer circle of the +# plate and so it is assumed that we want them to be cut out of the plate. +# A separate cut operation is not needed. +result = result.extrude(thickness) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex007_Using_Point_Lists.py b/examples/Ex007_Using_Point_Lists.py new file mode 100644 index 00000000..d824c750 --- /dev/null +++ b/examples/Ex007_Using_Point_Lists.py @@ -0,0 +1,32 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +plate_radius = 2.0 # The radius of the plate that will be extruded +hole_pattern_radius = 0.25 # Radius of circle where the holes will be placed +thickness = 0.125 # The thickness of the plate that will be extruded + +# Make a plate with 4 holes in it at various points in a polar arrangement from +# the center of the workplane. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. A 2D circle is drawn that will become though outer profile of the plate. +r = cq.Workplane("front").circle(plate_radius) + +# 3. Push 4 points on the stack that will be used as the center points of the +# holes. +r = r.pushPoints([(1.5, 0), (0, 1.5), (-1.5, 0), (0, -1.5)]) + +# 4. This circle() call will operate on all four points, putting a circle at +# each one. +r = r.circle(hole_pattern_radius) + +# 5. All 2D geometry is extruded to the specified thickness of the plate. +# 5a. The small hole circles are enclosed in the outer circle of the plate and +# so it is assumed that we want them to be cut out of the plate. A +# separate cut operation is not needed. +result = r.extrude(thickness) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex008_Polygon_Creation.py b/examples/Ex008_Polygon_Creation.py new file mode 100644 index 00000000..2fdecfc5 --- /dev/null +++ b/examples/Ex008_Polygon_Creation.py @@ -0,0 +1,39 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +width = 3.0 # The width of the plate +height = 4.0 # The height of the plate +thickness = 0.25 # The thickness of the plate +polygon_sides = 6 # The number of sides that the polygonal holes should have +polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points + +# Create a plate with two polygons cut through it +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. A 3D box is created in one box() operation to represent the plate. +# 2a. The box is centered around the origin, which creates a result that may +# be unituitive when the polygon cuts are made. +# 3. 2 points are pushed onto the stack and will be used as centers for the +# polygonal holes. +# 4. The two polygons are created, on for each point, with one call to +# polygon() using the number of sides and the circle that bounds the +# polygon. +# 5. The polygons are cut thru all objects that are in the line of extrusion. +# 5a. A face was not selected, and so the polygons are created on the +# workplane. Since the box was centered around the origin, the polygons end +# up being in the center of the box. This makes them cut from the center to +# the outside along the normal (positive direction). +# 6. The polygons are cut through all objects, starting at the center of the +# box/plate and going "downward" (opposite of normal) direction. Functions +# like cutBlind() assume a positive cut direction, but cutThruAll() assumes +# instead that the cut is made from a max direction and cuts downward from +# that max through all objects. +result = cq.Workplane("front").box(width, height, thickness) \ + .pushPoints([(0, 0.75), (0, -0.75)]) \ + .polygon(polygon_sides, polygon_dia) \ + .cutThruAll() + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex009_Polylines.py b/examples/Ex009_Polylines.py new file mode 100644 index 00000000..85a7d6ae --- /dev/null +++ b/examples/Ex009_Polylines.py @@ -0,0 +1,39 @@ +import cadquery as cq + +# These can be modified rather than hardcoding values for each dimension. +# Define up our Length, Height, Width, and thickness of the beam +(L, H, W, t) = (100.0, 20.0, 20.0, 1.0) + +# Define the points that the polyline will be drawn to/thru +pts = [ + (W/2.0, H/2.0), + (W/2.0, (H/2.0 - t)), + (t/2.0, (H/2.0-t)), + (t/2.0, (t - H/2.0)), + (W/2.0, (t - H/2.0)), + (W/2.0, H/-2.0), + (0, H/-2.0) +] + +# We generate half of the I-beam outline and then mirror it to create the full +# I-beam. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. moveTo() is used to move the first point from the origin (0, 0) to +# (0, 10.0), with 10.0 being half the height (H/2.0). If this is not done +# the first line will start from the origin, creating an extra segment that +# will cause the extrude to have an invalid shape. +# 3. The polyline function takes a list of points and generates the lines +# through all the points at once. +# 3. Only half of the I-beam profile has been drawn so far. That half is +# mirrored around the Y-axis to create the complete I-beam profile. +# 4. The I-beam profile is extruded to the final length of the beam. +result = cq.Workplane("front").moveTo(0, H/2.0) \ + .polyline(pts) \ + .mirrorY() \ + .extrude(L) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex010_Defining_an_Edge_with_a_Spline.py b/examples/Ex010_Defining_an_Edge_with_a_Spline.py new file mode 100644 index 00000000..8b4c67cb --- /dev/null +++ b/examples/Ex010_Defining_an_Edge_with_a_Spline.py @@ -0,0 +1,27 @@ +import cadquery as cq + +# 1. Establishes a workplane to create the spline on to extrude. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +s = cq.Workplane("XY") + +# The points that the spline will pass through +sPnts = [ + (2.75, 1.5), + (2.5, 1.75), + (2.0, 1.5), + (1.5, 1.0), + (1.0, 1.25), + (0.5, 1.0), + (0, 1.0) +] + +# 2. Generate our plate with the spline feature and make sure it is a +# closed entity +r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() + +# 3. Extrude to turn the wire into a plate +result = r.extrude(0.5) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex011_Mirroring_Symmetric_Geometry.py b/examples/Ex011_Mirroring_Symmetric_Geometry.py new file mode 100644 index 00000000..2fc10924 --- /dev/null +++ b/examples/Ex011_Mirroring_Symmetric_Geometry.py @@ -0,0 +1,20 @@ +import cadquery as cq + +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. A horizontal line is drawn on the workplane with the hLine function. +# 2a. 1.0 is the distance, not coordinate. hLineTo allows using xCoordinate +# not distance. +r = cq.Workplane("front").hLine(1.0) + +# 3. Draw a series of vertical and horizontal lines with the vLine and hLine +# functions. +r = r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0) + +# 4. Mirror the geometry about the Y axis and extrude it into a 3D object. +result = r.mirrorY().extrude(0.25) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex012_Creating_Workplanes_on_Faces.py b/examples/Ex012_Creating_Workplanes_on_Faces.py new file mode 100644 index 00000000..d73fafe1 --- /dev/null +++ b/examples/Ex012_Creating_Workplanes_on_Faces.py @@ -0,0 +1,16 @@ +import cadquery as cq + +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Creates a 3D box that will have a hole placed in it later. +result = cq.Workplane("front").box(2, 3, 0.5) + +# 3. Find the top-most face with the >Z max selector. +# 3a. Establish a new workplane to build geometry on. +# 3b. Create a hole down into the box. +result = result.faces(">Z").workplane().hole(0.5) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex013_Locating_a_Workplane_on_a_Vertex.py b/examples/Ex013_Locating_a_Workplane_on_a_Vertex.py new file mode 100644 index 00000000..197e5c06 --- /dev/null +++ b/examples/Ex013_Locating_a_Workplane_on_a_Vertex.py @@ -0,0 +1,21 @@ +import cadquery as cq + +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Creates a 3D box that will have a hole placed in it later. +result = cq.Workplane("front").box(3, 2, 0.5) + +# 3. Select the lower left vertex and make a workplane. +# 3a. The top-most Z face is selected using the >Z selector. +# 3b. The lower-left vertex of the faces is selected with the Z").vertices("Z") \ + .workplane() \ + .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) \ + .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex016_Using_Construction_Geometry.py b/examples/Ex016_Using_Construction_Geometry.py new file mode 100644 index 00000000..48a4f870 --- /dev/null +++ b/examples/Ex016_Using_Construction_Geometry.py @@ -0,0 +1,21 @@ +import cadquery as cq + +# Create a block with holes in each corner of a rectangle on that workplane. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects the top-most Z face of the box. +# 4. Creates a new workplane to build new geometry on. +# 5. Creates a for-construction rectangle that only exists to use for placing +# other geometry. +# 6. Selects the vertices of the for-construction rectangle. +# 7. Places holes at the center of each selected vertex. +result = cq.Workplane("front").box(2, 2, 0.5)\ + .faces(">Z").workplane() \ + .rect(1.5, 1.5, forConstruction=True).vertices() \ + .hole(0.125) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex017_Shelling_to_Create_Thin_Features.py b/examples/Ex017_Shelling_to_Create_Thin_Features.py new file mode 100644 index 00000000..91c68234 --- /dev/null +++ b/examples/Ex017_Shelling_to_Create_Thin_Features.py @@ -0,0 +1,14 @@ +import cadquery as cq + +# Create a hollow box that's open on both ends with a thin wall. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects faces with normal in +z direction. +# 4. Create a shell by cutting out the top-most Z face. +result = cq.Workplane("front").box(2, 2, 2).faces("+Z").shell(0.05) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex018_Making_Lofts.py b/examples/Ex018_Making_Lofts.py new file mode 100644 index 00000000..6e9ad1e2 --- /dev/null +++ b/examples/Ex018_Making_Lofts.py @@ -0,0 +1,20 @@ +import cadquery as cq + +# Create a lofted section between a rectangle and a circular section. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the named plane orientation "front" to define the workplane, meaning +# that the positive Z direction is "up", and the negative Z direction +# is "down". +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects the top-most Z face of the box. +# 4. Draws a 2D circle at the center of the the top-most face of the box. +# 5. Creates a workplane 3 mm above the face the circle was drawn on. +# 6. Draws a 2D circle on the new, offset workplane. +# 7. Creates a loft between the circle and the rectangle. +result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ + .circle(1.5).workplane(offset=3.0) \ + .rect(0.75, 0.5) \ + .loft(combine=True) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex019_Counter_Sunk_Holes.py b/examples/Ex019_Counter_Sunk_Holes.py new file mode 100644 index 00000000..e75039a4 --- /dev/null +++ b/examples/Ex019_Counter_Sunk_Holes.py @@ -0,0 +1,19 @@ +import cadquery as cq + +# Create a plate with 4 counter-sunk holes in it. +# 1. Establishes a workplane using an XY object instead of a named plane. +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects the top-most face of the box and established a workplane on that. +# 4. Draws a for-construction rectangle on the workplane which only exists for +# placing other geometry. +# 5. Selects the corner vertices of the rectangle and places a counter-sink +# hole, using each vertex as the center of a hole using the cskHole() +# function. +# 5a. When the depth of the counter-sink hole is set to None, the hole will be +# cut through. +result = cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") \ + .workplane().rect(3.5, 1.5, forConstruction=True) \ + .vertices().cskHole(0.125, 0.25, 82.0, depth=None) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex020_Rounding_Corners_with_Fillets.py b/examples/Ex020_Rounding_Corners_with_Fillets.py new file mode 100644 index 00000000..a7369704 --- /dev/null +++ b/examples/Ex020_Rounding_Corners_with_Fillets.py @@ -0,0 +1,13 @@ +import cadquery as cq + +# Create a plate with 4 rounded corners in the Z-axis. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects all edges that are parallel to the Z axis. +# 4. Creates fillets on each of the selected edges with the specified radius. +result = cq.Workplane("XY").box(3, 3, 0.5).edges("|Z").fillet(0.125) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex021_Splitting_an_Object.py b/examples/Ex021_Splitting_an_Object.py new file mode 100644 index 00000000..e903a13d --- /dev/null +++ b/examples/Ex021_Splitting_an_Object.py @@ -0,0 +1,25 @@ +import cadquery as cq + +# Create a simple block with a hole through it that we can split. +# 1. Establishes a workplane that an object can be built on. +# 1a. Uses the X and Y origins to define the workplane, meaning that the +# positive Z direction is "up", and the negative Z direction is "down". +# 2. Creates a plain box to base future geometry on with the box() function. +# 3. Selects the top-most face of the box and establishes a workplane on it +# that new geometry can be built on. +# 4. Draws a 2D circle on the new workplane and then uses it to cut a hole +# all the way through the box. +c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() \ + .circle(0.25).cutThruAll() + +# 5. Selects the face furthest away from the origin in the +Y axis direction. +# 6. Creates an offset workplane that is set in the center of the object. +# 6a. One possible improvement to this script would be to make the dimensions +# of the box variables, and then divide the Y-axis dimension by 2.0 and +# use that to create the offset workplane. +# 7. Uses the embedded workplane to split the object, keeping only the "top" +# portion. +result = c.faces(">Y").workplane(-0.5).split(keepTop=True) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex022_Revolution.py b/examples/Ex022_Revolution.py new file mode 100644 index 00000000..c5f31070 --- /dev/null +++ b/examples/Ex022_Revolution.py @@ -0,0 +1,21 @@ +import cadquery as cq + +# The dimensions of the model. These can be modified rather than changing the +# shape's code directly. +rectangle_width = 10.0 +rectangle_length = 10.0 +angle_degrees = 360.0 + +# Revolve a cylinder from a rectangle +# Switch comments around in this section to try the revolve operation with different parameters +result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) + +# Revolve a donut with square walls +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) + +# Displays the result of this script +show_object(result) diff --git a/examples/Ex023_Sweep.py b/examples/Ex023_Sweep.py new file mode 100644 index 00000000..c2ba5017 --- /dev/null +++ b/examples/Ex023_Sweep.py @@ -0,0 +1,40 @@ +import cadquery as cq + +# Points we will use to create spline and polyline paths to sweep over +pts = [ + (0, 1), + (1, 2), + (2, 4) +] + +# Spline path generated from our list of points (tuples) +path = cq.Workplane("XZ").spline(pts) + +# Sweep a circle with a diameter of 1.0 units along the spline path we just created +defaultSweep = cq.Workplane("XY").circle(1.0).sweep(path) + +# Sweep defaults to making a solid and not generating a Frenet solid. Setting Frenet to True helps prevent creep in +# the orientation of the profile as it is being swept +frenetShell = cq.Workplane("XY").circle(1.0).sweep(path, makeSolid=True, isFrenet=True) + +# We can sweep shapes other than circles +defaultRect = cq.Workplane("XY").rect(1.0, 1.0).sweep(path) + +# Switch to a polyline path, but have it use the same points as the spline +path = cq.Workplane("XZ").polyline(pts) + +# Using a polyline path leads to the resulting solid having segments rather than a single swept outer face +plineSweep = cq.Workplane("XY").circle(1.0).sweep(path) + +# Switch to an arc for the path +path = cq.Workplane("XZ").threePointArc((1.0, 1.5), (0.0, 1.0)) + +# Use a smaller circle section so that the resulting solid looks a little nicer +arcSweep = cq.Workplane("XY").circle(0.5).sweep(path) + +# Translate the resulting solids so that they do not overlap and display them left to right +show_object(defaultSweep) +show_object(frenetShell.translate((5, 0, 0))) +show_object(defaultRect.translate((10, 0, 0))) +show_object(plineSweep.translate((15, 0, 0))) +show_object(arcSweep.translate((20, 0, 0))) \ No newline at end of file diff --git a/examples/Ex024_Sweep_Along_List_Of_Wires.py b/examples/Ex024_Sweep_Along_List_Of_Wires.py new file mode 100644 index 00000000..6ead2c84 --- /dev/null +++ b/examples/Ex024_Sweep_Along_List_Of_Wires.py @@ -0,0 +1,47 @@ +import cadquery as cq + +# X axis line length 20.0 +path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) + +# Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 +defaultSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ + workplane(offset=10.0).circle(1.0). \ + workplane(offset=10.0).circle(2.0).sweep(path, sweepAlongWires=True) + +# We can sweep thrue different shapes +recttocircleSweep = cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ + workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ + workplane(offset=8.0).rect(2.0, 2.0).sweep(path, sweepAlongWires=True) + +circletorectSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ + workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ + workplane(offset=7.0).circle(1.0).sweep(path, sweepAlongWires=True) + + +# Placement of the Shape is important otherwise could produce unexpected shape +specialSweep = cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ + sweep(path, sweepAlongWires=True) + +# Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 +path = cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ + threePointArc((4, 0), (0, -4)).lineTo(-5, -4) + +# Placement of different shapes should follow the path +# cylinder r=1.5 along first line +# then sweep allong arc from r=1.5 to r=1.0 +# then cylinder r=1.0 along last line +arcSweep = cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ + workplane(offset=5).circle(1.5). \ + moveTo(0, -8).circle(1.0). \ + workplane(offset=-5).circle(1.0). \ + sweep(path, sweepAlongWires=True) + + +# Translate the resulting solids so that they do not overlap and display them left to right +show_object(defaultSweep) +show_object(circletorectSweep.translate((0, 5, 0))) +show_object(recttocircleSweep.translate((0, 10, 0))) +show_object(specialSweep.translate((0, 15, 0))) +show_object(arcSweep.translate((0, -5, 0))) + + diff --git a/examples/FreeCAD/Ex001_Simple_Block.py b/examples/FreeCAD/Ex001_Simple_Block.py deleted file mode 100644 index 8e1609c1..00000000 --- a/examples/FreeCAD/Ex001_Simple_Block.py +++ /dev/null @@ -1,32 +0,0 @@ -#File: Ex001_Simple_Block.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex001_Simple_Block - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex001_Simple_Block) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more in-depth explanation of this example at http://parametricparts.com/docs/quickstart.html - -import cadquery -import Part - -#The dimensions of the box. These can be modified rather than changing the box's code directly. -length = 80.0 -height = 60.0 -thickness = 10.0 - -#Create a 3D box based on the dimension variables above -result = cadquery.Workplane("XY").box(length, height, thickness) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex002_Block_With_Bored_Center_Hole.py b/examples/FreeCAD/Ex002_Block_With_Bored_Center_Hole.py deleted file mode 100644 index ea405f5c..00000000 --- a/examples/FreeCAD/Ex002_Block_With_Bored_Center_Hole.py +++ /dev/null @@ -1,33 +0,0 @@ -#File: Ex002_Block_With_Bored_Center_Hole.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex002_Block_With_Bored_Center_Hole - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex002_Block_With_Bored_Center_Hole) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more in-depth explantion of this example at http://parametricparts.com/docs/quickstart.html - -import cadquery -import Part - -#The dimensions of the box. These can be modified rather than changing the box's code directly. -length = 80.0 -height = 60.0 -thickness = 10.0 -center_hole_dia = 22.0 - -#Create a 3D box based on the dimension variables above and add a 22mm center hole -result = cadquery.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex003_Pillow_Block_With_Counterbored_Holes.py b/examples/FreeCAD/Ex003_Pillow_Block_With_Counterbored_Holes.py deleted file mode 100644 index 382f03e8..00000000 --- a/examples/FreeCAD/Ex003_Pillow_Block_With_Counterbored_Holes.py +++ /dev/null @@ -1,40 +0,0 @@ -#File: Ex003_Pillow_Block_With_Counterbored_Holes.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex003_Pillow_Block_With_Counterbored_Holes - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex003_Pillow_Block_With_Counterbored_Holes) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more in-depth explanation of this example at http://parametricparts.com/docs/quickstart.html - -import cadquery -import Part - -#The dimensions of the box. These can be modified rather than changing the box's code directly. -length = 80.0 -height = 60.0 -thickness = 10.0 -center_hole_dia = 22.0 -cbore_hole_diameter = 2.4 -cbore_diameter = 4.4 -cbore_depth = 2.1 - -#Create a 3D box based on the dimension variables above and add 4 counterbored holes -result = cadquery.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) \ - .faces(">Z").workplane() \ - .rect(length - 8.0, height - 8.0, forConstruction = True) \ - .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex004_Extruded_Cylindrical_Plate.py b/examples/FreeCAD/Ex004_Extruded_Cylindrical_Plate.py deleted file mode 100644 index 8b631cec..00000000 --- a/examples/FreeCAD/Ex004_Extruded_Cylindrical_Plate.py +++ /dev/null @@ -1,34 +0,0 @@ -#File: Ex004_Extruded_Cylindrical_Plate.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex004_Extruded_Cylindrical_Plate - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex004_Extruded_Cylindrical_Plate) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the box's code directly. -circle_radius = 50.0 -rectangle_width = 13.0 -rectangle_length = 19.0 -thickness = 13.0 - -#Extrude a cylindrical plate with a rectangular hole in the middle of it -result = cadquery.Workplane("front").circle(circle_radius).rect(rectangle_width, rectangle_length).extrude(thickness) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex005_Extruded_Lines_and_Arcs.py b/examples/FreeCAD/Ex005_Extruded_Lines_and_Arcs.py deleted file mode 100644 index 99944343..00000000 --- a/examples/FreeCAD/Ex005_Extruded_Lines_and_Arcs.py +++ /dev/null @@ -1,33 +0,0 @@ -#File: Ex005_Extruded_Lines_and_Arcs.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex005_Extruded_Lines_and_Arcs - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex005_Extruded_Lines_and_Arcs) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -#(Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the box's code directly. -width = 2.0 -thickness = 0.25 - -#Extrude a plate outline made of lines and an arc -result = cadquery.Workplane("front").lineTo(width, 0).lineTo(width, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0)) \ - .close().extrude(thickness) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex006_Moving_the_Current_Working_Point.py b/examples/FreeCAD/Ex006_Moving_the_Current_Working_Point.py deleted file mode 100644 index 892396c0..00000000 --- a/examples/FreeCAD/Ex006_Moving_the_Current_Working_Point.py +++ /dev/null @@ -1,38 +0,0 @@ -#File: Ex006_Moving_the_Current_Working_Point.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex006_Moving_the_Current_Working_Point - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex006_Moving_the_Current_Working_Point) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the box's code directly. -circle_radius = 3.0 -thickness = 0.25 - -#Make the plate with two cutouts in it -result = cadquery.Workplane("front").circle(circle_radius) # Current point is the center of the circle, at (0,0) -result = result.center(1.5,0.0).rect(0.5,0.5) # New work center is (1.5,0.0) - -result = result.center(-1.5,1.5).circle(0.25) # New work center is ( 0.0,1.5). -#The new center is specified relative to the previous center, not global coordinates! - -result = result.extrude(thickness) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex007_Using_Point_Lists.py b/examples/FreeCAD/Ex007_Using_Point_Lists.py deleted file mode 100644 index 2609d849..00000000 --- a/examples/FreeCAD/Ex007_Using_Point_Lists.py +++ /dev/null @@ -1,36 +0,0 @@ -#File: Ex007_Using_Point_Lists.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex007_Using_Point_Lists - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex007_Using_Point_Lists) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the box's code directly. -plate_radius = 2.0 -hole_pattern_radius = 0.25 -thickness = 0.125 - -#Make the plate with 4 holes in it at various points -r = cadquery.Workplane("front").circle(plate_radius) # Make the base -r = r.pushPoints([(1.5, 0), (0, 1.5), (-1.5, 0), (0, -1.5)]) # Now four points are on the stack -r = r.circle(hole_pattern_radius) # Circle will operate on all four points -result = r.extrude(thickness) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex008_Polygon_Creation.py b/examples/FreeCAD/Ex008_Polygon_Creation.py deleted file mode 100644 index 6eefe13b..00000000 --- a/examples/FreeCAD/Ex008_Polygon_Creation.py +++ /dev/null @@ -1,36 +0,0 @@ -#File: Ex008_Polygon_Creation.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex008_Polygon_Creation - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex008_Polygon_Creation) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the box's code directly. -width = 3.0 -height = 4.0 -thickness = 0.25 -polygon_sides = 6 -polygon_dia = 1.0 - -#Create a plate with two polygons cut through it -result = cadquery.Workplane("front").box(width, height, thickness).pushPoints([(0, 0.75), (0, -0.75)]) \ - .polygon(polygon_sides, polygon_dia).cutThruAll() - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex009_Polylines.py b/examples/FreeCAD/Ex009_Polylines.py deleted file mode 100644 index 73f92591..00000000 --- a/examples/FreeCAD/Ex009_Polylines.py +++ /dev/null @@ -1,44 +0,0 @@ -#File: Ex009_Polylines.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex009_Polylines - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex009_Polylines) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Set up our Length, Height, Width, and thickness that will be used to define the locations that the polyline -#is drawn to/thru -(L, H, W, t) = (100.0, 20.0, 20.0, 1.0) - -#Define the locations that the polyline will be drawn to/thru -pts = [ - (0, H/2.0), - (W/2.0, H/2.0), - (W/2.0, (H/2.0 - t)), - (t/2.0, (H/2.0-t)), - (t/2.0, (t - H/2.0)), - (W/2.0, (t - H/2.0)), - (W/2.0, H/-2.0), - (0, H/-2.0) -] - -#We generate half of the I-beam outline and then mirror it to create the full I-beam -result = cadquery.Workplane("front").polyline(pts).mirrorY().extrude(L) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex010_Defining_an_Edge_with_a_Spline.py b/examples/FreeCAD/Ex010_Defining_an_Edge_with_a_Spline.py deleted file mode 100644 index 7a9534aa..00000000 --- a/examples/FreeCAD/Ex010_Defining_an_Edge_with_a_Spline.py +++ /dev/null @@ -1,45 +0,0 @@ -#File: Ex010_Defining_an_Edge_with_a_Spline.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex010_Defining_an_Edge_with_a_Spline - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex010_Defining_an_Edge_with_a_Spline) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The workplane we want to create the spline on to extrude -s = cadquery.Workplane("XY") - -#The points that the spline will pass through -sPnts = [ - (2.75, 1.5), - (2.5, 1.75), - (2.0, 1.5), - (1.5, 1.0), - (1.0, 1.25), - (0.5, 1.0), - (0, 1.0) -] - -#Generate our plate with the spline feature and make sure it's a closed entity -r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() - -#Extrude to turn the wire into a plate -result = r.extrude(0.5) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) \ No newline at end of file diff --git a/examples/FreeCAD/Ex011_Mirroring_Symmetric_Geometry.py b/examples/FreeCAD/Ex011_Mirroring_Symmetric_Geometry.py deleted file mode 100644 index 54a02b65..00000000 --- a/examples/FreeCAD/Ex011_Mirroring_Symmetric_Geometry.py +++ /dev/null @@ -1,34 +0,0 @@ -#File: Ex011_Mirroring_Symmetric_Geometry.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex011_Mirroring_Symmetric_Geometry - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex011_Mirroring_Symmetric_Geometry) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#1.0 is the distance, not coordinate -r = cadquery.Workplane("front").hLine(1.0) - -#hLineTo allows using xCoordinate not distance -r = r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0) - -#Mirror the geometry and extrude -result = r.mirrorY().extrude(0.25) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex012_Creating_Workplanes_on_Faces.py b/examples/FreeCAD/Ex012_Creating_Workplanes_on_Faces.py deleted file mode 100644 index 50a8ba34..00000000 --- a/examples/FreeCAD/Ex012_Creating_Workplanes_on_Faces.py +++ /dev/null @@ -1,31 +0,0 @@ -#File: Ex012_Creating_Workplanes_on_Faces.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex012_Creating_Workplanes_on_Faces - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex012_Creating_Workplanes_on_Faces) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Make a basic prism -result = cadquery.Workplane("front").box(2,3,0.5) - -#Find the top-most face and make a hole -result = result.faces(">Z").workplane().hole(0.5) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex013_Locating_a_Workplane_on_a_Vertex.py b/examples/FreeCAD/Ex013_Locating_a_Workplane_on_a_Vertex.py deleted file mode 100644 index a50f74f8..00000000 --- a/examples/FreeCAD/Ex013_Locating_a_Workplane_on_a_Vertex.py +++ /dev/null @@ -1,34 +0,0 @@ -#File: Ex013_Locating_a_Workplane_on_a_Vertex.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex013_Locating_a_Workplane_on_a_Vertex - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex013_Locating_a_Workplane_on_a_Vertex) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Make a basic prism -result = cadquery.Workplane("front").box(3, 2, 0.5) - -#Select the lower left vertex and make a workplane -result = result.faces(">Z").vertices("Z").workplane() \ - .transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0)) \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex016_Using_Construction_Geometry.py b/examples/FreeCAD/Ex016_Using_Construction_Geometry.py deleted file mode 100644 index 69ba013f..00000000 --- a/examples/FreeCAD/Ex016_Using_Construction_Geometry.py +++ /dev/null @@ -1,29 +0,0 @@ -#File: Ex016_Using_Construction_Geometry.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex016_Using_Construction_Geometry - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex016_Using_Construction_Geometry) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a block with holes in each corner of a rectangle on that workplane -result = cadquery.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex017_Shelling_to_Create_Thin_Features.py b/examples/FreeCAD/Ex017_Shelling_to_Create_Thin_Features.py deleted file mode 100644 index 7965c442..00000000 --- a/examples/FreeCAD/Ex017_Shelling_to_Create_Thin_Features.py +++ /dev/null @@ -1,28 +0,0 @@ -#File: Ex017_Shelling_to_Create_Thin_Features.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex017_Shelling_to_Create_Thin_Features - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex017_Shelling_to_Create_Thin_Features) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a hollow box that's open on both ends with a thin wall -result = cadquery.Workplane("front").box(2, 2, 2).faces("+Z").shell(0.05) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex018_Making_Lofts.py b/examples/FreeCAD/Ex018_Making_Lofts.py deleted file mode 100644 index 847285ad..00000000 --- a/examples/FreeCAD/Ex018_Making_Lofts.py +++ /dev/null @@ -1,29 +0,0 @@ -#File: Ex018_Making_Lofts.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex018_Making_Lofts - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex018_Making_Lofts) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a lofted section between a rectangle and a circular section -result = cadquery.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) \ - .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex019_Counter_Sunk_Holes.py b/examples/FreeCAD/Ex019_Counter_Sunk_Holes.py deleted file mode 100644 index 4a2590d1..00000000 --- a/examples/FreeCAD/Ex019_Counter_Sunk_Holes.py +++ /dev/null @@ -1,30 +0,0 @@ -#File: Ex019_Counter_Sunk_Holes.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex019_Counter_Sunk_Holes - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex019_Counter_Sunk_Holes) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a plate with 4 counter-sunk holes in it -result = cadquery.Workplane(cadquery.Plane.XY()).box(4, 2, 0.5).faces(">Z").workplane() \ - .rect(3.5, 1.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82.0, depth=None) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex020_Rounding_Corners_with_Fillets.py b/examples/FreeCAD/Ex020_Rounding_Corners_with_Fillets.py deleted file mode 100644 index 2d71322a..00000000 --- a/examples/FreeCAD/Ex020_Rounding_Corners_with_Fillets.py +++ /dev/null @@ -1,28 +0,0 @@ -#File: Ex020_Rounding_Corners_with_Fillets.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex020_Rounding_Corners_with_Fillets - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex020_Rounding_Corners_with_Fillets) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a plate with 4 rounded corners in the Z-axis -result = cadquery.Workplane("XY").box(3, 3, 0.5).edges("|Z").fillet(0.125) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex021_Splitting_an_Object.py b/examples/FreeCAD/Ex021_Splitting_an_Object.py deleted file mode 100644 index 133104a3..00000000 --- a/examples/FreeCAD/Ex021_Splitting_an_Object.py +++ /dev/null @@ -1,31 +0,0 @@ -#File: Ex021_Splitting_an_Object.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex021_Splitting_an_Object - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex021_Splitting_an_Object) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Create a simple block with a hole through it that we can split -c = cadquery.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll() - -#Cut the block in half sideways -result = c.faces(">Y").workplane(-0.5).split(keepTop=True) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex022_Classic_OCC_Bottle.py b/examples/FreeCAD/Ex022_Classic_OCC_Bottle.py deleted file mode 100644 index 8ea52c54..00000000 --- a/examples/FreeCAD/Ex022_Classic_OCC_Bottle.py +++ /dev/null @@ -1,40 +0,0 @@ -#File: Ex022_Classic_OCC_Bottle.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex022_Classic_OCC_Bottle - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex022_Classic_OCC_Bottle) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Set up the length, width, and thickness -(L,w,t) = (20.0, 6.0, 3.0) -s = cadquery.Workplane("XY") - -#Draw half the profile of the bottle and extrude it -p = s.center(-L / 2.0, 0).vLine(w / 2.0) \ - .threePointArc((L / 2.0, w / 2.0 + t),(L, w / 2.0)).vLine(-w / 2.0) \ - .mirrorX().extrude(30.0, True) - -#Make the neck -p.faces(">Z").workplane().circle(3.0).extrude(2.0, True) - -#Make a shell -result = p.faces(">Z").shell(0.3) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex023_Parametric_Enclosure.py b/examples/FreeCAD/Ex023_Parametric_Enclosure.py deleted file mode 100644 index ef3308fd..00000000 --- a/examples/FreeCAD/Ex023_Parametric_Enclosure.py +++ /dev/null @@ -1,102 +0,0 @@ -#File: Ex023_Parametric_Enclosure.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex023_Parametric_Enclosure - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex023_Parametric_Enclosure) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#Parameter definitions -p_outerWidth = 100.0 # Outer width of box enclosure -p_outerLength = 150.0 # Outer length of box enclosure -p_outerHeight = 50.0 # Outer height of box enclosure - -p_thickness = 3.0 # Thickness of the box walls -p_sideRadius = 10.0 # Radius for the curves around the sides of the bo -p_topAndBottomRadius = 2.0 # Radius for the curves on the top and bottom edges of the box - -p_screwpostInset = 12.0 # How far in from the edges the screwposts should be placed -p_screwpostID = 4.0 # Inner diameter of the screwpost holes, should be roughly screw diameter not including threads -p_screwpostOD = 10.0 # Outer diameter of the screwposts. Determines overall thickness of the posts - -p_boreDiameter = 8.0 # Diameter of the counterbore hole, if any -p_boreDepth = 1.0 # Depth of the counterbore hole, if -p_countersinkDiameter = 0.0 # Outer diameter of countersink. Should roughly match the outer diameter of the screw head -p_countersinkAngle = 90.0 # Countersink angle (complete angle between opposite sides, not from center to one side) -p_flipLid = True # Whether to place the lid with the top facing down or not. -p_lipHeight = 1.0 # Height of lip on the underside of the lid. Sits inside the box body for a snug fit. - -#Outer shell -oshell = cadquery.Workplane("XY").rect(p_outerWidth, p_outerLength).extrude(p_outerHeight + p_lipHeight) - -#Weird geometry happens if we make the fillets in the wrong order -if p_sideRadius > p_topAndBottomRadius: - oshell.edges("|Z").fillet(p_sideRadius) - oshell.edges("#Z").fillet(p_topAndBottomRadius) -else: - oshell.edges("#Z").fillet(p_topAndBottomRadius) - oshell.edges("|Z").fillet(p_sideRadius) - -#Inner shell -ishell = oshell.faces("Z").workplane(-p_thickness)\ - .rect(POSTWIDTH, POSTLENGTH, forConstruction=True)\ - .vertices() - -for v in postCenters.all(): - v.circle(p_screwpostOD / 2.0).circle(p_screwpostID / 2.0)\ - .extrude((-1.0) * ((p_outerHeight + p_lipHeight) - (2.0 * p_thickness)), True) - -#Split lid into top and bottom parts -(lid, bottom) = box.faces(">Z").workplane(-p_thickness - p_lipHeight).split(keepTop=True, keepBottom=True).all() - -#Translate the lid, and subtract the bottom from it to produce the lid inset -lowerLid = lid.translate((0, 0, -p_lipHeight)) -cutlip = lowerLid.cut(bottom).translate((p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight)) - -#Compute centers for counterbore/countersink or counterbore -topOfLidCenters = cutlip.faces(">Z").workplane().rect(POSTWIDTH, POSTLENGTH, forConstruction=True).vertices() - -#Add holes of the desired type -if p_boreDiameter > 0 and p_boreDepth > 0: - topOfLid = topOfLidCenters.cboreHole(p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness) -elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: - topOfLid = topOfLidCenters.cskHole(p_screwpostID, p_countersinkDiameter, p_countersinkAngle, (2.0) * p_thickness) -else: - topOfLid= topOfLidCenters.hole(p_screwpostID, 2.0 * p_thickness) - -#Flip lid upside down if desired -if p_flipLid: - topOfLid.rotateAboutCenter((1, 0, 0), 180) - -#Return the combined result -result = topOfLid.combineSolids(bottom) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) diff --git a/examples/FreeCAD/Ex024_Using_FreeCAD_Solids_as_CQ_Objects.py b/examples/FreeCAD/Ex024_Using_FreeCAD_Solids_as_CQ_Objects.py deleted file mode 100644 index 7a8088fd..00000000 --- a/examples/FreeCAD/Ex024_Using_FreeCAD_Solids_as_CQ_Objects.py +++ /dev/null @@ -1,41 +0,0 @@ -#File: Ex024_Using_FreeCAD_Solids_as_CQ_Objects.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex024_Using_FreeCAD_Solids_as_CQ_Objects - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex024_Using_FreeCAD_Solids_as_CQ_Objects) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery, FreeCAD, Part - -#Create a new document that we can draw our model on -newDoc = FreeCAD.newDocument() - -#shows a 1x1x1 FreeCAD cube in the display -initialBox = newDoc.addObject("Part::Box","initialBox") -newDoc.recompute() - -#Make a CQ object -cqBox = cadquery.CQ(cadquery.Solid(initialBox.Shape)) - -#Extrude a peg -newThing = cqBox.faces(">Z").workplane().circle(0.5).extrude(0.25) - -#Add a FreeCAD object to the tree and then store a CQ object in it -nextShape = newDoc.addObject("Part::Feature", "nextShape") -nextShape.Shape = newThing.val().wrapped - -#Rerender the doc to see what the new solid looks like -newDoc.recompute() diff --git a/examples/FreeCAD/Ex025_Revolution.py b/examples/FreeCAD/Ex025_Revolution.py deleted file mode 100644 index e0f93648..00000000 --- a/examples/FreeCAD/Ex025_Revolution.py +++ /dev/null @@ -1,41 +0,0 @@ -#File: Ex025_Revolution.py -#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" -#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad - -#You run this example by typing the following in the FreeCAD python console, making sure to change -#the path to this example, and the name of the example appropriately. -#import sys -#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') -#import Ex025_Revolution - -#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. -#reload(Ex025_Revolution) - -#You'll need to delete the original shape that was created, and the new shape should be named sequentially -# (Shape001, etc). - -#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. -#You can get a more information on this example at -# http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid - -import cadquery -import Part - -#The dimensions of the model. These can be modified rather than changing the shape's code directly. -rectangle_width = 10.0 -rectangle_length = 10.0 -angle_degrees = 360.0 - -#Revolve a cylinder from a rectangle -#Switch comments around in this section to try the revolve operation with different parameters -result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) - -#Revolve a donut with square walls -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) - -#Boiler plate code to render our solid in FreeCAD's GUI -Part.show(result.toFreecad()) From 5d674514776b9e513f54df63694bc9d1278818b9 Mon Sep 17 00:00:00 2001 From: Jeremy Mack Wright Date: Sat, 29 Dec 2018 21:03:08 -0500 Subject: [PATCH 18/28] Added the lego example back into this core repo. --- examples/Ex100_Lego_Brick.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/Ex100_Lego_Brick.py diff --git a/examples/Ex100_Lego_Brick.py b/examples/Ex100_Lego_Brick.py new file mode 100644 index 00000000..6a6381f7 --- /dev/null +++ b/examples/Ex100_Lego_Brick.py @@ -0,0 +1,56 @@ +# This script can create any regular rectangular Lego(TM) Brick +import cadquery as cq + +##### +# Inputs +###### +lbumps = 1 # number of bumps long +wbumps = 1 # number of bumps wide +thin = True # True for thin, False for thick + +# +# Lego Brick Constants-- these make a lego brick a lego :) +# +pitch = 8.0 +clearance = 0.1 +bumpDiam = 4.8 +bumpHeight = 1.8 +if thin: + height = 3.2 +else: + height = 9.6 + +t = (pitch - (2 * clearance) - bumpDiam) / 2.0 +postDiam = pitch - t # works out to 6.5 +total_length = lbumps*pitch - 2.0*clearance +total_width = wbumps*pitch - 2.0*clearance + +# make the base +s = cq.Workplane("XY").box(total_length, total_width, height) + +# shell inwards not outwards +s = s.faces("Z").workplane(). \ + rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ + .extrude(bumpHeight) + +# add posts on the bottom. posts are different diameter depending on geometry +# solid studs for 1 bump, tubes for multiple, none for 1x1 +tmp = s.faces(" 1 and wbumps > 1: + tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ + circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) +elif lbumps > 1: + tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ + circle(t).extrude(height - t) +elif wbumps > 1: + tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ + circle(t).extrude(height - t) +else: + tmp = s + +# Render the solid +show_object(tmp) From 8786d93bcb7381fb3f3a365d0acfd213564b4484 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Thu, 3 Jan 2019 23:37:58 +1100 Subject: [PATCH 19/28] Plane equality + tests --- cadquery/occ_impl/geom.py | 21 +++++++++++++++++++ tests/TestCadObjects.py | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 9592f103..534a9bfa 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -309,6 +309,10 @@ class Plane(object): created automatically from faces. """ + # equality tolerances + _eq_tolerance_origin = 1e-6 + _eq_tolerance_dot = 1e-6 + @classmethod def named(cls, stdName, origin=(0, 0, 0)): """Create a predefined Plane based on the conventional names. @@ -458,6 +462,23 @@ class Plane(object): self._setPlaneDir(xDir) self.origin = origin + def _eq_iter(self, other): + """Iterator to successively test equality""" + cls = type(self) + yield isinstance(other, Plane) # comparison is with another Plane + # origins are the same + yield abs(self.origin - other.origin) < cls._eq_tolerance_origin + # z-axis vectors are parallel (assumption: both are unit vectors) + yield abs(self.zDir.dot(other.zDir) - 1) < cls._eq_tolerance_dot + # x-axis vectors are parallel (assumption: both are unit vectors) + yield abs(self.xDir.dot(other.xDir) - 1) < cls._eq_tolerance_dot + + def __eq__(self, other): + return all(self._eq_iter(other)) + + def __ne__(self, other): + return not self.__eq__(other) + @property def origin(self): return self._origin diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py index aa5c4ad4..0b98d5e5 100644 --- a/tests/TestCadObjects.py +++ b/tests/TestCadObjects.py @@ -205,6 +205,50 @@ class TestCadObjects(BaseTest): gp_Pnt(1, 1, 0)).Edge()) self.assertEqual(2, len(e.Vertices())) + def testPlaneEqual(self): + # default orientation + self.assertEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)) + ) + # moved origin + self.assertEqual( + Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)), + Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)) + ) + # moved x-axis + self.assertEqual( + Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)), + Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) + ) + # moved z-axis + self.assertEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)), + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) + ) + + def testPlaneNotEqual(self): + # type difference + for value in [None, 0, 1, 'abc']: + self.assertNotEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), + value + ) + # origin difference + self.assertNotEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), + Plane(origin=(0,0,1), xDir=(1,0,0), normal=(0,0,1)) + ) + # x-axis difference + self.assertNotEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), + Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) + ) + # z-axis difference + self.assertNotEqual( + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), + Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) + ) if __name__ == '__main__': unittest.main() From 27539f081bd6da80896a8787d9deb967ea484099 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Fri, 4 Jan 2019 00:29:48 +1100 Subject: [PATCH 20/28] Matrix validation robuustness + tests --- cadquery/occ_impl/geom.py | 37 +++++++++++++++++++------------------ tests/TestCadObjects.py | 21 ++++++++++++++++++--- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 9592f103..470daae7 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -215,20 +215,21 @@ class Matrix: self.wrapped = gp_Trsf() elif isinstance(matrix, gp_Trsf): self.wrapped = matrix - elif isinstance(matrix, list): + elif isinstance(matrix, (list, tuple)): + # Validate matrix size & 4x4 last row value + valid_sizes = all( + (isinstance(row, (list, tuple)) and (len(row) == 4)) + for row in matrix + ) and len(matrix) in (3, 4) + if not valid_sizes: + raise TypeError("Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(matrix)) + elif (len(matrix) == 4) and (tuple(matrix[3]) != (0,0,0,1)): + raise ValueError("Expected the last row to be [0,0,0,1], but got: {!r}".format(matrix[3])) + + # Assign values to matrix self.wrapped = gp_Trsf() - if len(matrix) == 3: - flattened = [e for row in matrix for e in row] - self.wrapped.SetValues(*flattened) - elif len(matrix) == 4: - # Only use first 3 rows - the last must be [0, 0, 0, 1]. - lastRow = matrix[3] - if lastRow != [0., 0., 0., 1.]: - raise ValueError("Expected the last row to be [0,0,0,1], but got: {}".format(lastRow)) - flattened = [e for row in matrix[0:3] for e in row] - self.wrapped.SetValues(*flattened) - else: - raise TypeError("Matrix constructor requires list of length 12 or 16") + flattened = [e for row in matrix[:3] for e in row] + self.wrapped.SetValues(*flattened) else: raise TypeError( "Invalid param to matrix constructor: {}".format(matrix)) @@ -282,18 +283,18 @@ class Matrix: and column parameters start at zero, which is consistent with most python libraries, but is counter to gp_Trsf(), which is 1-indexed. """ - if len(rc) != 2: + if not isinstance(rc, tuple) or (len(rc) != 2): raise IndexError("Matrix subscript must provide (row, column)") - r, c = rc[0], rc[1] - if r >= 0 and r < 4 and c >= 0 and c < 4: + (r, c) = rc + if (0 <= r <= 3) and (0 <= c <= 3): if r < 3: - return self.wrapped.Value(r+1,c+1) + return self.wrapped.Value(r + 1, c + 1) else: # gp_Trsf doesn't provide access to the 4th row because it has # an implied value as below: return [0., 0., 0., 1.][c] else: - raise IndexError("Out of bounds access into 4x4 matrix: {}".format(rc)) + raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc)) class Plane(object): diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py index aa5c4ad4..3c112790 100644 --- a/tests/TestCadObjects.py +++ b/tests/TestCadObjects.py @@ -166,14 +166,19 @@ class TestCadObjects(BaseTest): [0., 1., 0., 2.], [0., 0., 1., 3.], [0., 0., 0., 1.]] + vals4x4_tuple = tuple(tuple(r) for r in vals4x4) # test constructor with 16-value input m = Matrix(vals4x4) self.assertEqual(vals4x4, matrix_vals(m)) + m = Matrix(vals4x4_tuple) + self.assertEqual(vals4x4, matrix_vals(m)) # test constructor with 12-value input (the last 4 are an implied # [0,0,0,1]) - m = Matrix(vals4x4[0:12]) + m = Matrix(vals4x4[:3]) + self.assertEqual(vals4x4, matrix_vals(m)) + m = Matrix(vals4x4_tuple[:3]) self.assertEqual(vals4x4, matrix_vals(m)) # Test 16-value input with invalid values for the last 4 @@ -184,14 +189,24 @@ class TestCadObjects(BaseTest): with self.assertRaises(ValueError): Matrix(invalid) - # Test input with invalid size + # Test input with invalid size / nested types + with self.assertRaises(TypeError): + Matrix([[1, 2, 3, 4], [1, 2, 3], [1, 2, 3, 4]]) with self.assertRaises(TypeError): Matrix([1,2,3]) + # Invalid sub-type + with self.assertRaises(TypeError): + Matrix([[1, 2, 3, 4], 'abc', [1, 2, 3, 4]]) + # test out-of-bounds access m = Matrix() with self.assertRaises(IndexError): - m[5, 5] + m[0, 4] + with self.assertRaises(IndexError): + m[4, 0] + with self.assertRaises(IndexError): + m['ab'] def testTranslate(self): From 1a2b1ee706d17b4b00878494bda88ff3af927524 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Fri, 4 Jan 2019 00:53:24 +1100 Subject: [PATCH 21/28] moved tol default from method signature for increased flexibility --- cadquery/occ_impl/geom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 9592f103..01d34292 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -790,10 +790,11 @@ class BoundBox(object): return None @classmethod - def _fromTopoDS(cls, shape, tol=TOL, optimal=False): + def _fromTopoDS(cls, shape, tol=None, optimal=False): ''' Constructs a bounding box from a TopoDS_Shape ''' + tol = TOL if tol is None else tol # tol = TOL (by default) bbox = Bnd_Box() bbox.SetGap(tol) if optimal: From b4e4c4dbeb6ceccc4df67967c4e535899844b085 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Fri, 4 Jan 2019 22:33:45 +1100 Subject: [PATCH 22/28] uuid1 to uuid4 (for python 2.x osx compatability) --- cadquery/occ_impl/jupyter_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index d7ef310b..a0dd21e5 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -47,7 +47,7 @@ FOV = 0.2 def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, rot='{} {} {} {} '.format(*ROT)): return BOILERPLATE.format(src=src, - id=uuid1(), + id=uuid4(), height=height, x=d[0], y=d[1], From 4fc485f18cde975f0017ad7ddb7aa84f991d2b77 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sat, 5 Jan 2019 13:30:47 +1100 Subject: [PATCH 23/28] response to TypeError exception: TypeError: to_x3dfile_string() missing 1 required positional argument: 'shape_id' --- cadquery/occ_impl/jupyter_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index a0dd21e5..80c653b4 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -83,7 +83,7 @@ def x3d_display(shape, mesh_quality) exporter.compute() - x3d_str = exporter.to_x3dfile_string() + x3d_str = exporter.to_x3dfile_string(shape_id=0) x3d_str = '\n'.join(x3d_str.splitlines()[N_HEADER_LINES:]) bb = BoundBox._fromTopoDS(shape) From f6f69e6aea8f065c616f12dcab06eb1492d0a19d Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sat, 5 Jan 2019 13:47:59 +1100 Subject: [PATCH 24/28] extract Scene tag from xml --- cadquery/occ_impl/jupyter_tools.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index 80c653b4..2ad0c8ce 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -2,10 +2,10 @@ from OCC.Display.WebGl.x3dom_renderer import X3DExporter from OCC.gp import gp_Quaternion, gp_Vec from uuid import uuid1 from math import tan +from xml.etree import ElementTree from .geom import BoundBox -N_HEADER_LINES = 10 BOILERPLATE = \ ''' @@ -25,14 +25,14 @@ BOILERPLATE = \ scr.async = false; scr.id = 'X3DOM_JS_MODULE'; scr.onload = function () {{ - x3dom.reload(); + x3dom.reload(); }} head.insertBefore(scr, head.lastChild); }} else if (typeof x3dom != 'undefined') {{ //call reload only if x3dom already loaded x3dom.reload(); }} - + //document.getElementById('{id}').runtime.fitAll() ''' @@ -69,7 +69,8 @@ def x3d_display(shape, line_color=(0,0,0), line_width=2., mesh_quality=.3): - + + # Export to XML tag exporter = X3DExporter(shape, vertex_shader, fragment_shader, @@ -81,19 +82,22 @@ def x3d_display(shape, line_color, line_width, mesh_quality) - + exporter.compute() x3d_str = exporter.to_x3dfile_string(shape_id=0) - x3d_str = '\n'.join(x3d_str.splitlines()[N_HEADER_LINES:]) - + xml_et = ElementTree.fromstring(x3d_str) + scene_tag = xml_et.find('./Scene') + + # Viewport Parameters bb = BoundBox._fromTopoDS(shape) d = max(bb.xlen,bb.ylen,bb.zlen) c = bb.center - + vec = gp_Vec(0,0,d/1.5/tan(FOV/2)) quat = gp_Quaternion(*ROT) vec = quat*(vec) + c.wrapped - - return add_x3d_boilerplate(x3d_str, + + # return boilerplate + Scene + return add_x3d_boilerplate(ElementTree.tostring(scene_tag), d=(vec.X(),vec.Y(),vec.Z()), - center=(c.x,c.y,c.z)) \ No newline at end of file + center=(c.x,c.y,c.z)) From d27ccbc4cecd71d3d8820400415ff8943757c66c Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sat, 5 Jan 2019 13:55:23 +1100 Subject: [PATCH 25/28] decoding bytes --- cadquery/occ_impl/jupyter_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index 2ad0c8ce..2af4ea6d 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -98,6 +98,6 @@ def x3d_display(shape, vec = quat*(vec) + c.wrapped # return boilerplate + Scene - return add_x3d_boilerplate(ElementTree.tostring(scene_tag), + return add_x3d_boilerplate(str(ElementTree.tostring(scene_tag).decode('utf-8')), d=(vec.X(),vec.Y(),vec.Z()), center=(c.x,c.y,c.z)) From 06ea0df15c9c0e0a813602003d92dee7cc05d9eb Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sat, 5 Jan 2019 13:56:58 +1100 Subject: [PATCH 26/28] added TestJupyter --- runtests.py | 9 +++++---- tests/TestJupyter.py | 13 +++++++++++++ tests/__init__.py | 13 +++++++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 tests/TestJupyter.py diff --git a/runtests.py b/runtests.py index 89e1fd85..f1052ca9 100644 --- a/runtests.py +++ b/runtests.py @@ -8,14 +8,15 @@ import unittest #on py 2.7.x on win suite = unittest.TestSuite() -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI)) +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters)) +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestJupyter.TestJupyter)) +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) if __name__ == '__main__': result = unittest.TextTestRunner().run(suite) - sys.exit(not result.wasSuccessful()) \ No newline at end of file + sys.exit(not result.wasSuccessful()) diff --git a/tests/TestJupyter.py b/tests/TestJupyter.py new file mode 100644 index 00000000..7f4b92c2 --- /dev/null +++ b/tests/TestJupyter.py @@ -0,0 +1,13 @@ +from tests import BaseTest + +import cadquery + +class TestJupyter(BaseTest): + def test_repr_html(self): + cube = cadquery.Workplane('XY').box(1, 1, 1) + 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 diff --git a/tests/__init__.py b/tests/__init__.py index ae906992..bae4439d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -49,5 +49,14 @@ class BaseTest(unittest.TestCase): self.assertAlmostEqual(i, j, places) -__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes', - 'TestExporters', 'TestCQSelectors', 'TestImporters', 'TestCQGI'] +__all__ = [ + 'TestCadObjects', + 'TestCadQuery', + 'TestCQGI', + 'TestCQSelectors', + 'TestCQSelectors', + 'TestExporters', + 'TestImporters', + 'TestJupyter', + 'TestWorkplanes', +] From d55e22de1fd629fa57b7cebdb6acd9132b1471d4 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sat, 5 Jan 2019 14:11:51 +1100 Subject: [PATCH 27/28] import uuid4 (derp) --- cadquery/occ_impl/jupyter_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index 2af4ea6d..d378a784 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -1,6 +1,6 @@ from OCC.Display.WebGl.x3dom_renderer import X3DExporter from OCC.gp import gp_Quaternion, gp_Vec -from uuid import uuid1 +from uuid import uuid4 from math import tan from xml.etree import ElementTree From ad6d0b0b9356efbfe6c3d95d91315e02995f6858 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Sun, 6 Jan 2019 02:51:25 +1100 Subject: [PATCH 28/28] cast to str redundant --- cadquery/occ_impl/jupyter_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index d378a784..de95d5b5 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -98,6 +98,6 @@ def x3d_display(shape, vec = quat*(vec) + c.wrapped # return boilerplate + Scene - return add_x3d_boilerplate(str(ElementTree.tostring(scene_tag).decode('utf-8')), + return add_x3d_boilerplate(ElementTree.tostring(scene_tag).decode('utf-8'), d=(vec.X(),vec.Y(),vec.Z()), center=(c.x,c.y,c.z))