diff --git a/cadquery/cq.py b/cadquery/cq.py index 211dd48ce..4dd8f72a0 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1065,7 +1065,7 @@ def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True): 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 +1242,7 @@ def hLineTo(self, xCoord, forConstruction=False): """ 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,7 +1271,7 @@ def polarLineTo(self, distance, angle, forConstruction=False): 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): """ @@ -1312,14 +1312,16 @@ def move(self, xDist=0, yDist=0): return self.newObject([self.plane.toWorldCoords(newCenter)]) def spline(self, listOfXYTuple, tangents=None, periodic=False, - forConstruction=False): + forConstruction=False, includeCurrent=True, makeWire=False): """ Create a spline interpolated through the provided points. :param listOfXYTuple: points to interpolate through :type listOfXYTuple: list of 2-tuple :param tangents: tuple of Vectors specifying start and finish tangent - :param periodic: creation of peridic curves + :param periodic: creation of periodic curves + :param includeCurrent: use current point as a starting point of the curve + :param makeWire: convert the resulting spline edge to a wire :return: a Workplane object with the current point at the end of the spline The spline will begin at the current point, and @@ -1346,22 +1348,49 @@ def spline(self, listOfXYTuple, tangents=None, periodic=False, Future Enhancements: * provide access to control points """ - gstartPoint = self._findFromPoint(False) vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple] - allPoints = [gstartPoint] + vecs - + + if includeCurrent: + gstartPoint = self._findFromPoint(False) + allPoints = [gstartPoint] + vecs + else: + allPoints = vecs + if tangents: - t1, t2 = tangents - tangents = (self.plane.toWorldCoords(t1), - self.plane.toWorldCoords(t2)) + t1, t2 = tangents + tangents = (self.plane.toWorldCoords(t1), + self.plane.toWorldCoords(t2)) e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic) - if not forConstruction: - self._addPendingEdge(e) + if makeWire: + rv = Wire.assembleEdges([e]) + if not forConstruction: + self._addPendingWire(rv) + else: + rv = e + if not forConstruction: + self._addPendingEdge(e) + + return self.newObject([rv]) + + def parametricCurve(self, func, N=400, start=0, stop=1): + """ + Create a spline interpolated through the provided points. + + :param func: function f(t) that will generate (x,y) pairs + :type func: float --> (float,float) + :param N: number of points for discretization + :param start: starting value of the parameter t + :param stop: final value of the parameter t + :return: a Workplane object with the current point unchanged + + """ - return self.newObject([e]) + allPoints = [func(start+stop*t/N) for t in range(N+1)] + + return self.spline(allPoints,includeCurrent=False,makeWire=True) def threePointArc(self, point1, point2, forConstruction=False): """ @@ -1389,7 +1418,7 @@ def threePointArc(self, point1, point2, forConstruction=False): 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). @@ -2268,7 +2297,7 @@ def _combineWithBase(self, obj): r = baseSolid.fuse(obj) return self.newObject([r]) - + def _cutFromBase(self, obj): """ Cuts the provided object from the base solid, if one can be found. @@ -2380,7 +2409,7 @@ def cut(self, toCut, combine=True, clean=True): solidRef.wrapped = newS.wrapped return self.newObject([newS]) - + def intersect(self, toIntersect, combine=True, clean=True): """ Intersects the provided solid from the current solid. @@ -2541,7 +2570,7 @@ def _extrude(self, distance, both=False, taper=None): # return r toFuse = [] - + if taper: for ws in wireSets: thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper) @@ -2550,7 +2579,7 @@ def _extrude(self, distance, both=False, taper=None): for ws in wireSets: thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) toFuse.append(thisObj) - + if both: thisObj = Solid.extrudeLinear( ws[0], ws[1:], eDir.multiply(-1.)) @@ -2784,7 +2813,7 @@ def clean(self): raise AttributeError( "%s object doesn't support `clean()` method!" % obj.ShapeType()) return self.newObject(cleanObjects) - + def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True, font="Arial", kind='regular'): """ @@ -2813,7 +2842,7 @@ def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True, """ r = Compound.makeText(txt,fontsize,distance,font=font,kind=kind, position=self.plane) - + if cut: newS = self._cutFromBase(r) elif combine: @@ -2823,12 +2852,12 @@ def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True, if clean: newS = newS.clean() return newS - + 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: diff --git a/doc/examples.rst b/doc/examples.rst index 5ac528dd3..b8eb487db 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -225,7 +225,7 @@ Polylines This example uses a polyline to create one half of an i-beam shape, which is mirrored to create the final profile. .. cq_plot:: - + (L,H,W,t) = ( 100.0, 20.0, 20.0, 1.0) pts = [ (0,H/2.0), @@ -344,7 +344,7 @@ Mirroring 3D Objects .lineTo(10,1) .close()) result = result0.extrude(100) - + result = result.rotate((0, 0, 0),(1, 0, 0), 90) result = result.translate(result.val().BoundingBox().center.multiply(-1)) @@ -354,8 +354,8 @@ Mirroring 3D Objects mirZY_neg = result.mirror(mirrorPlane="ZY", basePointVector=(-30,0,0)) mirZY_pos = result.mirror(mirrorPlane="ZY", basePointVector=(30,0,0)) - result = result.union(mirXY_neg).union(mirXY_pos).union(mirZY_neg).union(mirZY_pos) - + result = result.union(mirXY_neg).union(mirXY_pos).union(mirZY_neg).union(mirZY_pos) + show_object(result) .. topic:: Api References @@ -363,13 +363,13 @@ Mirroring 3D Objects .. hlist:: :columns: 2 - * :py:meth:`Workplane.moveTo` + * :py:meth:`Workplane.moveTo` * :py:meth:`Workplane.lineTo` * :py:meth:`Workplane.threePointArc` - * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane.extrude` * :py:meth:`Workplane.mirror` - * :py:meth:`Workplane.union` - * :py:meth:`CQ.rotate` + * :py:meth:`Workplane.union` + * :py:meth:`CQ.rotate` Creating Workplanes on Faces ----------------------------- @@ -701,25 +701,25 @@ A Parametric Enclosure 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 box 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 screw posts should be place. p_screwpostID = 4.0 #Inner Diameter of the screw post holes, should be roughly screw diameter not including threads p_screwpostOD = 10.0 #Outer Diameter of the screw posts.\nDetermines 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.\nSits inside the box body for a snug fit. - + #outer shell oshell = cq.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 = oshell.edges("|Z").fillet(p_sideRadius) @@ -727,35 +727,35 @@ A Parametric Enclosure else: oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius) oshell = oshell.edges("|Z").fillet(p_sideRadius) - + #inner shell ishell = oshell.faces("Z").workplane(-p_thickness)\ .rect(POSTWIDTH,POSTLENGTH,forConstruction=True)\ .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)\ .extrude((-1.0)*(p_outerHeight + p_lipHeight -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() #splits into two solids - + #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) @@ -763,14 +763,14 @@ A Parametric Enclosure 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 = topOfLid.rotateAboutCenter((1,0,0),180) - + #return the combined result result =topOfLid.combineSolids(bottom) - + show_object(result) .. topic:: Api References @@ -1091,3 +1091,35 @@ Panel With Various Connector Holes # Render the solid show_object(result) + + +Cycloidal gear +-------------- + +You can define complex geometries using the parametricCurve functionality. +This specific examples generates a helical cycloidal gear. + +.. cq_plot:: + :height: 400 + + import cadquery as cq + from math import sin, cos,pi,floor + + # define the generating function + def hypocycloid(t,r1,r2): + return ((r1-r2)*cos(t)+r2*cos(r1/r2*t-t),(r1-r2)*sin(t)+r2*sin(-(r1/r2*t-t))) + + def epicycloid(t,r1,r2): + return ((r1+r2)*cos(t)-r2*cos(r1/r2*t+t),(r1+r2)*sin(t)-r2*sin(r1/r2*t+t)) + + def gear(t,r1=4,r2=1): + if (-1)**(1+floor(t/2/pi*(r1/r2))) < 0: + return epicycloid(t,r1,r2) + else: + return hypocycloid(t,r1,r2) + + # create the gear profile and extrude it + result = cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1))\ + .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll() + + show_object(result) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index a03dbb335..dc817535d 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -362,7 +362,7 @@ def testRevolveCone(self): self.assertEqual(2, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) - + def testSpline(self): """ Tests construction of splines @@ -379,17 +379,17 @@ def testSpline(self): # Closed spline path_closed = Workplane("XZ").spline(pts,periodic=True).val() self.assertTrue(path_closed.IsClosed()) - + # attempt to build a valid face w = Wire.assembleEdges([path_closed,]) f = Face.makeFromWires(w) self.assertTrue(f.isValid()) - + # attempt to build an invalid face w = Wire.assembleEdges([path,]) f = Face.makeFromWires(w) self.assertFalse(f.isValid()) - + # Spline with explicit tangents path_const = Workplane("XZ").spline(pts,tangents=((0,1),(1,0))).val() self.assertFalse(path.tangentAt(0) == path_const.tangentAt(0)) @@ -763,7 +763,7 @@ def testIntersect(self): self.assertEqual(6, currentS.faces().size()) self.assertAlmostEqual(currentS.val().Volume(),0.5) - + currentS.intersect(toIntersect) self.assertEqual(6, currentS.faces().size()) @@ -1705,7 +1705,7 @@ def testExtrude(self): r = 1. h = 1. decimal_places = 9. - + # extrude in one direction s = Workplane("XY").circle(r).extrude(h, both=False) @@ -1731,13 +1731,13 @@ def testExtrude(self): self.assertTupleAlmostEquals(delta.toTuple(), (0., 0., 2. * h), decimal_places) - + def testTaperedExtrudeCutBlind(self): - + h = 1. r = 1. t = 5 - + # extrude with a positive taper s = Workplane("XY").circle(r).extrude(h, taper=t) @@ -1748,7 +1748,7 @@ def testTaperedExtrudeCutBlind(self): delta = top_face.val().Area() - bottom_face.val().Area() self.assertTrue(delta < 0) - + # extrude with a negative taper s = Workplane("XY").circle(r).extrude(h, taper=-t) @@ -1759,13 +1759,13 @@ def testTaperedExtrudeCutBlind(self): delta = top_face.val().Area() - bottom_face.val().Area() self.assertTrue(delta > 0) - + # cut a tapered hole s = Workplane("XY").rect(2*r,2*r).extrude(2*h).faces('>Z').workplane()\ .rect(r,r).cutBlind(-h, taper=t) - + middle_face = s.faces('>Z[-2]') - + self.assertTrue(middle_face.val().Area() < 1) def testClose(self): @@ -1800,27 +1800,49 @@ def testClose(self): # The obj1 shape shall have the same volume as the obj2 shape. self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) - + def testText(self): - + box = Workplane("XY" ).box(4, 4, 0.5) - + obj1 = box.faces('>Z').workplane().text('CQ 2.0',0.5,-.05,cut=True) - + #combined object should have smaller volume self.assertGreater(box.val().Volume(),obj1.val().Volume()) - + obj2 = box.faces('>Z').workplane()\ .text('CQ 2.0',0.5,.05,cut=False,combine=True) - + #combined object should have bigger volume self.assertLess(box.val().Volume(),obj2.val().Volume()) - + #verify that the number of top faces is correct (NB: this is font specific) self.assertEqual(len(obj2.faces('>Z').vals()),5) - + obj3 = box.faces('>Z').workplane()\ .text('CQ 2.0',0.5,.05,cut=False,combine=False) - + #verify that the number of solids is correct - self.assertEqual(len(obj3.solids().vals()),5) \ No newline at end of file + self.assertEqual(len(obj3.solids().vals()),5) + + def testParametricCurve(self): + + from math import sin, cos, pi + + k = 4 + r = 1 + + func = lambda t: ( r*(k+1)*cos(t) - r* cos((k+1)*t), + r*(k+1)*sin(t) - r* sin((k+1)*t)) + + res_open = Workplane('XY').parametricCurve(func).extrude(3) + + #open profile generates an invalid solid + self.assertFalse(res_open.solids().val().isValid()) + + res_closed = Workplane('XY').parametricCurve(func,start=0,stop=2*pi)\ + .extrude(3) + + #closed profile will generate a valid solid with 3 faces + self.assertTrue(res_closed.solids().val().isValid()) + self.assertEqual(len(res_closed.faces().vals()),3) \ No newline at end of file