Skip to content

Joints

joints

Joints: relations between two parts that place them and cut both.

Most joints put one part (or a tenon on it) into a socket cut into the other (Housing): the socket is derived from where the inserted part lands, so the two fit by construction, and the cut list names it by its shape (dado, rabbet, groove, mortise, ...).

Allowance dataclass

Allowance(side: SideRef, amount: float)

Extension of a part's blank beyond its design box at one side.

Joint

Base class of joints between two parts.

placement() gives the location of parts()[1] in the frame of parts()[0]. Cuts are produced once the placement is known, so that joints defined in a shared region can map geometry between the parts. A joint that doesn't place its parts (e.g. a screw through two parts placed by other joints) gets the placement they ended up in.

check_parts

check_parts() -> None

Fail early if the joint can't join its parts, e.g. a rod into a dado.

with_defaults

with_defaults() -> Self

A copy with every unset parameter filled in from the rules.

pieces

pieces(placement: Location) -> list[tuple[Part, Location]]

Separate pieces the joint places, e.g. a loose tenon or wedges, with their locations in the frame of parts()[0]. They must be added to the assembly like any other part.

Butt dataclass

Butt(
    a: SideRef,
    b: SideRef,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    angle: float = 0.0,
)

Bases: Joint

Side b glued against side a, without any cuts.

flush are the sides kept facing the same way (by default the reference faces, or the reference edges for a face); see mate for position and across. With angle, part b is turned by that many degrees in the plane of side a (see mate), e.g. a diagonal brace on a face.

Housing

Bases: Joint

A side of part b goes into a socket cut into a side of part a.

Part b reaches beyond its design box into part a (an allowance), either with its whole cross-section or with a tenon. The socket is cut where it lands, with clearance at the bottom if it doesn't go through. With through, the socket runs out of part a at both ends of the mating arris, e.g. a groove along the whole edge. With stopped, it stops that far short of each of the given sides of part a, e.g. a dado stopped short of the front edge so that it doesn't show; where part b would reach into the part left standing there, it is notched to clear it (e.g. the front corner of a shelf in a stopped dado). Subclasses give the sides, the depth and the tenon, and are dataclasses with the fields position, across, flush and clearance (and through and stopped, if they apply).

sides

sides() -> tuple[SideRef, SideRef]

The side the socket is cut into and the side going into it.

reach

reach() -> float

How far part b reaches into part a.

tenon_cut

tenon_cut() -> Tenon | None

The tenon on part b, or None when its whole cross-section goes in.

notches

notches(
    placement: Location,
    inserted: list[tuple[Range, Range, Range]],
) -> list[Cut]

Notches in part b where it reaches past the stops of the socket, so it clears the part of a left standing there; inserted are the boxes of b (in its frame) going into the socket.

socket_box

socket_box(
    placement: Location,
    inserted: tuple[Range, Range, Range],
    clearance: float | None = None,
) -> tuple[Range, Range, Range]

Ranges of the socket in part a, for a box of part b given in its frame.

DadoJoint dataclass

DadoJoint(
    dado: SideRef,
    end: SideRef,
    depth: float | None = None,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
)

Bases: Housing

An end (or an edge) of part b housed in a dado in a face (or an edge) of part a.

Part b's design length stops at the face of part a; the dado depth is added to it. By default the reference edges are flush and b is centered along a; give position e.g. as offset(100, ref=a.left_end). With stopped, e.g. {a.front_edge: 15}, a stopped dado: it stops short of the front edge, and the front corner of part b is notched to the stop.

RabbetJoint dataclass

RabbetJoint(
    rabbet: SideRef,
    end: SideRef,
    at: SideRef,
    depth: float | None = None,
    lap: float | None = None,
    through: bool = False,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
)

Bases: Housing

An end of part b in a rabbet cut into side rabbet of part a, at its side at.

E.g. the top of a carcase in rabbets at the top ends of the sides. The reference face of part b is flush with at; along the arris b is centered (or as given by position). With lap, part b gets a rabbet too, keeping a lip lap thick at its reference face: the two rabbets interlock. With through, the rabbet runs the whole length of part a, e.g. for a box bottom, into the rabbets at the box's corners.

SlidingDovetail dataclass

SlidingDovetail(
    socket: SideRef,
    end: SideRef,
    depth: float | None = None,
    thickness: float | None = None,
    slope: float = 7.0,
    half: bool = False,
    cut_tongue: bool = True,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
)

Bases: Housing

An end of part b with a dovetail tongue, slid into a dovetail socket across part a.

Like a dado joint (see DadoJoint for the placement), but the socket is narrower at the face than at its bottom, so the parts can only slide together along it. thickness is the tongue's thickness at the shoulder; with half only one cheek is angled.

With end a face of part b, part b lies flat against part a and its whole width goes into a wide, shallow socket, its edges bevelled by 1:slope (thickness is then the width at the shoulder, by default so that the tongue is as wide as part b at its end): e.g. a batten across the underside of a glued-up panel.

Without cut_tongue, the tongue is cut by another sliding dovetail and this one only places part b and cuts the socket into part a: e.g. a batten whose tongue runs across several boards glued edge to edge, each of them housing it.

GrooveJoint dataclass

GrooveJoint(
    groove: SideRef,
    edge: SideRef,
    depth: float | None = None,
    through: bool = True,
    position: Position = CENTERED,
    across: Position = CENTERED,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
)

Bases: Housing

An edge of part b (e.g. a panel) in a groove in a side of part a (e.g. a rail).

By default part b is centered across the thickness of part a and the groove runs through the whole length of part a, as when cut on a table saw.

TongueAndGroove dataclass

TongueAndGroove(
    tongue: SideRef,
    groove: SideRef,
    tongue_thickness: float | None = None,
    tongue_length: float | None = None,
    tongue_offset: float | None = None,
    cut_tongue: bool = True,
    through: bool = True,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
)

Bases: Housing

A tongue along an edge (or across an end) of part b in a groove in a side of part a.

The boards' design widths are their visible faces; the tongue is added. It is centered across the thickness unless tongue_offset (from the reference face) is given: with 0 it is flush with the reference face, as on a drawer bottom rabbeted on its underside.

Without cut_tongue, part b goes in whole and only the groove is cut, as wide as the tongue would be: e.g. a raised panel whose bevel, planed down to the groove's width at its mouth, runs on into it (see Chamfer with run_out).

MortiseTenon dataclass

MortiseTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] = 5.0,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
)

Bases: Housing

A tenon on an end of one part, in a mortise in a side of the other.

The reference faces of both parts are flush (see across to change that). The tenon has a shoulder on both edges, or a pair of them (at the front edge, at the back edge), and is centered across the thickness unless tenon_offset (from the reference face) is given. The mortise is cut where the tenon goes, with some clearance at its bottom; a tenon as long as the mortised part is deep goes through.

With haunch, an edge of the tenoned part (e.g. the outer edge of a rail), the tenon gets a haunch towards it, haunch_length long (by default as deep as a panel groove), and the mortised part a haunch mortise. It fills a groove running out of the mortised part, or the shoulder of a wide rail (e.g. a table apron) set down from the end of the mortised part, so its mortise doesn't run out of it.

With mitre, a face of the tenoned part, the end of the tenon is mitred at 45 degrees, shorter at the cheek towards that face: for two tenons meeting inside a leg. The mortise stays rectangular.

With tenon_chamfer, the arrises around the end of the tenon are chamfered, e.g. on a through tenon a bit longer than the mortised part is deep, so that it stands proud of it. With tenon_corner_chamfer, the corners at its edges get big chamfers, that far each way (see Tenon).

With tenons, there are that many tenons side by side between the shoulders, e.g. twin tenons on a wide rail, tenon_gap apart (by default as wide as the tenons), each in its own mortise. The other options apply to each of them (the haunch to the outer one).

With thickness_tenons, there are that many tenons one beside the other across the thickness, e.g. on a thick, square rail, thickness_gap apart (by default as thick as the tenons). Each is tenon_thickness thick, by default about as thick as the gaps and the cheek shoulders; tenon_offset is then to the nearest of them. With tenons too, they make a grid, each in its own mortise. A haunch is beside each of them across the thickness; a mitre runs across all.

WedgedMortiseTenon dataclass

WedgedMortiseTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] = 5.0,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    wedges: tuple[Board, ...] = (),
    kerf: float = 2.0,
    kerf_inset: float | None = None,
)

Bases: MortiseTenon

A through mortise and tenon, the tenon spread by wedges driven into saw kerfs in its end, into a mortise flared towards the outside.

The wedges are separate parts, one per kerf: one or two per tenon (see tenons and thickness_tenons), in the order of the kerfs along the tenon, the top tenons' first. They are given as made: as thick as the tenon (which by default they set), as long as the kerfs are deep, and as wide at their heads as the kerf plus the spread of the tenon (twice the spread for one kerf in the middle). The joint tapers them to fit. Two kerfs lie kerf_inset in from the edges of the tenon (by default a fifth of its width), so the outer parts of the tenon bend out into the flare at each end of the mortise; one kerf spreads both halves of the tenon. The mortise is flared over the depth of the kerfs (less what the tenon stands proud) and straight below.

The tenon goes through, by default flush with the far side of the mortised part; it may stand proud of it, then the wedges are as long. The model is of the joint glued up, the tenon spread (see WedgedTenon).

kerfs property

kerfs: int

Per tenon.

TuskTenon dataclass

TuskTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] = 5.0,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    wedge: Board | None = None,
    wedge_head: SideRef | None = None,
    wedge_slope: float = 8.0,
    protrusion: float | None = None,
    draw: float = 3.0,
)

Bases: MortiseTenon

A tusk tenon: a long through tenon held by a wedge (a tusk, a key) through a mortise in its end, a knock-down joint, e.g. for the stretchers of a workbench or a trestle table.

The tenon goes through the mortised part (the leg) and stands protrusion proud of its outer side (by default two and a half times the wedge's width). The wedge is a separate part, given as made: a straight blank as long, wide (at its head) and thick as the wedge; the joint tapers its back edge by 1:wedge_slope towards its right end, the tip. It goes through a mortise in the protruding part of the tenon, its straight front edge bearing on the outer side of the leg, its tapered edge on the outer end of the mortise, which is sloped to match. The mortise starts draw inside the outer side of the leg, so the wedge bears on the leg, not on the inner end of the mortise: driven in, it pulls the tenon out of the leg and the shoulders tight against it. The wedge is centered on the tenon both ways, so its head and its tip stick out on either side. The model is of the joint assembled, the wedge driven home.

wedge_head is the side of the mortised part the head of the wedge points to (by default its right end). With an end, the wedge is parallel to the stile (the leg's length): with the usual placement it goes through the tenon's width, from one edge to the other, e.g. upright through the tenon of a horizontal stretcher in a standing leg, and is as thick as a third or so of the tenon. With a face (or an edge), the wedge is perpendicular to the stile: it goes through the tenon's thickness, from one cheek to the other, e.g. lying level through an upright tenon, and may be thicker, its thickness across the tenon's width.

taper

taper() -> float

How much narrower the wedge is at its tip than at its head.

DowelTuskTenon dataclass

DowelTuskTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] = 5.0,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    tusks: tuple[Rod, ...] = (),
    bearings: tuple[Rod, ...] = (),
    tusk_head: SideRef | None = None,
    tusk_slope: float = 12.0,
    flat_length: float | None = None,
    protrusion: float | None = None,
)

Bases: MortiseTenon

A tusk tenon held by dowels instead of a wedge (as in Rex Krueger's workbench): a long through tenon, and in the part of it standing proud of the mortised part, a straight hole for each of the tusks, dowels with a tapered flat driven through, bearing on a half dowel.

The tenon goes through the mortised part (the leg) and stands protrusion proud of its outer side (by default the tusk's diameter and a half beyond the hole). The tusks are rods, given as made; the joint planes a flat on each over flat_length from its tip (its right end; by default half its length), from nothing to 1:tusk_slope of that at the tip, its flat towards the leg. Their heads point to tusk_head, a side of the mortised part (by default its front edge: the tusks perpendicular to the stile, through the tenon's thickness; an end makes them parallel to it, through its width). Across the tenon, they lie as tenons would in a rail: the tenon divided into twice as many parts and one, the tusks in the middle of every other one, e.g. of two tusks in the second and the fourth fifth.

The bearings, one per tusk or one for all of them, are half dowels (rods as made; the joint rips them in half) glued flat side down on the outer side of the leg, across the tusks, on the tips' side of the tenon, next to it. The tusks' flats bear on their round tops, which stay tangent to the flat however the tusk lies as it is driven in. A tusk is driven home when it bears on the half dowel at the middle of its flat; its hole is bored parallel to the leg, its inner edge below the top of the half dowel by about half the flat's depth there: driving the tusk on pulls the tenon out of the leg and its shoulders tight against it. Knocked out, the tusks free the tenon. The model is of the joint assembled.

taper

taper() -> float

How deep the flat is at the tusk's tip.

MitredShoulderTenon dataclass

MitredShoulderTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] | None = None,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    mitre_depth: float | None = None,
    mitred: tuple[SideRef, ...] | None = None,
)

Bases: MortiseTenon

A mortise and tenon whose shoulders are partly mitred, e.g. in a frame of square-ish stock.

The tenoned part reaches mitre_depth into the mortised part (by default a third of its width) with its whole cross-section, into a notch, and the tenon starts there. At each of mitred, edges of the tenoned part (by default both of them, for a T-joint), the notch is mitred at 45 degrees back to the side of the mortised part: the parts meet on a short mitre there and along the mortised part in between. For an L-shaped corner, mitre only the inner edge; the tenon may then go through, as long as the material behind the notch. The design length of the tenoned part is to the side of the mortised part, as for a plain tenon.

The tenon may reach under the mitres, which makes it wider and the joint sturdier: its shoulders are by default half as deep as the mitres at the mitred edges, as deep as them at the others (e.g. to keep material at the end of the stile at a corner). Under a mitre the tenon starts at the side of the mortised part, elsewhere at the bottom of the notch; its length is measured from there. The mortise is cut from the side of the mortised part, through the notch and the mitres.

LooseTenon dataclass

LooseTenon(
    a: SideRef,
    b: SideRef,
    tenon: Board,
    depth: float | None = None,
    tenon_position: Position = CENTERED,
    tenon_offset: float | None = None,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 2.0,
)

Bases: Joint

Two parts joined by a loose (floating) tenon glued into a mortise in each.

Side b of part b (an end, or an edge) butts against side a of part a, placed as by Butt. The loose tenon tenon is a separate part, e.g. a strip planed as thick as the mortises are wide: it crosses the joint line along its length (and grain), reaching depth into part a (by default half its length) and the rest into part b. Its reference face faces the same way as b's; on side b it is centered, or placed by tenon_position (along the arris with b's reference face) and tenon_offset (below b's reference face). Each mortise is clearance deeper than the tenon reaches, unless it goes through.

tenon_location

tenon_location() -> Location

Of the loose tenon in the frame of part b, its left end inside b.

LooseTongue dataclass

LooseTongue(
    a: SideRef,
    b: SideRef,
    tongue: Board,
    depth: float | None = None,
    through: bool = True,
    tongue_position: Position = CENTERED,
    tongue_offset: float | None = None,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 1.0,
)

Bases: Joint

Two parts edge to edge, joined by a loose tongue (a spline) glued into a groove in each.

Side b of part b (an edge, or an end) butts against side a of part a, placed as by Butt. The loose tongue tongue is a separate part, a strip planed as thick as the grooves are wide, its length along the joint and so its grain too: simple to make, if weaker across the joint than a strip of plywood or of short grain. Its width crosses the joint line, reaching depth into part a (by default half its width) and the rest into part b. Its reference face faces the same way as b's; along side b it is centered, or placed by tongue_position (along the arris with b's reference face), across it by tongue_offset (below b's reference face). Each groove is clearance deeper than the tongue reaches, for the glue.

With through (the default), the grooves run the whole length of the parts along the joint, as cut on a table saw, showing at their ends. Without, they are stopped grooves as long as the tongue, as cut with a router, e.g. hidden by a tongue shorter than the boards.

tongue_location

tongue_location() -> Location

Of the loose tongue in the frame of part b, its front edge inside b.

Bridle dataclass

Bridle(
    tenon: SideRef,
    slot: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_offset: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
)

Bases: Housing

A tenon over the whole width of an end of part b, through a slot in part a.

For an L-shaped corner the slot is at an end of part a, e.g. with position=flush_with(a.left_end); it then is a slot mortise. For a T-shaped bridle, with the slot in part b, see TeeBridle. The tenon is centered across the thickness unless tenon_offset is given.

TeeBridle dataclass

TeeBridle(
    slot: SideRef,
    edge: SideRef,
    position: Position = CENTERED,
    slot_thickness: float | None = None,
    slot_offset: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
)

Bases: Joint

A T-shaped bridle joint: a slot in the end of part b straddles part a, which is cut down to a tongue by a dado across each of its faces.

Part b comes to part a at its side edge and reaches across it, its end flush with a's opposite side: its design length is overall. The slot is slot_thickness wide (by default a third of part a, as is usual for bridles), centered across b's thickness unless slot_offset (from b's reference face) is given. The reference faces are flush (see across to change that); along part a, b is centered (or as given by position).

HalfLap dataclass

HalfLap(
    lap: SideRef,
    socket: SideRef,
    position: Position = CENTERED,
    lap_thickness: float | None = None,
    over: bool = False,
    proud: float = 0.0,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
)

Bases: Housing

An end of part b lapped over (or under) part a, both cut to half thickness.

Part b gets a lap as long as part a is wide, part a a dado (or an end rabbet at a corner) where it lands. With over, part b keeps the half at its reference face and lies over part a; by default it lies under it, so the reference face of part a stays whole.

With proud, part b stands that far out of part a beyond its flush side (a face), so it is let in that much less deep: e.g. the square rail of a workbench's leg, lapped to half its thickness, let in a quarter of the leg's thickness (half its lap) and standing out of the leg by the other quarter.

DovetailHalfLap dataclass

DovetailHalfLap(
    lap: SideRef,
    socket: SideRef,
    position: Position = CENTERED,
    lap_thickness: float | None = None,
    over: bool = False,
    proud: float = 0.0,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 0.0,
    stopped: dict[SideRef, float] | None = None,
    slope: float = 7.0,
    sloped: tuple[SideRef, ...] | None = None,
)

Bases: HalfLap

A half lap whose lap is dovetailed across the face, in a dovetail-shaped socket, so that part b can't be pulled out of part a along its length.

Placed and cut to half thickness as HalfLap. The lap is a tail as wide as part b at its end, its edges sloping in by 1:slope to the shoulder, where part b is cut back beside it; part a gets a socket of the tail's shape. sloped are the edges of part b that slope: by default both, except one flush with an end of part a, i.e. at an L-shaped corner it is a half dovetail sloped at the inner edge.

With proud (see HalfLap), e.g. a rail lapped to half its thickness and let in only half of that, the tail is shaped through the whole lap, and the socket is as deep as the lap is let in. With stopped, e.g. {leg.right_end: 30}, the socket stops that far short of that side of part a, so that a leg keeps a solid end above it: the tail is narrower there, part b cut back over the lap to the stop, a sloped edge sloping from there.

BoxJoint dataclass

BoxJoint(
    a: SideRef,
    b: SideRef,
    fingers: int | None = None,
    finger_width: float | None = None,
    flush: tuple[SideRef, SideRef] | None = None,
)

Bases: _CornerJoint

A box (finger) joint: both ends cut into square fingers that interlock.

The fingers alternate across the width, part a's at its reference edge; each finger is as long as the other board is thick, so the boards reach through each other, as with through dovetails: part a's reference face is outside, flush are the sides kept flush (by default the reference edges). The fingers are all equally wide: fingers of them in all, or else the odd number of them coming closest to finger_width (by default the thickness of part a), so that part a has fingers at both edges and the width is used up without a narrow finger left over.

ThroughDovetail dataclass

ThroughDovetail(
    tails: SideRef,
    pins: SideRef,
    tail_count: int = 4,
    slope: float = 7.0,
    pin_width: float = 8.0,
    half_pin_width: float = 10.0,
    flush: tuple[SideRef, SideRef] | None = None,
)

Bases: _CornerJoint

Through dovetails at a corner of two boards.

The tails are cut on an end of one board, the pins on an end of the other (any ends, so a board may have pins or tails at both ends). Both boards reach through each other, so each gets an allowance of the other's thickness. The pin board's reference face is outside; flush are the sides kept flush along the corner (by default the reference edges). The pin board may be narrower than the tail board, e.g. the back of a drawer sitting on its bottom; the tails are laid out across it. The geometry is built once in the frame of the pin board and split.

MitredCornerDovetail dataclass

MitredCornerDovetail(
    tails: SideRef,
    pins: SideRef,
    tail_count: int = 4,
    slope: float = 7.0,
    pin_width: float = 8.0,
    half_pin_width: float = 10.0,
    flush: tuple[SideRef, SideRef] | None = None,
    mitred: tuple[SideRef, ...] | None = None,
    mitre_width: float = 10.0,
)

Bases: ThroughDovetail

Through dovetails mitred at the edges of the corner, e.g. to hide a groove or a rabbet running along the inside of a box.

At each of the pin board's edges in mitred (by default both), a strip mitre_width wide across the width is mitred through the whole thickness instead of dovetailed, so the edge of the corner shows a mitre and a groove or a rabbet within the strip runs through both boards unseen. The dovetails are laid out across the rest of the width, from a half pin next to each mitre. The boards must be equally wide.

HalfBlindDovetail dataclass

HalfBlindDovetail(
    tails: SideRef,
    pins: SideRef,
    tail_count: int = 4,
    slope: float = 7.0,
    pin_width: float = 8.0,
    half_pin_width: float = 10.0,
    flush: tuple[SideRef, SideRef] | None = None,
    lap: float | None = None,
)

Bases: ThroughDovetail

Half-blind dovetails, e.g. at the front of a drawer.

Like through dovetails, but the tails stop short of the pin board's outside face, so it keeps a lap that hides them (by default a quarter of its thickness). The tails are as long as the sockets are deep.

BlindMitredDovetail dataclass

BlindMitredDovetail(
    tails: SideRef,
    pins: SideRef,
    tail_count: int = 4,
    slope: float = 7.0,
    pin_width: float = 8.0,
    half_pin_width: float = 10.0,
    flush: tuple[SideRef, SideRef] | None = None,
    lap: float | None = None,
    mitre_width: float = 10.0,
)

Bases: ThroughDovetail

Blind mitred (secret mitre) dovetails, e.g. at the corners of a fine carcase: from outside the corner looks like a plain mitre.

Both boards keep a lip at their outside face, mitred where they meet: the pin board one lap thick (by default a quarter of its thickness), the tail board one in proportion to its thickness, so that both lips meet the mitre at their inside corner. Behind the lips the tails and the pins interlock hidden, the tails as long as the pin board's sockets are deep and the pins stopping at the tail board's lip. At both edges a strip mitre_width wide is mitred through the whole thickness, so the edges show a mitre too; the dovetails are laid out between these strips, from a half pin next to each. The boards must be equally wide.

tail_lip

tail_lip() -> float

The thickness of the tail board's lip, covering the ends of the pins.

MitreCorner dataclass

MitreCorner(
    a: SideRef,
    b: SideRef,
    a_inside: SideRef,
    b_inside: SideRef,
    flush: tuple[SideRef, SideRef] | None = None,
)

Bases: Joint

Two ends meeting at a right-angled corner, both mitred.

a_inside and b_inside are the sides inside the corner: edges for a picture frame (mitre across the face), faces for a carcase (mitre across the edge). Design lengths are outside lengths, to the long points. The mitres meet on the diagonal of the corner: at 45 degrees for parts of equal width (or thickness, for a carcase).

Two edges can meet the same way, mitred along the grain, the faces inside the corner: e.g. the boards of a hollow leg, a long L-shaped corner. Design widths are then outside widths; the left ends are flush.

mitre_frame

mitre_frame() -> _MitreFrame

The mitre in the frame of part a.

arris_ref

arris_ref() -> SideRef

The side of part a that positions along the corner are measured from: its left end, front edge or top face.

MitredHalfLap dataclass

MitredHalfLap(
    a: SideRef,
    b: SideRef,
    a_inside: SideRef,
    b_inside: SideRef,
    flush: tuple[SideRef, SideRef] | None = None,
    face: SideRef | None = None,
    depth: float | None = None,
)

Bases: MitreCorner

A corner half lap mitred on the show face, as on a frame: the front half of both parts is mitred, the back half lapped.

Placed as MitreCorner (ends to the outside of the corner), with edges inside the corner. On face of part a (by default its reference face) and the face of part b flush with it, both parts are mitred depth deep (by default half their thickness), so the face shows a mitre; behind it part a keeps the whole corner, a lap, and part b is cut away there by an end rabbet, so the back shows a plain half lap. The mitre hides the end grain on the face, the lap gives the glue far more long-grain area than a plain mitre.

MitredFaceTenon dataclass

MitredFaceTenon(
    a: SideRef,
    b: SideRef,
    a_inside: SideRef,
    b_inside: SideRef,
    flush: tuple[SideRef, SideRef] | None = None,
    face: SideRef | None = None,
    depth: float | None = None,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    tenon_offset: float | None = None,
    shoulder: float | tuple[float, float] | None = None,
    clearance: float = 2.0,
)

Bases: MitredHalfLap

A frame corner mitred on the show face, with a hidden mortise and tenon behind the mitre (a mitred face tenon, as on a frame with a moulded or chamfered front).

Placed as MitreCorner (ends to the outside of the corner, design lengths to the outside), with edges inside the corner. On face of part a (by default its reference face) and the face of part b flush with it, both parts are mitred depth deep (by default a third of their thickness), so the face shows a mitre. Behind it, part a (the stile) runs on square to the corner and part b (the rail) butts square against its inside edge, as in a plain mortise and tenon: b is cut back to that shoulder by an end rabbet, but for a tenon, which goes into a mortise in a's inside edge. The tenon lies in the layer behind the mitre, by default centered across it, tenon_thickness thick (by default a third or so of the layer) and tenon_offset below face if given. Across part b's width it keeps a shoulder at both edges (or a pair of them, at the front edge and at the back edge), by default a quarter of its width, so the mortise stays closed both towards the end of part a and at its inside edge. It is tenon_length long (by default two thirds of part a's width), so it doesn't show at the outer edge, and the mortise clearance deeper.

tenon_box

tenon_box(
    placement: Location,
) -> tuple[Range, Range, Range]

Ranges of the tenon in the frame of part b.

KeyedMitre dataclass

KeyedMitre(
    a: SideRef,
    b: SideRef,
    a_inside: SideRef,
    b_inside: SideRef,
    flush: tuple[SideRef, SideRef] | None = None,
    keys: tuple[Board, ...] = (),
    depth: float | tuple[float, ...] | None = None,
    offsets: tuple[float, ...] | None = None,
    angle: float | tuple[float, ...] = 0.0,
    dovetail: float | None = None,
)

Bases: MitreCorner

A mitre corner with keys (slip feathers) glued into kerfs across its outside corner.

A key is a small piece slid into an open notch running across the arris where the boards meet (unlike a spline, SplinedMitre, hidden in the mitre): a straight key goes in from the outside, a dovetail key only along its grain, across the corner.

Placed and mitred as MitreCorner, in either orientation: a picture frame (edges inside the corner) or a carcase (faces inside). Each kerf is a slab square to the corner's arris, sawn from the outside corner in along the mitre line, depth deep (by default a third of the mitre line), on a cradle on the table saw. The keys are thin boards as made, as thick as the kerf, their grain across the mitre line, glued in proud and trimmed flush with both outside faces: the model shows them trimmed, as triangles. They are spaced evenly along the corner, or centered at offsets from part a's left end, front edge or top face (whichever the corner runs from); depth and angle may be given per key.

With angle, the kerf is turned that many degrees about the mitre line (the cradle set askew), so that a key shows as a slanted line on each face, a chevron round the corner. With dovetail (the slope of its sides, in degrees), the kerf is cut with a dovetail bit: as wide as the key is thick at its bottom, narrower towards the corner, and the key is a dovetail strip.

kerf

kerf(
    key: Board, depth: float, center: float, angle: float
) -> Part

The kerf for key in the frame of part a, through both parts.

key_location

key_location(
    key: Board, depth: float, center: float, angle: float
) -> Location

Of key in the frame of part a: its front edge on the bottom of the kerf, centered on the mitre, its length along the key, across the mitre.

SplinedMitre dataclass

SplinedMitre(
    a: SideRef,
    b: SideRef,
    a_inside: SideRef,
    b_inside: SideRef,
    flush: tuple[SideRef, SideRef] | None = None,
    splines: tuple[Board, ...] = (),
    along: Literal["corner", "mitre"] | None = None,
    depth: float | None = None,
    offsets: tuple[float, ...] | None = None,
    spline_offset: float | None = None,
    clearance: float = 1.0,
)

Bases: MitreCorner

A mitre corner with loose splines across the mitre, glued into grooves cut square into both mitre faces.

A spline is a small loose tenon (or tongue) in mortises or grooves in both parts, usually hidden; unlike a key (KeyedMitre), slid into an open notch across the corner's arris.

Placed and mitred as MitreCorner, in either orientation: a picture frame (edges inside the corner) or a carcase or a shelf (faces inside). The splines are thin boards, as thick as the grooves are wide, their grain along their length (as LooseTongue). Their width crosses the mitre, reaching depth into part a (by default half the width) and the rest into part b; each groove is clearance deeper.

along gives the direction of their length, by default depending on how the boards meet:

  • "corner" (the default for faces inside the corner, a carcase or a shelf): parallel to the corner's arris, e.g. one spline through the whole length of a box corner, or several shorter ones in a row in stopped grooves on a wide board;
  • "mitre" (the default for edges inside, a picture frame): along the mitre line, from the outside corner towards the inside, e.g. a spline hidden in a frame's mitre.

Along the corner, the splines are spaced with equal gaps, or centered at offsets from part a's left end, front edge or top face (whichever the corner runs from). Across the mitre line, they are centered on it, or at spline_offset from the outside corner along it. A groove reaching the end of the mitre runs out there (through); grooves breaking out of the outside of a part are refused.

spline_location

spline_location(spline: Board, center: float) -> Location

Of spline in the frame of part a, centered at coordinate center along the arris, its back edge depth inside part a.

MitreSplice dataclass

MitreSplice(
    a: SideRef,
    b: SideRef,
    short: SideRef,
    angle: float = 45.0,
)

Bases: Joint

Two parts end to end, lengthened by a mitre across both ends.

short is the side of part a that the mitre shortens: an edge for a mitre across the face, a face for a mitre across the edge. The reference faces and edges of both parts are flush. Design lengths are to the long points, so the overall length is shorter by the overlap of the mitres.

CrossLap dataclass

CrossLap(
    a: SideRef,
    b: SideRef,
    position: Position = CENTERED,
    overhang: float | None = None,
    depth: float | None = None,
)

Bases: Joint

Two parts crossing each other, each with a dado where the other one crosses.

Part b crosses part a at its edge a, with its end b first, and reaches overhang beyond a's other edge (by default the parts cross in their middles). The reference faces are flush; part a is cut depth deep from its reference face (by default half its thickness), part b from the other face, so b lies over a at the crossing.

Button dataclass

Button(
    mortise: SideRef,
    tongue: SideRef,
    level: SideRef,
    position: Position = CENTERED,
    thickness: float = 6.0,
    depth: float = 4.0,
    gap: float = 2.0,
    pull: float = 2.0,
    clearance: float = 2.0,
    play: float = 1.0,
)

Bases: Joint

A wooden button holding a top down: a block screwed to the underside of the top, with a tongue at its end in a short mortise in the inside of an apron.

The button's end tongue faces side mortise of the apron, gap away from it, and its top face is level with the apron's side level (its top edge, where the top lies); along the apron it is placed by position. The tongue is at the bottom of the button, thickness thick, and reaches depth into the apron. The mortise is pull higher than the tongue, so screwing the button pulls the top down onto the apron, clearance deeper, so the top can move towards the apron, and longer by play at either end, so it can move along it.

RoundMortiseTenon dataclass

RoundMortiseTenon(
    tenon: SideRef,
    mortise: SideRef,
    diameter: float | None = None,
    length: float | None = None,
    tip_diameter: float | None = None,
    chamfer: float = 0.0,
    clearance: float = 2.0,
    position: Position = CENTERED,
    across: Position = CENTERED,
    flush: tuple[SideRef, SideRef] | None = None,
    gap: float = 0.0,
)

Bases: Joint

A round tenon on an end of part b (a rod, or square stock) in a hole drilled into a side of part a, e.g. a rung into a leg or a stile.

The hole is drilled where the tenon lands, so they fit by construction, clearance deeper than the tenon reaches; a tenon as long as part a is deep goes through. By default the parts are centered on each other both ways (see mate for position, across and flush).

diameter is of the tenon, by default the whole rod (a rod going into a hole with its whole cross-section, without a shoulder), or about 0.6 of square stock. length is by default two thirds of the material behind the hole's side. With tip_diameter, the tenon and the hole are tapered (reamed); with chamfer, the tenon's end is chamfered.

The hole's side may be a long side of a rod, e.g. a stretcher into a round leg; the tenon's shoulder then meets the rod along a line.

With gap, the shoulder stands that far off part a, e.g. below a batten between them, through which the tenon passes (see Bore); the tenon's length includes it.

inside

inside() -> float

How far the tenon reaches into part a.

Bore dataclass

Bore(part: Part, tenon: SideRef, diameter: float)

Bases: Joint

A hole bored through part a for a round tenon (or a whole rod) on the end tenon of part b, passing through it, both placed by other joints: e.g. a stool's leg through a batten under the seat, tenoned into the seat (RoundMortiseTenon with a gap). The hole is diameter wide, on the tenon's axis.

WedgedRoundMortiseTenon dataclass

WedgedRoundMortiseTenon(
    tenon: SideRef,
    mortise: SideRef,
    diameter: float | None = None,
    length: float | None = None,
    tip_diameter: float | None = None,
    chamfer: float = 0.0,
    clearance: float = 2.0,
    position: Position = CENTERED,
    across: Position = CENTERED,
    flush: tuple[SideRef, SideRef] | None = None,
    gap: float = 0.0,
    wedge: Board | None = None,
    kerf: float = 2.0,
    spread: SideRef | None = None,
)

Bases: RoundMortiseTenon

A round tenon through a hole, held by a wedge in a saw kerf across its end, e.g. a stool's leg through its seat.

The wedge is a separate part, given as made: as long as the kerf is deep, at its head as wide as the kerf plus what it opens the kerf by, and as thick as fits within the tenon (its corners inside the circle, so a bit less than the tenon is wide); the joint tapers both its edges to fit. The kerf runs across the grain of part a, so that the wedge spreads the tenon along it and doesn't split it: towards spread, a side of part a (by default its left end).

The tenon goes through, by default flush with the far side of part a; it may stand proud of it, then the wedge is as long. The model ignores the wood's give: the tenon and the hole stay straight, the kerf opened to the wedge's shape (see WedgedRoundTenon). The small gaps left between the wedge's ends and the hole fill with glue.

Dowels dataclass

Dowels(
    a: SideRef,
    b: SideRef,
    dowels: tuple[Rod, ...],
    depth: float | None = None,
    offsets: tuple[float, ...] | None = None,
    dowel_offset: float | None = None,
    position: Position = CENTERED,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    clearance: float = 1.0,
)

Bases: Joint

Side b of part b (an end or an edge) butted against side a of part a, as by Butt, and joined by dowels glued into holes in both.

The dowels are separate parts (rods), all of the same size, reaching depth into part a (by default half their length) and the rest into part b. Along side b they are spaced evenly, or at offsets from its front edge (on an end) or its left end (on an edge); across it they lie in the middle of its thickness, or dowel_offset below its reference face. Each hole is clearance deeper than the dowel reaches, for glue.

centers

centers() -> list[Vector]

Of the dowels on side b, in the frame of part b.

PeggedMortiseTenon dataclass

PeggedMortiseTenon(
    tenon: SideRef,
    mortise: SideRef,
    position: Position = CENTERED,
    tenon_thickness: float | None = None,
    tenon_length: float | None = None,
    shoulder: float | tuple[float, float] = 5.0,
    clearance: float = 2.0,
    tenon_offset: float | None = None,
    haunch: SideRef | None = None,
    haunch_length: float | None = None,
    mitre: SideRef | None = None,
    tenon_chamfer: float = 0.0,
    tenon_corner_chamfer: float = 0.0,
    tenons: int = 1,
    tenon_gap: float | None = None,
    thickness_tenons: int = 1,
    thickness_gap: float | None = None,
    across: Position = FLUSH,
    flush: tuple[SideRef, SideRef] | None = None,
    peg: Rod | None = None,
    peg_distance: float | None = None,
    drawbore: float = 0.0,
)

Bases: MortiseTenon

A mortise and tenon held by a peg (a rod) through the mortised part and the tenon, across its cheeks: pinned, or drawbored.

The peg goes through the mortised part, centered on its thickness there (so a longer peg stands proud of both sides, to be trimmed), its axis peg_distance from the mortised side (by default about a third of the tenon's length), in the middle of the tenon's width.

With drawbore, the hole in the tenon is drilled that much closer to the shoulder, so the peg driven in pulls the shoulders tight. As for wedged tenons, the model shows the joint assembled: the holes and the peg straight; the offset is noted in the cut list.

Screw dataclass

Screw(
    head: SideRef,
    into: Part,
    diameter: float = 4.0,
    length: float = 30.0,
    at: dict[SideRef, float] | None = None,
    head_diameter: float | None = None,
    counterbore_depth: float | None = None,
    clearance_diameter: float | None = None,
    pilot_diameter: float | None = None,
)

Bases: Joint

A screw driven through part b from its side head into part a, both placed by other joints, e.g. a batten screwed to the back of a door.

Part b gets a clearance hole, countersunk for the screw's head (by default twice as wide as the screw), or with counterbore_depth counterbored as wide, e.g. for a plug; part a a pilot hole, as deep as the screw reaches into it. The hole is at at on side head (see Hole). The screw itself isn't modelled; the cut list names it.

Butterfly dataclass

Butterfly(
    face: SideRef,
    key: Board,
    at: dict[SideRef, float] | None = None,
    angle: float = 0.0,
    waist: float | None = None,
    other: SideRef | None = None,
)

Bases: Joint

A butterfly key (a bow tie, two dovetails tail to tail) inlaid into a shallow mortise in a face, across a crack in a board or across the joint of two boards, glued and flush with the face.

The key is a board as made: as long as the bow tie, as wide as its ends and as thick as the mortise is deep, its grain along its length (across the crack); the joint shapes it to its waist (by default a third of its width). On face it is centered, or at at distances of its center from sides of the part next to the face (as for Hole), its length across the grain, or turned angle degrees from that.

With other, the face of a second board flush with face (e.g. two slabs butted edge to edge), the mortise is cut into both. The joint doesn't place the boards (Joint.places), only the key. The crack itself isn't modelled: the board stays whole.

center

center() -> Vector

Of the key on the face, in the frame of its part.

key_location

key_location() -> Location

Of the key in the frame of the face's part, its top face flush with the face.

outline

outline(z0: float, z1: float) -> Part

The bow tie in the frame of the key, from z0 to z1.

default_flush

default_flush(
    a: SideRef, b: SideRef
) -> tuple[SideRef, SideRef]

The sides kept flush by default when side b mates with side a.

The reference faces, unless one of the mating sides is a face: then the reference edges (or the reference face for a mating edge).