Fillets/Faces/Edges Oh My

716 views
Skip to first unread message

nairb nilpop

unread,
May 1, 2021, 2:05:50 PM5/1/21
to CadQuery
Hi All,
I'm looking for design patterns to efficiently work with objects.  Here is a picture of a piece I designed to fix something for my wife.  Top view:
topOfHat.png
Bottom View:
bottomOfHat.png
Here is the code I used to create this object.  It took me a while to get this right and it still feels obtuse.  Are there better design patterns to use?  Any suggestions are greatly appreciated.
import cadquery as cq

wallThickness = 1.3
hatHeight = 9.0
topHatThickness = 4.0
pegHeight = 8.0
pegDiameter = 3.45
pegSubtraction = 0.02
brimWidth = 31.7
brimDepth = 16.45
hatTopperDiameter = 4.9
headHoleWidth = 14.0
headHoleDepth = 7.8
topHatWidth = 16.5
topHatDepth = 10.2
groveDepthDiameter = 1.0

brim = cq.Workplane("XY").rect(brimWidth, brimDepth)\
.rect(headHoleWidth, headHoleDepth)\
.extrude(wallThickness)
# Fillet the vertical edges of the inside of the head hole
brim = brim.edges("|Z and (>>Y[-2] or <<Y[-2])").fillet(1)
# Fillet the vertical edges of the brim
brim = brim.edges("|Z and (>Y or <Y)").fillet(4)
# Create the hollowed out portion of the hat
hallowCap = cq.Workplane("XY")\
.rect(topHatWidth, topHatDepth)\
.rect(headHoleWidth, headHoleDepth)\
.extrude(hatHeight-topHatThickness)
# Fillet the inside vertical edges of the head hole
hallowCap = hallowCap.edges("|Z and (>>Y[-2] or <<Y[-2])").fillet(1)
# Fillet the outside vertical edges of the head hole
hallowCap = hallowCap.edges("|Z and (>Y or <Y)").fillet(2)
# Create the solid top portion of the hat
solidCap = cq.Workplane("XY")\
.rect(topHatWidth, topHatDepth)\
.extrude(topHatThickness)
solidCap.faces(">Z").workplane(centerOption="CenterOfMass").tag(">Z")
solidCap.faces(">X").workplane(centerOption="CenterOfMass").tag(">X")
solidCap.faces(">Y").workplane(centerOption="CenterOfMass").tag(">Y")
# Hallow out a portion of the solid cap
solidCap = solidCap.workplaneFromTagged(">Z")\
.hole(hatTopperDiameter)
# Fillet the outside vertical edges of the solid cap
solidCap = solidCap.edges("|Z and (>Y or <Y)").fillet(2)
# Create the x axis detent
solidCap = solidCap.workplaneFromTagged(">X")\
.move(0, topHatThickness/2)\
.hole(groveDepthDiameter)
# create the y axis detent
solidCap = solidCap.workplaneFromTagged(">Y")\
.move(0, topHatThickness/2)\
.hole(groveDepthDiameter)

hat = (
cq.Assembly(hat)
.add(hallowCap, loc=cq.Location(cq.Vector(0, 0, wallThickness)))
.add(solidCap, loc=cq.Location(cq.Vector(0, 0, wallThickness+(hatHeight-topHatThickness))))
)

show_object(hat)

cq.exporters.export(hat, "testHat.stl", "STL")

nairb nilpop

unread,
May 1, 2021, 2:24:08 PM5/1/21
to CadQuery
Corrected code.

import cadquery as cq
cq.Assembly(brim)

.add(hallowCap, loc=cq.Location(cq.Vector(0, 0, wallThickness)))
.add(solidCap, loc=cq.Location(cq.Vector(0, 0, wallThickness+(hatHeight-topHatThickness))))
)

show_object(hat)

hat.save("testHat.step")

m...@geosol.com.au

unread,
May 2, 2021, 8:20:11 PM5/2/21
to CadQuery
I've run your code through black then added my version at the bottom as object hat2. Take what you want from it, both our versions work and this is just coding style, with one exception. Assembly does not union, so your STEP file will contain 3 solids where (I assume?) you just want one. You can use Workplane.translate in a very similar way to how you used the loc keyword of Assembly.add, and you probably want Workplane.union, not Assembly.add.



import cadquery as cq


wallThickness = 1.3
hatHeight = 9.0
topHatThickness = 4.0
pegHeight = 8.0
pegDiameter = 3.45
pegSubtraction = 0.02
brimWidth = 31.7
brimDepth = 16.45
hatTopperDiameter = 4.9
headHoleWidth = 14.0
headHoleDepth = 7.8
topHatWidth = 16.5
topHatDepth = 10.2
groveDepthDiameter = 1.0

brim = (
    cq.Workplane("XY")
    .rect(brimWidth, brimDepth)
    .rect(headHoleWidth, headHoleDepth)
    .extrude(wallThickness)
)
# Fillet the vertical edges of the inside of the head hole
brim = brim.edges("|Z and (>>Y[-2] or <<Y[-2])").fillet(1)
# Fillet the vertical edges of the brim
brim = brim.edges("|Z and (>Y or <Y)").fillet(4)
# Create the hollowed out portion of the hat
hallowCap = (
    cq.Workplane("XY")
    .rect(topHatWidth, topHatDepth)
    .rect(headHoleWidth, headHoleDepth)
    .extrude(hatHeight - topHatThickness)
)
# Fillet the inside vertical edges of the head hole
hallowCap = hallowCap.edges("|Z and (>>Y[-2] or <<Y[-2])").fillet(1)
# Fillet the outside vertical edges of the head hole
hallowCap = hallowCap.edges("|Z and (>Y or <Y)").fillet(2)
# Create the solid top portion of the hat
solidCap = cq.Workplane("XY").rect(topHatWidth, topHatDepth).extrude(topHatThickness)
solidCap.faces(">Z").workplane(centerOption="CenterOfMass").tag(">Z")
solidCap.faces(">X").workplane(centerOption="CenterOfMass").tag(">X")
solidCap.faces(">Y").workplane(centerOption="CenterOfMass").tag(">Y")
# Hallow out a portion of the solid cap
solidCap = solidCap.workplaneFromTagged(">Z").hole(hatTopperDiameter)
# Fillet the outside vertical edges of the solid cap
solidCap = solidCap.edges("|Z and (>Y or <Y)").fillet(2)
# Create the x axis detent
solidCap = (
    solidCap.workplaneFromTagged(">X")
    .move(0, topHatThickness / 2)
    .hole(groveDepthDiameter)
)
# create the y axis detent
solidCap = (
    solidCap.workplaneFromTagged(">Y")
    .move(0, topHatThickness / 2)
    .hole(groveDepthDiameter)
)

hat = (
    cq.Assembly(brim)
    .add(hallowCap, loc=cq.Location(cq.Vector(0, 0, wallThickness)))
    .add(
        solidCap,
        loc=cq.Location(cq.Vector(0, 0, wallThickness + (hatHeight - topHatThickness))),
    )
)

show_object(hat)

# hat.save("testHat.step")

hat2 = (
    cq.Workplane()
    .box(brimWidth, brimDepth, wallThickness, centered=(True, True, False))
    .tag("brim")
    .faces(">Z")
    .workplane()
    .box(topHatWidth, topHatDepth, hatHeight, centered=(True, True, False))
)

for sel in ["X", "Y"]:
    hat2 = (
        hat2
        .faces(">" + sel + "[1]")
        .edges(">Z")
        .workplane(centerOption="CenterOfMass")
        .hole(groveDepthDiameter)
    )

hat2 = (
    hat2
    .faces("<Z")
    .workplane(centerOption="ProjectedOrigin", origin=(0, 0, 0))
    .rect(headHoleWidth, headHoleDepth)
    .cutBlind(-(hatHeight - topHatThickness))
    .faces(">Z")
    .workplane()
    .hole(hatTopperDiameter)
    .edges("|Z", tag="brim")
    .fillet(4)
    .faces(">X[1] or >X[4]")
    .edges("|Z")
    .fillet(2)
    .faces(">X[2] or >X[3]")
    .edges("|Z")
    .fillet(1)
)

show_object(hat2, "hat2")




The way you used Assembly is very interesting. I hadn't thought about making simple solids, then using Assembly to position them, then creating a single Solid from the Assembly. It seems obvious in hindsight, but I'm going to blame my experience with commercial CAD for giving me tunnel vision on how an assembly should be used.

In my code I leave fillets until the last step because the filleting process is complicated and sometimes you can avoid a kernel error this way.

nairb nilpop

unread,
May 2, 2021, 9:59:59 PM5/2/21
to CadQuery
Thanks!  It's late tonight so I'll have to give this a try this week.  I really appreciate the help.

Best regards!

Roger Maitland

unread,
Jun 1, 2021, 7:42:49 PM6/1/21
to CadQuery
Here is an alternative using some different techniques that hopefully will be useful:

import cadquery as cq

wallThickness = 1.3
hatHeight = 9.0
topHatThickness = 4.0
brimWidth = 31.7
brimDepth = 16.45
hatTopperDiameter = 4.9
headHoleWidth = 14.0
headHoleDepth = 7.8
topHatWidth = 16.5
topHatDepth = 10.2
groveDepthDiameter = 1.0

def makeRectFillet(length: float, width: float, radius:float=0.0, center:bool=True) -> cq.Wire:
    """
    Create a cq.Wire in the shape of a square with optionally rounded corners
    """
    # define the location of the center of the shape
    centerShift = cq.Vector(length/2,width/2,0) if not center else cq.Vector(0,0,0)
    hlr = length/2-radius
    hwr = width/2-radius
    if hlr<0 or hwr<0:
        raise ValueError("Fillet radius {} is too large for given rectangle dimension ({},{})".format(radius,length,width))
    r = cq.Wire.makePolygon([cq.Vector(hlr,hwr,0),cq.Vector(-hlr,hwr,0),cq.Vector(-hlr,-hwr,0),cq.Vector(hlr,-hwr,0),cq.Vector(hlr,hwr,0)])
    r = cq.Wire.assembleEdges(r.offset2D(radius)).translate(centerShift)
    return r
def _rectFillet(self, length: float, width: float, radius=0.0, center=True) -> cq.Workplane:
    """
    Create a cq.Workplane in the shape of a square with optionally rounded corners
    """
    r = makeRectFillet(length,width,radius,center)
    return self.eachpoint(lambda loc: r.moved(loc), True)
cq.Workplane.rectFillet = _rectFillet       # Add this custom method to the Workplane class

hat = (cq.Workplane("XY")
        .rectFillet(brimWidth,brimDepth,4)              # Define the brim as a ..
        .rectFillet(headHoleWidth,headHoleDepth,1)      # .. filleted rectangle with a ..
        .extrude(wallThickness)                         # .. filleted rectangular hole of wallThickness
        .faces(">Z").workplane()                        # On the top of the brim ..
        .rectFillet(topHatWidth,topHatDepth,2)          # .. create the walls of the hat ..
        .rectFillet(headHoleWidth,headHoleDepth,1)      # .. as the difference between two filleted ..
        .extrude(hatHeight-topHatThickness)             # .. rectangles extruded to the cap
        .faces(">Z").workplane()                        # On the top of the hat walls ..
        .rectFillet(topHatWidth,topHatDepth,2)          # .. create a cap with a ..
        .circle(hatTopperDiameter/2)                    # .. circular hole in it of ..
        .extrude(topHatThickness)                       # .. thickness topHatThickness
        # Create two perpendicular semi-circular slots across the top of the hat
        .cut(cq.Solid.makeCylinder(groveDepthDiameter/2,topHatWidth,pnt=cq.Vector(-topHatWidth/2,0,wallThickness+hatHeight),dir=cq.Vector(1,0,0)))
        .cut(cq.Solid.makeCylinder(groveDepthDiameter/2,topHatDepth,pnt=cq.Vector(0,-topHatDepth/2,wallThickness+hatHeight),dir=cq.Vector(0,1,0)))

)
cq.exporters.export(hat, "testHat.stl", "STL")
if "show_object" in locals():
    show_object(hat,name="hat")


I expanded on the core Workplane class by adding a 'rectFillet' method which is a filleted rectangle similar to the 'slot2D' method but with width. This allows the definition of 'hat' to build up from the brim to the top of the hat without having to select vertical edges for which to fillet. For the semi-circular slots on the top of the hat, I've found it tricky know where the center of the workplane will be when using '.faces(">X")' so I used the cylinder from the cq.Solid class which takes global coordinates.

Cheers,
Roger

Jeremy Wright

unread,
Jun 1, 2021, 7:46:38 PM6/1/21
to CadQuery
Just in case it's useful, fillet2D and chamfer2D have been added to CadQuery since this thread was started.



Both of those operations also apply to faces.

Roger Maitland

unread,
Jun 2, 2021, 10:27:08 AM6/2/21
to CadQuery
Thanks Jeremy, fillet2D and chamfer2D are welcome additions. Unfortunately, I guess I don't have the latest version of cadquery even after doing a '$ conda update cadquery'. Here is the version info I get from conda:

$ conda list cadquery
# packages in environment at ../anaconda3/envs/cadquery:
#
# Name                    Version                   Build  Channel
cadquery                  master                    py3.8    cadquery


Any idea what I'm doing wrong?

Thanks,
Roger

Jeremy Wright

unread,
Jun 2, 2021, 11:15:19 AM6/2/21
to CadQuery
Some users have problems when updating their Anaconda environment, but there is a way to try to force the update with the following that might work.

conda remove cadquery
conda clean --all --force-pkgs-dirs
conda install --force-reinstall -c conda-forge -c cadquery cadquery=master

If that doesn't work, the least problematic way is to remove the environment and recreate it (which is almost always what I do). Just deactivate your cadquery environment and execute the following.

conda env remove -n cadquery

Then you can reinstall via the Anaconda instructions in the readme.

# Set up a new environment
conda create -n cadquery

# Activate the new environment
conda activate cadquery

# CadQuery development is moving quickly, so it is best to install the latest version from GitHub master
conda install -c conda-forge -c cadquery cadquery=master

Roger Maitland

unread,
Jun 3, 2021, 11:56:10 AM6/3/21
to CadQuery
Thanks Jeremy - after several failed attempts I just recreated my entire environment with both cadquery and cq-editor installed from master and it's all good now.

Back to the fillet2D feature.  I got the following to work but I'm wondering if this how fillet2D is intended to be used?

brimRectangle = cq.Workplane("XY").rect(brimWidth,brimDepth)    # Create the rectangle that encloses the brim
corners = brimRectangle.vertices(">Z").vals()                   # Select the corners to be filleted (in this case ">Z" selects all just like () would)
brim = brimRectangle.val().fillet2D(4,corners)                  # Fillet the enclosing rectangle to create the brim shape


I can't see how to make the syntax any more compact as the rectangle is needed to create the corners then used in the fillet operation.

Cheers,
Roger

Jojain

unread,
Jun 4, 2021, 12:38:40 AM6/4/21
to CadQuery
Hello, 

You can make yourself a simple plugin like discussed here 
https://github.com/CadQuery/cadquery/issues/746#issue-866842125

Jeremy Wright

unread,
Jun 4, 2021, 6:56:58 AM6/4/21
to CadQuery
Reply all
Reply to author
Forward
0 new messages