Extending CadQuery

If you find that CadQuery doesnt suit your needs, you can easily extend it. CadQuery provides several extension methods:

  • You can load plugins others have developed. This is by far the easiest way to access other code
  • you can define your own plugins.
  • you can use FreeCAD script directly

Loading external Plugins

You can load a plugin using the tools.loadScript(URL) directive in your script.

Using FreeCAD Script

The easiest way to extend CadQuery is to simply use FreeCAD script inside of your build method. Just about any valid FreeCAD script will execute just fine. For example, this simple CadQuery script:

return Workplane("XY").box(1.0,2.0,3.0).val()

is actually equivalent to:

return Part.makeBox(1.0,2.0,3.0)

As long as you return a valid FreeCAD Shape, you can use any FreeCAD methods you like. You can even mix and match the two. For example, consider this script, which creates a FreeCAD box, but then uses cadquery to select its faces:

box = Part.makeBox(1.0,2.0,3.0)
cq = CQ(box).faces(">Z").size() # returns 6

Extending CadQuery: Plugins

Though you can get a lot done with FreeCAD, the code gets pretty nasty in a hurry. CadQuery shields you from a lot of the complexity of the FreeCAD api.

You can get the best of both worlds by wrapping your freecad script into a CadQuery plugin.

A CadQuery plugin is simply a function that is attached to the CadQuery CQ() or Workplane() class. When connected, your plugin can be used in the chain just like the built-in functions.

There are a few key concepts important to understand when building a plugin

The Stack

Every CadQuery object has a local stack, which contains a list of items. The items on the stack will be one of these types: * A CadQuery SolidReference object, which holds a reference to a FreeCAD solid * A FreeCAD object, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound

The stack is available by using self.objects, and will always contain at least one object.

Note

Objects and points on the stack are always in global coordinates. Similarly, any objects you create must be created in terms of global coordinates as well!

Preserving the Chain

CadQuery’s fluent api relies on the ability to chain calls together one after another. For this to work, you must return a valid CadQuery object as a return value. If you choose not to return a CadQuery object, then your plugin will end the chain. Sometimes this is desired for example CQ.size()

There are two ways you can safely continue the chain:

  1. return self If you simply wish to modify the stack contents, you can simply return a reference to self. This approach is destructive, because the contents of the stack are modified, but it is also the simplest.
  2. CQ.newObject() Most of the time, you will want to return a new object. Using newObject will return a new CQ or Workplane object having the stack you specify, and will link this object to the previous one. This preserves the original object and its stack.

Helper Methods

When you implement a CadQuery plugin, you are extending CadQuery’s base objects. As a result, you can call any CadQuery or Workplane methods from inside of your extension. You can also call a number of internal methods that are designed to aid in plugin creation:

  • Workplane._pointsOnStack() returns a FreeCAD Vector ( a point ) for each item on the stack. Useful if you are writing a plugin that you’d like to operate on all values on the stack, like Workplane.circle() and most other built-ins do
  • Workplane._makeWireAtPoints() will invoke a factory function you supply for all points on the stack, and return a properly constructed cadquery object. This function takes care of registering wires for you and everything like that
  • Workplane.newObject() returns a new Workplane object with the provided stack, and with its parent set to the current object. The preferred way to continue the chain
  • Workplane.findSolid() returns the first Solid found in the chain, working from the current object upwards in the chain. commonly used when your plugin will modify an existing solid, or needs to create objects and then combine them onto the ‘main’ part that is in progress
  • Workplane._addWire() must be called if you add a wire. This allows the base class to track all the wires that are created, so that they can be managed when extrusion occurs.
  • Workplane.wire() gathers up all of the edges that have been drawn ( eg, by line, vline, etc ), and attempts to combine them into a single wire, which is returned. This should be used when your plugin creates 2-d edges, and you know it is time to collect them into a single wire.
  • Workplane.plane() provides a reference to the workplane, which allows you to convert between workplane coordinates and global coordinates: * Plane.toWorldCoords() will convert local coordinates to global ones * Plane.toLocalCoords() will convet from global coordinates to local coordinates

Coordinate Systems

Keep in mind that the user may be using a work plane that has created a local coordinate system. Consequently, the orientation of shapes that you create are often implicitly defined by the user’s workplane.

Any objects that you create must be fully defined in global coordinates, even though some or all of the users’ inputs may be defined in terms of local coordinates.

Linking in your plugin

Your plugin is a single method, which is attached to the main Workplane or CadQuery object.

Your plugin method’s first parameter should be ‘self’, which will provide a reference to base class functionality. You can also accept other arguments.

To install it, simply attach it to the CadQuery or Workplane object, like this:

def _yourFunction(self,arg1,arg):
    do stuff
    return whatever_you_want

Workplane.yourPlugin = _yourFunction

That’s it!

Plugin Example

This ultra simple plugin makes cubes of the specified size for each stack point.

(The cubes are off-center because the boxes have their lower left corner at the reference points.)

X Y Z
def makeCubes(self,length):
    #self refers to the CQ or Workplane object

    #inner method that creates a cube
    def _singleCube(pnt):
        #pnt is a location in local coordinates
        #since we're using eachpoint with useLocalCoordinates=True
        return Solid.makeBox(length,length,length,pnt)

    #use CQ utility method to iterate over the stack, call our
    #method, and convert to/from local coordinates.
    return self.eachpoint(_singleCube,True)

#link the plugin into cadQuery
Workplane.makeCubes = makeCubes

#use the plugin
result = Workplane("XY").box(6.0,8.0,0.5).faces(">Z").rect(4.0,4.0,forConstruction=True).vertices() \
    .makeCubes(1.0).combineSolids()