extensions - finger jointed boxes

CNC controlled laser cutters have enabled the creation of inexpensive finger jointed boxes constructed from many materials. However, the creation of the patterns to feed into the laser can be quite time consuming. The finger jointed boxes methods within the cq_warehouse extensions package allow the creation of these patterns from a simple solid object.

finger_jointed_boxes

Usage

To create these boxes, one just needs to create the solid object, select edges, and use the makeFingerJoints() method, as follows:

import cadquery as cq
import cq_warehouse.extensions

# Create an empty assembly which will be populated with the finger jointed box
polygon_box_assembly = cq.Assembly()

# Create the box shape and then the finger jointed faces
polygon_box_faces = (
    cq.Workplane("XY")
    .polygon(5, 100)
    .extrude(60)
    .edges("not >Z")
    .makeFingerJoints(
        materialThickness=5,
        targetFingerWidth=20,
        kerfWidth=0,
        baseAssembly=polygon_box_assembly,
    )
)
# Is the finger jointed box valid?
print(f"{polygon_box_assembly.areObjectsValid()=}")
print(f"{polygon_box_assembly.doObjectsIntersect()=}")

# Store the faces as DXF files
for i, box_face in enumerate(polygon_box_faces.faces().vals()):
    center = box_face.Center()
    face_plane = cq.Plane(origin=center, normal=box_face.normalAt(center))
    face_workplane = cq.Workplane(face_plane).add(box_face)
    cq.exporters.export(face_workplane, fname="box_face" + str(i) + ".DXF")

if "show_object" in locals():
    show_object(polygon_box_assembly, name="polygon_box_assembly")
>>> polygon_box_assembly.areObjectsValid()
True
>>> polygon_box_assembly.doObjectsIntersect()
False

Warning

Not all shapes will result in a valid set of finger jointed box patterns.

Although the goal of this package is to enable the creation of a finger jointed box from any object with planar faces, this goal has not been fully achieved. Carefully inspect the output to ensure it is correct before cutting boxes to avoid disappointment.

The Workplane used to create a finger jointed box must contain a solid object (actually a Solid or Compound object), and one or more Edges that is to be jointed. In the above example, all of the edges are selected except for those on the top of the object which results in an open box. The resulting faces can be further modified if required.

Here is an example of finger jointed faces of a simple box:

finger_jointed_box_faces

Notice how the finger joints on the nearest corner are in a different pattern than the simple fingers on the other parts of the box? This is done to avoid the creation of a missing corner.

Finger joint size is calculated internally such that an integer number of finger joints are present on each edge - i.e. if the targetFingerWidth would result in a partial finger joint, the actual finger joint width will reduced such the number of finger joints is rounded to an integer. This may result in finger joints on different edges being different sizes.

The use of an Assembly is optional but is recommended to aid in the visual validation of the output. Random colors are assigned to each of the box walls to aid this validation. Corners are where errors are most likely to appear, either as interference or as missing corners.

When working with shapes with non perpendicular faces (i.e. faces that don’t meet at 90˚) the depth of the finger joint is calculated to compensate for the angle by either making the joint extra deep (for angles greater than 90˚) or smaller (for angles less than 90˚). Unfortunately, not all possible combinations of corner angles have been compensated for so pay extra care when inspecting these corners.

The result of the makeFingerJoints() method is a set of Faces within the Workplane aligned with the original solid object. To store these faces as DXF files it is necessary to create a workplane oriented in the same way as the face as shown in the above example.

The full API is as follows:

Workplane.makeFingerJoints(materialThickness, targetFingerWidth, kerfWidth=0.0, baseAssembly=None)

Starting with a base object and a set of selected edges, create Faces with finger joints that they could be laser cut from flat material.

Example

For example, make a simple open topped laser cut box.

finger_jointed_box_assembly = Assembly()
finger_jointed_faces = (
    Workplane("XY")
    .box(100, 80, 60)
    .edges("not >Z")
    .makeFingerJoints(
        materialThickness=5,
        targetFingerWidth=10,
        kerfWidth=1,
        baseAssembly=finger_jointed_box_assembly,
    )
)

The assembly part is optional but if present the Assembly will contain the parts as if they were laser cut from a material of the given thickness.

Parameters
  • self (T) – workplane

  • materialThickness (float) – thickness of finger joints

  • targetFingerWidth (float) – approximate with of notch - actual finger width will be calculated such that there are an integer number of fingers on Edge

  • kerfWidth (float, optional) – Extra size to add (or subtract) to account for the kerf of the laser cutter. Defaults to 0.0.

  • baseAssembly (Assembly, optional) – Assembly to add parts to

Raises
  • ValueError – Missing Solid object

  • ValueError – Missing finger joint Edges

Returns

Faces ready to be exported to DXF files and laser cut

Return type

T

The kerfWidth parameter can be used to compensate for the size of the laser cut thus allowing a path for the laser to the created directly from the face. Check with the manufacturer to see if this compensation is required.

Validation

To help validate the finger jointed box two Assembly methods are available:

Checking for intersecting objects within the Assembly (a general purpose method where every pair of objects within the Assembly - in their given Location - are checked for an intersection) will identify if there are overlapping finger joints but will not find missing fingers. To check for missing corners, one can use the Volume method as follows:

import cadquery as cq
import cq_warehouse.extensions

simple_box_assembly = Assembly()
simple_box = Workplane("XY").box(100, 80, 50)
simple_box_volume = simple_box.faces(">Z").shell(-5, kind="intersection").val().Volume()
simple_box_faces = (
    simple_box.edges("not >Z").makeFingerJoints(
        materialThickness=5,
        targetFingerWidth=10,
        baseAssembly=simple_box_assembly,
    )
)
simple_box_assembly_volume_error = abs(
    simple_box_assembly.toCompound().Volume() - simple_box_volume
)
print(f"Is volume correct: {simple_box_assembly_volume_error<1e-5=}")