Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 52 additions & 23 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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.))
Expand Down Expand Up @@ -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'):
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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 '&lt {} &gt'.format(self.__repr__()[1:-1])
else:
Expand Down
80 changes: 56 additions & 24 deletions doc/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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))
Expand All @@ -354,22 +354,22 @@ 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

.. 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
-----------------------------
Expand Down Expand Up @@ -701,76 +701,76 @@ 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)
oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)
else:
oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)
oshell = oshell.edges("|Z").fillet(p_sideRadius)

#inner shell
ishell = oshell.faces("<Z").workplane(p_thickness,True)\
.rect((p_outerWidth - 2.0* p_thickness),(p_outerLength - 2.0*p_thickness))\
.extrude((p_outerHeight - 2.0*p_thickness),False) #set combine false to produce just the new boss
ishell = ishell.edges("|Z").fillet(p_sideRadius - p_thickness)

#make the box outer box
box = oshell.cut(ishell)

#make the screw posts
POSTWIDTH = (p_outerWidth - 2.0*p_screwpostInset)
POSTLENGTH = (p_outerLength -2.0*p_screwpostInset)

box = box.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)
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 = topOfLid.rotateAboutCenter((1,0,0),180)

#return the combined result
result =topOfLid.combineSolids(bottom)

show_object(result)

.. topic:: Api References
Expand Down Expand Up @@ -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)
Loading