Skip to content

kumiki.joints.workshop.corner

Flat import path

kumiki/__init__.py re-exports everything on this page via from kumiki import *, so every name below is also available directly as kumiki.corner -- you do not need to import from the submodule path shown in the heading above.

kumiki.joints.workshop.corner

Kumiki - Corner joint construction functions

cut_箱相欠き車知栓仕口 module-attribute

cut_hako_aikaki_shachi_sen_shikuchi module-attribute

cut_hako_aikaki_shachi_sen_shikuchi = cut_mitered_and_keyed_lap_joint_on_plane_aligned_timbers

SingleDovetailSizeParameter dataclass

SingleDovetailSizeParameter(angle: Numeric, small_width: Numeric, depth: Optional[Numeric] = None)
______

_\ /____ }depth ^ small_width

angle: flare of the dovetail walls, 0 is perpendicular (a straight finger) small_width: width at the shoulder, where the dovetail is narrowest depth: how far the dovetail reaches into timber2. None goes all the way through; less than that makes a half blind dovetail. Full blind dovetails are done with another joint.

angle instance-attribute

angle: Numeric

small_width instance-attribute

small_width: Numeric

depth class-attribute instance-attribute

depth: Optional[Numeric] = None

cut_plain_miter_joint

cut_plain_miter_joint(arrangement: CornerJointTimberArrangement) -> Joint

Creates a miter joint between two timbers, cutting each end at half the angle between them.

Both ends are cut by the same bisecting plane so they meet flush at the corner. Works for any angle including parallel timbers (parallel produces a perpendicular end cut).

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner joint arrangement with timber1, timber2, timber1_end, timber2_end.

required

Returns:

Type Description
Joint

Joint object containing the two CutTimbers.

Source code in kumiki/joints/workshop/corner/plain_miter_joint.py
def cut_plain_miter_joint(arrangement: CornerJointTimberArrangement) -> Joint:
    """
    Creates a miter joint between two timbers, cutting each end at half the angle between them.

    Both ends are cut by the same bisecting plane so they meet flush at the corner.
    Works for any angle including parallel timbers (parallel produces a perpendicular end cut).

    Args:
        arrangement: Corner joint arrangement with timber1, timber2, timber1_end, timber2_end.

    Returns:
        Joint object containing the two CutTimbers.
    """
    timberA = arrangement.timber1
    timberA_end = arrangement.timber1_end
    timberB = arrangement.timber2
    timberB_end = arrangement.timber2_end

    warn_if_arrangement_timbers_imperfect(arrangement)

    # Get the end directions for each timber (pointing outward from the timber)
    if timberA_end == TimberEnd.TOP:
        directionA = timberA.get_length_direction_global()
        endA_position = locate_top_center_position(timberA).position
    else:  # BOTTOM
        directionA = -timberA.get_length_direction_global()
        endA_position = locate_bottom_center_position(timberA).position

    if timberB_end == TimberEnd.TOP:
        directionB = timberB.get_length_direction_global()
        endB_position = locate_top_center_position(timberB).position
    else:  # BOTTOM
        directionB = -timberB.get_length_direction_global()
        endB_position = locate_bottom_center_position(timberB).position

    # Find the intersection point (or closest point) between the two timber centerlines
    w0 = prune(endA_position - endB_position)
    a = safe_dot_product(directionA, directionA)
    b = safe_dot_product(directionA, directionB)
    c = safe_dot_product(directionB, directionB)
    d = safe_dot_product(directionA, w0)
    e = safe_dot_product(directionB, w0)

    denom = prune(a * c - b * b)
    if safe_zero_test(denom):
        # Parallel timbers: lines don't converge; bisect between the two end positions
        intersection_point = prune((endA_position + endB_position) / 2)
    else:
        # Parameters for closest points on each line
        t = prune((b * e - c * d) / denom)
        s = prune((a * e - b * d) / denom)

        # Get the closest points on each centerline
        pointA = prune(endA_position + directionA * t)
        pointB = prune(endB_position + directionB * s)

        # The intersection point is the midpoint between the two closest points
        intersection_point = prune((pointA + pointB) / 2)

    # Create the miter plane normal
    normA = safe_normalize_vector(directionA)
    normB = safe_normalize_vector(directionB)

    # The bisecting direction is the normalized sum of the two directions
    bisector = safe_normalize_vector(normA + normB)

    # The plane formed by the two timber directions has normal:
    plane_normal = cross_product(normA, normB)

    plane_normal_sq = safe_dot_product(plane_normal, plane_normal)
    if safe_zero_test_sq(plane_normal_sq):
        miter_normal = normA
    else:
        miter_normal = safe_normalize_vector(cross_product(bisector, plane_normal))

    # For timberA: check if miter_normal points away from or towards the timber
    dot_A = safe_dot_product(normA, miter_normal)
    if safe_compare(dot_A, 0, Comparison.GT):
        normalA = miter_normal
    else:
        normalA = -miter_normal

    local_normalA = safe_transform_vector(timberA.orientation.matrix.T, normalA)

    # For timberB: check if miter_normal points away from or towards the timber
    dot_B = safe_dot_product(normB, miter_normal)
    if safe_compare(dot_B, 0, Comparison.GT):
        normalB = miter_normal
    else:
        normalB = -miter_normal

    local_normalB = safe_transform_vector(timberB.orientation.matrix.T, normalB)

    # Calculate offset for each timber's miter cut
    cos_angle = safe_dot_product(normA, normB)
    cos_angle_clamped = max(prune(scalar(-1)), min(prune(scalar(1)), cos_angle))
    sin_half_angle_sq = (scalar(1) - cos_angle_clamped) / scalar(2)
    sin_half_angle = sqrt(sin_half_angle_sq) if safe_compare(sin_half_angle_sq, 0, Comparison.GT) else scalar(1)

    half_width_A = timberA.size[0] / scalar(2)
    half_height_A = timberA.size[1] / scalar(2)
    outer_corner_dist_A = sqrt(half_width_A ** scalar(2) + half_height_A ** scalar(2))

    half_width_B = timberB.size[0] / scalar(2)
    half_height_B = timberB.size[1] / scalar(2)
    outer_corner_dist_B = sqrt(half_width_B ** scalar(2) + half_height_B ** scalar(2))

    if not safe_zero_test(sin_half_angle):
        offset_dist_A = outer_corner_dist_A / sin_half_angle
        offset_dist_B = outer_corner_dist_B / sin_half_angle
    else:
        offset_dist_A = outer_corner_dist_A * scalar(100)
        offset_dist_B = outer_corner_dist_B * scalar(100)

    miter_cut_point_A = prune(intersection_point + bisector * offset_dist_A)
    miter_cut_point_B = prune(intersection_point + bisector * offset_dist_B)

    local_offsetA = safe_dot_product(miter_cut_point_A, normalA) - safe_dot_product(normalA, timberA.get_bottom_position_global())
    local_offsetB = safe_dot_product(miter_cut_point_B, normalB) - safe_dot_product(normalB, timberB.get_bottom_position_global())

    end_cut_A = HalfSpace(normal=local_normalA, offset=local_offsetA)
    end_cut_B = HalfSpace(normal=local_normalB, offset=local_offsetB)

    end_cut_A_distance_from_bottom = safe_dot_product(
        miter_cut_point_A - timberA.get_bottom_position_global(),
        timberA.get_length_direction_global(),
    )
    end_cut_B_distance_from_bottom = safe_dot_product(
        miter_cut_point_B - timberB.get_bottom_position_global(),
        timberB.get_length_direction_global(),
    )

    cutA = Cutting(
        timber=timberA,
        maybe_top_end_cut_distance_from_bottom=end_cut_A_distance_from_bottom if timberA_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=end_cut_A_distance_from_bottom if timberA_end == TimberEnd.BOTTOM else None,
        negative_csg=end_cut_A,
        assembly_freedom=AssemblyFreedom.translation(-directionA, freed_after=scalar(0)),
        label=CutCSGLabel("miter_cut"),
    )

    cutB = Cutting(
        timber=timberB,
        maybe_top_end_cut_distance_from_bottom=end_cut_B_distance_from_bottom if timberB_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=end_cut_B_distance_from_bottom if timberB_end == TimberEnd.BOTTOM else None,
        negative_csg=end_cut_B,
        assembly_freedom=AssemblyFreedom.translation(-directionB, freed_after=scalar(0)),
        label=CutCSGLabel("miter_cut"),
    )

    return Joint(
        cuttings={"timberA": cutA, "timberB": cutB},
        ticket=JointTicket(joint_type="plain_miter"),
        jointAccessories={},
    )

cut_plain_miter_joint_on_face_aligned_timbers

cut_plain_miter_joint_on_face_aligned_timbers(arrangement: CornerJointTimberArrangement) -> Joint

Creates a miter joint between two face-aligned timbers meeting at a 90-degree corner.

Like cut_plain_miter_joint, but requires the timbers to be orthogonal (perpendicular length axes). This is the common case for timber-frame corners.

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner joint arrangement with timber1, timber2, timber1_end, timber2_end. Timbers must be face-aligned and orthogonal.

required

Returns:

Type Description
Joint

Joint object containing the two CutTimbers.

Raises:

Type Description
AssertionError

If the timbers are not face-aligned or not orthogonal.

Source code in kumiki/joints/workshop/corner/plain_miter_joint.py
def cut_plain_miter_joint_on_face_aligned_timbers(arrangement: CornerJointTimberArrangement) -> Joint:
    """
    Creates a miter joint between two face-aligned timbers meeting at a 90-degree corner.

    Like `cut_plain_miter_joint`, but requires the timbers to be orthogonal (perpendicular
    length axes). This is the common case for timber-frame corners.

    Args:
        arrangement: Corner joint arrangement with timber1, timber2, timber1_end, timber2_end.
                     Timbers must be face-aligned and orthogonal.

    Returns:
        Joint object containing the two CutTimbers.

    Raises:
        AssertionError: If the timbers are not face-aligned or not orthogonal.
    """
    assert are_timbers_face_aligned(arrangement.timber1, arrangement.timber2), (
        "Timbers must be face-aligned (orientations related by 90-degree rotations) for this joint type"
    )
    assert are_timbers_orthogonal(arrangement.timber1, arrangement.timber2), (
        "Timbers must have perpendicular length axes (90-degree angle) for this joint type"
    )

    return cut_plain_miter_joint(arrangement)

cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers

cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers(arrangement: CornerJointTimberArrangement, tongue_thickness: Optional[Numeric] = None, tongue_position: Numeric = scalar(0)) -> Joint

Creates a plain tongue-and-fork corner joint (corner bridle style). Note, you can make an identical joint with cut_practice_mortise_and_tenon_corner_joint_on_plane_aligned_timbers setting tenon_distance_from_end to 0

In this joint, timber1 forms the tongue (material removed on both cheeks), and timber2 forms the fork (slot cut into its end). Tongue thickness is measured along the shared plane normal between the two timbers and defaults to one-third of the tongue timber dimension in that axis. Tongue position is an offset from the tongue timber centerline in that same axis.

End cuts are aligned to the opposing timber's opposite face: - Tongue timber end is cut to the face opposite the fork entry face. - Fork timber end is cut to the face opposite the tongue entry face.

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner arrangement where timber1 is the tongue timber and timber2 is the fork timber.

required
tongue_thickness Optional[Numeric]

Tongue thickness along the shared plane normal. If None, defaults to 1/3 of the tongue timber dimension in that axis.

None
tongue_position Numeric

Offset of the tongue center from the tongue timber centerline along the shared plane normal. 0 means centered.

scalar(0)

Returns:

Type Description
Joint

Joint containing both cut timbers.

Raises:

Type Description
AssertionError

If timbers are not plane aligned, are parallel, or tongue parameters are out of bounds.

Source code in kumiki/joints/workshop/corner/tongue_and_fork_corner_joint.py
def cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers(
    arrangement: CornerJointTimberArrangement,
    tongue_thickness: Optional[Numeric] = None,
    tongue_position: Numeric = scalar(0),
) -> Joint:
    """
    Creates a plain tongue-and-fork corner joint (corner bridle style).
    Note, you can make an identical joint with cut_practice_mortise_and_tenon_corner_joint_on_plane_aligned_timbers setting tenon_distance_from_end to 0

    In this joint, timber1 forms the tongue (material removed on both cheeks), and timber2
    forms the fork (slot cut into its end). Tongue thickness is measured along the shared
    plane normal between the two timbers and defaults to one-third of the tongue timber
    dimension in that axis. Tongue position is an offset from the tongue timber centerline
    in that same axis.

    End cuts are aligned to the opposing timber's opposite face:
    - Tongue timber end is cut to the face opposite the fork entry face.
    - Fork timber end is cut to the face opposite the tongue entry face.

    Args:
        arrangement: Corner arrangement where timber1 is the tongue timber and timber2 is
            the fork timber.
        tongue_thickness: Tongue thickness along the shared plane normal. If None, defaults
            to 1/3 of the tongue timber dimension in that axis.
        tongue_position: Offset of the tongue center from the tongue timber centerline along
            the shared plane normal. 0 means centered.

    Returns:
        Joint containing both cut timbers.

    Raises:
        AssertionError: If timbers are not plane aligned, are parallel, or tongue parameters
            are out of bounds.
    """
    error = arrangement.check_plane_aligned()
    assert error is None, error

    tongue_timber = arrangement.timber1
    fork_timber = arrangement.timber2
    tongue_end = arrangement.timber1_end
    fork_end = arrangement.timber2_end

    warn_if_arrangement_timbers_imperfect(arrangement)

    assert not are_vectors_parallel(
        tongue_timber.get_length_direction_global(),
        fork_timber.get_length_direction_global(),
    ), "Timbers cannot be parallel for a tongue-and-fork corner joint"

    # -------------------------------------------------------------------------
    # Tongue geometry: shared plane normal, thickness, width
    # -------------------------------------------------------------------------
    shared_plane_normal_hint = arrangement.compute_normalized_timber_cross_product()
    tongue_normal_face = tongue_timber.get_closest_oriented_long_face_from_global_direction(shared_plane_normal_hint)
    tongue_normal_direction = tongue_timber.get_face_direction_global(tongue_normal_face)

    tongue_normal_dimension = tongue_timber.get_size_in_face_normal_axis(tongue_normal_face)
    if tongue_thickness is None:
        tongue_thickness = tongue_normal_dimension / scalar(3)

    assert safe_compare(tongue_thickness, 0, Comparison.GT), "tongue_thickness must be greater than 0"
    assert safe_compare(tongue_normal_dimension - tongue_thickness, 0, Comparison.GE), (
        "tongue_thickness must be <= the tongue timber size in the shared plane normal axis"
    )

    half_tongue_dimension = tongue_normal_dimension / scalar(2)
    half_tongue_thickness = tongue_thickness / scalar(2)
    assert safe_compare(half_tongue_dimension - (Abs(tongue_position) + half_tongue_thickness), 0, Comparison.GE), (
        "tongue_position and tongue_thickness place the tongue outside the tongue timber boundary"
    )

    tongue_normal_axis_index = tongue_timber.get_size_index_in_long_face_normal_axis(tongue_normal_face)
    tongue_width_axis_index = 1 if tongue_normal_axis_index == 0 else 0
    tongue_width = tongue_timber.size[tongue_width_axis_index]

    tongue_end_direction = tongue_timber.get_face_direction_global(tongue_end)
    fork_end_direction = fork_timber.get_face_direction_global(fork_end)

    # -------------------------------------------------------------------------
    # Shoulder plane (M&T pattern): compute on fork timber, mark onto tongue
    # -------------------------------------------------------------------------
    butt_arrangement_for_shoulder = ButtJointTimberArrangement(
        receiving_timber=fork_timber,
        butt_timber=tongue_timber,
        butt_timber_end=tongue_end,
    )
    fork_entry_long_face = fork_timber.get_closest_oriented_long_face_from_global_direction(-tongue_end_direction)
    fork_shoulder_distance = fork_timber.get_size_in_face_normal_axis(fork_entry_long_face) / scalar(2)

    shoulder_plane = locate_mortise_timber_shoulder_plane_from_centerline_towards_tenon_timber(
        butt_arrangement_for_shoulder, fork_shoulder_distance
    )
    shoulder_from_tongue_end_mark = mark_distance_from_end_along_centerline(
        shoulder_plane, tongue_timber, tongue_end
    )
    shoulder_point_global = shoulder_from_tongue_end_mark.locate().position

    # -------------------------------------------------------------------------
    # Marking space at shoulder (M&T pattern)
    # -------------------------------------------------------------------------
    marking_origin_global = shoulder_point_global + tongue_normal_direction * tongue_position

    tongue_orientation_global = Orientation.from_z_and_y(
        z_direction=safe_normalize_vector(tongue_end_direction),
        y_direction=safe_normalize_vector(tongue_normal_direction),
    )
    marking_space_transform = Transform(position=marking_origin_global, orientation=tongue_orientation_global)
    marking_space = Space(transform=marking_space_transform)

    # -------------------------------------------------------------------------
    # Tongue prism and shoulder half-space (M&T pattern)
    # -------------------------------------------------------------------------
    tongue_back_extension = max(tongue_timber.size[0], tongue_timber.size[1])
    tongue_prism_global = RectangularPrism(
        size=create_v2(tongue_width, tongue_thickness),
        transform=marking_space.transform,
        start_distance=-tongue_back_extension,
        end_distance=tongue_timber.length,
        label=CutCSGLabel("tongue"),
        # The tongue itself: the waste is everything past the shoulder except this
        # prism, so the surfaces it leaves are the tongue's. Only the two cheeks
        # are named: the tongue spans the timber's full width, so its other two
        # faces are flush with the timber's own and belong to those names --
        # naming them as well derives the same line twice (see
        # _drop_duplicate_derived).
        feature_overrides=[
            FeatureOverride(prism_face_key(PrismFace.FRONT), "tongue_front", measured_against(_TONGUE_FACE, "tongue_back")),
            FeatureOverride(prism_face_key(PrismFace.BACK), "tongue_back", measured_against(_TONGUE_FACE, "tongue_front")),
        ],
    )

    shoulder_half_space_global = HalfSpace(
        normal=-shoulder_plane.normal,
        offset=safe_dot_product(-shoulder_plane.normal, marking_space.transform.position),
        label=CutCSGLabel("shoulder"),
        feature_overrides=[shoulder_override(HALF_SPACE_PLANE, name="shoulder",
                                             group=FeatureGroup.SHOULDER_PLANE)],
    )

    tongue_prism_local = adopt_csg(None, tongue_timber.transform, tongue_prism_global)
    shoulder_half_space_local = adopt_csg(None, tongue_timber.transform, shoulder_half_space_global)

    tongue_negative_csg = Difference(
        base=shoulder_half_space_local,
        subtract=[tongue_prism_local],
        label=CutCSGLabel("tongue_waste"),
    )

    # -------------------------------------------------------------------------
    # Fork slot: depth extends to the fork's far face so the slot matches
    # the tongue after its end-cut extension (same orientation as tongue)
    # -------------------------------------------------------------------------
    fork_entry_long_face_for_end_cut = fork_timber.get_closest_oriented_long_face_from_global_direction(-tongue_end_direction)
    fork_far_face = fork_entry_long_face_for_end_cut.to.face().get_opposite_face()
    fork_far_face_normal_global = fork_timber.get_face_direction_global(fork_far_face)
    fork_far_face_point_global = get_center_point_on_face_global(fork_far_face, fork_timber)

    fork_slot_depth = safe_dot_product(
        fork_far_face_point_global - shoulder_point_global,
        safe_normalize_vector(tongue_end_direction),
    )
    assert safe_compare(fork_slot_depth, 0, Comparison.GT), (
        "Fork slot depth must be > 0; check timber arrangement and end selections"
    )

    fork_slot_back_extension = max(fork_timber.size[0], fork_timber.size[1]) * scalar(2)
    fork_slot_prism_global = RectangularPrism(
        size=create_v2(tongue_width, tongue_thickness),
        transform=marking_space.transform,
        start_distance=-fork_slot_back_extension,
        end_distance=fork_slot_depth,
        label=CutCSGLabel("fork_slot"),
    )
    fork_negative_csg = adopt_csg(None, fork_timber.transform, fork_slot_prism_global)

    # -------------------------------------------------------------------------
    # End cuts: each timber is cut to align with the far face of the opposing timber
    # -------------------------------------------------------------------------
    tongue_end_hs_normal_global = (
        fork_far_face_normal_global
        if safe_dot_product(fork_far_face_normal_global, tongue_end_direction) > 0
        else -fork_far_face_normal_global
    )
    tongue_end_cut_local_normal = safe_transform_vector(
        tongue_timber.orientation.matrix.T, tongue_end_hs_normal_global
    )
    tongue_end_cut_local_offset = (
        safe_dot_product(tongue_end_hs_normal_global, fork_far_face_point_global)
        - safe_dot_product(tongue_end_hs_normal_global, tongue_timber.get_bottom_position_global())
    )
    tongue_end_cut = HalfSpace(
        normal=tongue_end_cut_local_normal,
        offset=tongue_end_cut_local_offset,
        label=CutCSGLabel("tongue_end_cut"),
    )

    tongue_entry_long_face = tongue_timber.get_closest_oriented_long_face_from_global_direction(-fork_end_direction)
    tongue_far_face = tongue_entry_long_face.to.face().get_opposite_face()
    tongue_far_face_normal_global = tongue_timber.get_face_direction_global(tongue_far_face)
    tongue_far_face_point_global = get_center_point_on_face_global(tongue_far_face, tongue_timber)

    fork_end_hs_normal_global = (
        tongue_far_face_normal_global
        if safe_dot_product(tongue_far_face_normal_global, fork_end_direction) > 0
        else -tongue_far_face_normal_global
    )
    fork_end_cut_local_normal = safe_transform_vector(
        fork_timber.orientation.matrix.T, fork_end_hs_normal_global
    )
    fork_end_cut_local_offset = (
        safe_dot_product(fork_end_hs_normal_global, tongue_far_face_point_global)
        - safe_dot_product(fork_end_hs_normal_global, fork_timber.get_bottom_position_global())
    )
    fork_end_cut = HalfSpace(
        normal=fork_end_cut_local_normal,
        offset=fork_end_cut_local_offset,
        label=CutCSGLabel("fork_end_cut"),
    )

    tongue_end_cut_distance_from_bottom = safe_dot_product(
        fork_far_face_point_global - tongue_timber.get_bottom_position_global(),
        tongue_timber.get_length_direction_global(),
    )
    fork_end_cut_distance_from_bottom = safe_dot_product(
        tongue_far_face_point_global - fork_timber.get_bottom_position_global(),
        fork_timber.get_length_direction_global(),
    )

    tongue_negative_parts: list[CutCSG] = [tongue_negative_csg, tongue_end_cut]
    fork_negative_parts: list[CutCSG] = [fork_negative_csg, fork_end_cut]

    tongue_cut = Cutting(
        timber=tongue_timber,
        maybe_top_end_cut_distance_from_bottom=tongue_end_cut_distance_from_bottom if tongue_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=tongue_end_cut_distance_from_bottom if tongue_end == TimberEnd.BOTTOM else None,
        negative_csg=CSGUnion(children=tongue_negative_parts),
        label=CutCSGLabel("tongue_cut"),
        assembly_freedom=AssemblyFreedom.combine(
            AssemblyFreedom.translation(
                -tongue_end_direction,
                freed_after=fork_timber.get_size_in_direction_3d(tongue_end_direction),
            ),
            AssemblyFreedom.translation(
                fork_end_direction,
                freed_after=fork_timber.get_size_in_direction_3d(fork_end_direction),
            ),
        ),
    )

    fork_cut = Cutting(
        timber=fork_timber,
        maybe_top_end_cut_distance_from_bottom=fork_end_cut_distance_from_bottom if fork_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=fork_end_cut_distance_from_bottom if fork_end == TimberEnd.BOTTOM else None,
        negative_csg=CSGUnion(children=fork_negative_parts),
        label=CutCSGLabel("fork_cut"),
        assembly_freedom=AssemblyFreedom.combine(
            AssemblyFreedom.translation(
                tongue_end_direction,
                freed_after=fork_timber.get_size_in_direction_3d(tongue_end_direction),
            ),
            AssemblyFreedom.translation(
                -fork_end_direction,
                freed_after=fork_timber.get_size_in_direction_3d(fork_end_direction),
            ),
        ),
    )

    return Joint(
        cuttings={
            "tongue_timber": tongue_cut,
            "fork_timber": fork_cut,
        },
        ticket=JointTicket(joint_type="tongue_and_fork_corner"),
        jointAccessories={},
    )

cut_plain_tongue_and_fork_joint_on_plane_aligned_timbers

cut_plain_tongue_and_fork_joint_on_plane_aligned_timbers(arrangement: CornerJointTimberArrangement, tongue_thickness: Optional[Numeric] = None, tongue_position: Numeric = scalar(0)) -> Joint

Compatibility alias for cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers.

Source code in kumiki/joints/workshop/corner/tongue_and_fork_corner_joint.py
def cut_plain_tongue_and_fork_joint_on_plane_aligned_timbers(
    arrangement: CornerJointTimberArrangement,
    tongue_thickness: Optional[Numeric] = None,
    tongue_position: Numeric = scalar(0),
) -> Joint:
    """Compatibility alias for `cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers`."""
    return cut_tongue_and_fork_corner_joint_on_plane_aligned_timbers(
        arrangement=arrangement,
        tongue_thickness=tongue_thickness,
        tongue_position=tongue_position,
    )

cut_plain_corner_lap_joint_on_plane_aligned_timbers

cut_plain_corner_lap_joint_on_plane_aligned_timbers(arrangement: CornerJointTimberArrangement, cut_ratio: Numeric = scalar(1, 2)) -> Joint

Creates a corner-lap joint between two corner timbers with trimmed ends.

Geometry is the same lap construction as cut_plain_cross_lap_joint, but applied to corner-arranged timbers and augmented with one end cut per timber so neither member extends past the corner.

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner-joint arrangement with timber1, timber2, timber1_end, timber2_end, and optional front_face_on_timber1.

required
cut_ratio Numeric

Fraction [0, 1] controlling how much lap material is removed from each timber (same semantics as cross lap).

scalar(1, 2)

Returns:

Type Description
Joint

Joint object containing the two CutTimbers.

Raises:

Type Description
AssertionError

If timbers are not plane-aligned.

Source code in kumiki/joints/workshop/corner/plain_corner_lap_joint.py
def cut_plain_corner_lap_joint_on_plane_aligned_timbers(
    arrangement: CornerJointTimberArrangement,
    cut_ratio: Numeric = scalar(1, 2),
) -> Joint:
    """
    Creates a corner-lap joint between two corner timbers with trimmed ends.

    Geometry is the same lap construction as `cut_plain_cross_lap_joint`, but applied
    to corner-arranged timbers and augmented with one end cut per timber so neither
    member extends past the corner.

    Args:
        arrangement: Corner-joint arrangement with timber1, timber2, timber1_end,
            timber2_end, and optional front_face_on_timber1.
        cut_ratio: Fraction [0, 1] controlling how much lap material is removed
            from each timber (same semantics as cross lap).

    Returns:
        Joint object containing the two CutTimbers.

    Raises:
        AssertionError: If timbers are not plane-aligned.
    """
    error = arrangement.check_plane_aligned()
    assert error is None, error

    timberA = arrangement.timber1
    timberB = arrangement.timber2
    timberA_end = arrangement.timber1_end
    timberB_end = arrangement.timber2_end

    # Reuse cross-lap geometry for the lap volume split.
    cross_lap_joint = cut_plain_cross_lap_joint(
        CrossJointTimberArrangement(
            timber1=timberA,
            timber2=timberB,
            front_face_on_timber1=arrangement.front_face_on_timber1,
        ),
        cut_ratio=cut_ratio,
    )

    timberA_end_direction = timberA.get_face_direction_global(timberA_end)
    timberB_end_direction = timberB.get_face_direction_global(timberB_end)

    timberB_entry_face = timberB.get_closest_oriented_face_from_global_direction(-timberA_end_direction)
    timberB_far_face = timberB_entry_face.get_opposite_face()
    timberB_far_face_point_global = get_center_point_on_face_global(timberB_far_face, timberB)

    timberA_entry_face = timberA.get_closest_oriented_face_from_global_direction(-timberB_end_direction)
    timberA_far_face = timberA_entry_face.get_opposite_face()
    timberA_far_face_point_global = get_center_point_on_face_global(timberA_far_face, timberA)

    timberA_end_cut_distance_from_bottom = safe_dot_product(
        timberB_far_face_point_global - timberA.get_bottom_position_global(),
        timberA.get_length_direction_global(),
    )
    timberB_end_cut_distance_from_bottom = safe_dot_product(
        timberA_far_face_point_global - timberB.get_bottom_position_global(),
        timberB.get_length_direction_global(),
    )

    cross_lap_cutA = cross_lap_joint.cuttings["timberA"]
    cross_lap_cutB = cross_lap_joint.cuttings["timberB"]

    cutA = Cutting(
        timber=timberA,
        maybe_top_end_cut_distance_from_bottom=(
            timberA_end_cut_distance_from_bottom
            if timberA_end == TimberEnd.TOP
            else cross_lap_cutA.maybe_top_end_cut_distance_from_bottom
        ),
        maybe_bottom_end_cut_distance_from_bottom=(
            timberA_end_cut_distance_from_bottom
            if timberA_end == TimberEnd.BOTTOM
            else cross_lap_cutA.maybe_bottom_end_cut_distance_from_bottom
        ),
        negative_csg=cross_lap_cutA.negative_csg,
        label=cross_lap_cutA.label,
        assembly_freedom=cross_lap_cutA.assembly_freedom,
    )
    cutB = Cutting(
        timber=timberB,
        maybe_top_end_cut_distance_from_bottom=(
            timberB_end_cut_distance_from_bottom
            if timberB_end == TimberEnd.TOP
            else cross_lap_cutB.maybe_top_end_cut_distance_from_bottom
        ),
        maybe_bottom_end_cut_distance_from_bottom=(
            timberB_end_cut_distance_from_bottom
            if timberB_end == TimberEnd.BOTTOM
            else cross_lap_cutB.maybe_bottom_end_cut_distance_from_bottom
        ),
        negative_csg=cross_lap_cutB.negative_csg,
        label=cross_lap_cutB.label,
        assembly_freedom=cross_lap_cutB.assembly_freedom,
    )

    return Joint(
        cuttings={
            "timberA": cutA,
            "timberB": cutB,
        },
        ticket=JointTicket(joint_type="plain_corner_lap"),
        jointAccessories={},
    )

cut_mitered_and_keyed_lap_joint_on_plane_aligned_timbers

cut_mitered_and_keyed_lap_joint_on_plane_aligned_timbers(arrangement: CornerJointTimberArrangement, lap_thickness: Optional[Numeric] = None, lap_start_distance_from_reference_miter_face: Optional[Numeric] = None, distance_between_lap_and_outside: Optional[Numeric] = None, num_laps: int = 2, key_width: Optional[Numeric] = None, key_thickness: Optional[Numeric] = None) -> Joint

Creates a mitered and keyed lap joint (箱相欠き車知栓仕口 / Hako Aikaki Shachi Sen Shikuchi) between two timbers.

This is a traditional Japanese timber joint that combines a miter joint with interlocking finger laps on the inside of the miter for additional mechanical strength.

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner joint arrangement where timber1 and timber2 are the joined timbers, timber1_end and timber2_end are the joined ends, and front_face_on_timber1 defines the reference miter face on timber1.

required
lap_thickness Optional[Numeric]

Thickness of each lap/finger (optional, auto-calculated if None)

None
lap_start_distance_from_reference_miter_face Optional[Numeric]

Distance from miter face to first lap (optional)

None
distance_between_lap_and_outside Optional[Numeric]

Inset distance from outer face (optional)

None
num_laps int

Number of interlocking laps/fingers (minimum 2)

2
key_width Optional[Numeric]

Width of each key measured along the diagonal direction. If None, defaults to lap_thickness.

None
key_thickness Optional[Numeric]

Thickness of each key (the narrow dimension). If None, defaults to lap_thickness / 3.

None

Returns:

Type Description
Joint

Joint object containing the two CutTimbers with miter and finger cuts applied

Raises:

Type Description
ValueError

If parameters are invalid or timbers are not properly positioned

Source code in kumiki/joints/workshop/corner/mitered_and_keyed_lap_joint.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def cut_mitered_and_keyed_lap_joint_on_plane_aligned_timbers(
    arrangement: CornerJointTimberArrangement,
    lap_thickness: Optional[Numeric] = None,
    lap_start_distance_from_reference_miter_face: Optional[Numeric] = None,
    distance_between_lap_and_outside: Optional[Numeric] = None,
    num_laps: int = 2,
    key_width: Optional[Numeric] = None,
    key_thickness: Optional[Numeric] = None,
) -> Joint:
    """
    Creates a mitered and keyed lap joint (箱相欠き車知栓仕口 / Hako Aikaki Shachi Sen Shikuchi)
    between two timbers.

    This is a traditional Japanese timber joint that combines a miter joint with interlocking
    finger laps on the inside of the miter for additional mechanical strength.

    Args:
        arrangement: Corner joint arrangement where timber1 and timber2 are the joined
            timbers, timber1_end and timber2_end are the joined ends, and
            front_face_on_timber1 defines the reference miter face on timber1.
        lap_thickness: Thickness of each lap/finger (optional, auto-calculated if None)
        lap_start_distance_from_reference_miter_face: Distance from miter face to first lap (optional)
        distance_between_lap_and_outside: Inset distance from outer face (optional)
        num_laps: Number of interlocking laps/fingers (minimum 2)
        key_width: Width of each key measured along the diagonal direction. If None,
            defaults to lap_thickness.
        key_thickness: Thickness of each key (the narrow dimension). If None,
            defaults to lap_thickness / 3.

    Returns:
        Joint object containing the two CutTimbers with miter and finger cuts applied

    Raises:
        ValueError: If parameters are invalid or timbers are not properly positioned
    """
    require_check(arrangement.check_plane_aligned())
    warn_if_arrangement_timbers_imperfect(arrangement)
    assert arrangement.front_face_on_timber1 is not None, (
        "arrangement.front_face_on_timber1 must be set to determine the reference miter face"
    )
    timberA = arrangement.timber1
    timberA_end = arrangement.timber1_end
    timberA_reference_miter_face = arrangement.front_face_on_timber1
    timberB = arrangement.timber2
    timberB_end = arrangement.timber2_end

    # Step 1: Parameter validation and find matching miter faces
    if num_laps < 2:
        raise ValueError(f"num_laps must be at least 2, got {num_laps}")

    timberA_miter_face_normal = timberA.get_face_direction_global(timberA_reference_miter_face)
    timberB_reference_miter_face_enum = timberB.get_closest_oriented_face_from_global_direction(
        timberA_miter_face_normal
    )

    if timberB_reference_miter_face_enum == TimberFace.RIGHT:
        timberB_reference_miter_face = TimberLongFace.RIGHT
    elif timberB_reference_miter_face_enum == TimberFace.LEFT:
        timberB_reference_miter_face = TimberLongFace.LEFT
    elif timberB_reference_miter_face_enum == TimberFace.FRONT:
        timberB_reference_miter_face = TimberLongFace.FRONT
    elif timberB_reference_miter_face_enum == TimberFace.BACK:
        timberB_reference_miter_face = TimberLongFace.BACK
    else:
        raise ValueError(
            f"timberB matching face must be a long face (not TOP or BOTTOM), got {timberB_reference_miter_face_enum}"
        )

    timberB_miter_face_normal = timberB.get_face_direction_global(timberB_reference_miter_face)
    if not are_vectors_parallel(timberA_miter_face_normal, timberB_miter_face_normal):
        raise ValueError(
            f"Miter face normals must be parallel. "
            f"timberA face normal: {timberA_miter_face_normal.T}, "
            f"timberB face normal: {timberB_miter_face_normal.T}"
        )

    # Step 2: Calculate and validate angle between timbers
    if timberA_end == TimberEnd.TOP:
        directionA = timberA.get_length_direction_global()
    else:
        directionA = -timberA.get_length_direction_global()

    if timberB_end == TimberEnd.TOP:
        directionB = timberB.get_length_direction_global()
    else:
        directionB = -timberB.get_length_direction_global()

    dot_product = safe_dot_product(safe_normalize_vector(directionA), safe_normalize_vector(directionB))
    dot_product_clamped = Max(scalar(-1), Min(scalar(1), dot_product))
    angle = acos(dot_product_clamped)

    min_angle = radians(pi / scalar(4))  # 45 degrees
    max_angle = radians(scalar(3) * pi / scalar(4))  # 135 degrees

    if angle < min_angle or angle > max_angle:
        raise ValueError(
            f"Angle between timbers must be between 45° and 135°, "
            f"got {float(angle * 180 / pi):.1f}°"
        )

    # Step 3: Calculate dimensions and default values
    miter_face_depth = timberA.get_size_in_face_normal_axis(timberA_reference_miter_face.to.face())
    miter_face_width = timberA.get_size_in_face_normal_axis(timberA_reference_miter_face.rotate_right().to.face())

    if lap_thickness is None and lap_start_distance_from_reference_miter_face is None:
        lap_thickness_final = miter_face_depth / (num_laps + scalar(2))
        lap_start_distance_final = lap_thickness_final
    elif lap_thickness is None:
        assert lap_start_distance_from_reference_miter_face is not None
        remaining_depth: Numeric = miter_face_depth - lap_start_distance_from_reference_miter_face
        lap_thickness_final = remaining_depth / scalar(num_laps + 1)
        lap_start_distance_final = lap_start_distance_from_reference_miter_face
    elif lap_start_distance_from_reference_miter_face is None:
        total_lap_depth: Numeric = lap_thickness * num_laps
        lap_start_distance_final = (miter_face_depth - total_lap_depth) / scalar(2)
        lap_thickness_final = lap_thickness
    else:
        lap_thickness_final = lap_thickness
        lap_start_distance_final = lap_start_distance_from_reference_miter_face

    if distance_between_lap_and_outside is None:
        distance_between_lap_and_outside = miter_face_width * scalar(1, 5)

    # Step 4: Validate fit
    total_lap_depth = lap_start_distance_final + lap_thickness_final * num_laps
    if total_lap_depth >= miter_face_depth:
        raise ValueError(
            f"Laps do not fit in timber depth. "
            f"Total lap depth: {float(total_lap_depth):.3f}, "
            f"Miter face depth: {float(miter_face_depth):.3f}"
        )

    timberB_miter_face_depth = timberB.get_size_in_face_normal_axis(timberB_reference_miter_face.to.face())
    if total_lap_depth >= timberB_miter_face_depth:
        raise ValueError(
            f"Laps do not fit on timberB. "
            f"Total lap depth: {float(total_lap_depth):.3f}, "
            f"TimberB miter face depth: {float(timberB_miter_face_depth):.3f}"
        )

    # Step 5: Find inner face normals and inner shoulder direction
    diagonal_direction = safe_normalize_vector(directionA + directionB)
    negative_diagonal = -diagonal_direction
    timberA_inner_face_enum = timberA.get_closest_oriented_long_face_from_global_direction(negative_diagonal).to.face()
    timberB_inner_face_enum = timberB.get_closest_oriented_long_face_from_global_direction(negative_diagonal).to.face()

    timberA_inner_face_normal = timberA.get_face_direction_global(timberA_inner_face_enum)
    timberB_inner_face_normal = timberB.get_face_direction_global(timberB_inner_face_enum)

    inner_shoulder_direction = cross_product(timberA_inner_face_normal, timberB_inner_face_normal)
    inner_shoulder_direction = safe_normalize_vector(inner_shoulder_direction)

    timberA_inner_face_size = timberA.get_size_in_face_normal_axis(timberA_inner_face_enum)
    timberB_inner_face_size = timberB.get_size_in_face_normal_axis(timberB_inner_face_enum)
    if not safe_zero_test(timberA_inner_face_size - timberB_inner_face_size):
        raise ValueError(
            f"Timber widths in the miter plane are not the same. "
            f"TimberA size: {float(timberA_inner_face_size):.3f}, "
            f"TimberB size: {float(timberB_inner_face_size):.3f}"
        )

    # Step 6: Create marking transform on timberA
    timberB_centerline = locate_centerline(timberB)
    centerline_marking = mark_distance_from_end_along_centerline(timberB_centerline, timberA, end=TimberEnd.BOTTOM)

    marking_position = timberA.get_bottom_position_global()
    marking_position = marking_position + timberA.get_length_direction_global() * centerline_marking.distance
    marking_position_on_centerline = marking_position + timberA_miter_face_normal * (
        -timberA.get_size_in_face_normal_axis(timberA_reference_miter_face.to.face()) / scalar(2)
    )

    cos_angle = safe_dot_product(directionA, directionB)
    timber_angle = acos(cos_angle)
    half_angle = timber_angle / scalar(2)
    diagonal_scale_factor = scalar(1) / sin(half_angle)

    inner_edge_offset = timberA.get_size_in_face_normal_axis(timberA_inner_face_enum) / scalar(2)
    diagonal_offset = inner_edge_offset * diagonal_scale_factor
    marking_position = marking_position_on_centerline - diagonal_direction * diagonal_offset

    if timberA_end == TimberEnd.TOP:
        marking_z = timberA.get_length_direction_global()
    else:
        marking_z = -timberA.get_length_direction_global()

    marking_x = inner_shoulder_direction
    marking_y = cross_product(marking_z, marking_x)
    marking_y = safe_normalize_vector(marking_y)
    marking_x = cross_product(marking_y, marking_z)
    marking_x = safe_normalize_vector(marking_x)

    # Step 7: Generate finger prisms
    finger_length = lap_thickness_final
    finger_size_x = miter_face_width
    finger_size_y = miter_face_width * tan(half_angle)
    finger_size = create_v2(finger_size_x, finger_size_y)

    all_fingers_global = []

    for i in range(num_laps):
        is_timber_a = (i % 2 == 0)
        z_offset = lap_start_distance_final + i * lap_thickness_final
        finger_position = marking_position_on_centerline + timberA_miter_face_normal * z_offset

        finger_z = timberA_miter_face_normal
        finger_x = timberA_inner_face_normal if is_timber_a else timberB_inner_face_normal
        finger_y = cross_product(finger_z, finger_x)
        finger_y = safe_normalize_vector(finger_y)

        finger_orientation = Orientation(Matrix([
            [finger_x[0], finger_y[0], finger_z[0]],
            [finger_x[1], finger_y[1], finger_z[1]],
            [finger_x[2], finger_y[2], finger_z[2]],
        ]))

        finger_transform_global = Transform(position=finger_position, orientation=finger_orientation)
        finger_prism_global = RectangularPrism(
            size=finger_size,
            transform=finger_transform_global,
            start_distance=scalar(0),
            end_distance=finger_length,
            label=CutCSGLabel("lap_finger"),
        )
        all_fingers_global.append((finger_prism_global, is_timber_a))

    A_finger_indices = [i for i in range(num_laps) if i % 2 == 0]
    B_finger_indices = [i for i in range(num_laps) if i % 2 == 1]

    def convert_finger_to_local(finger_global: RectangularPrism, target_timber: TimberLike, is_timber_a_finger: bool) -> CutCSG:
        if is_timber_a_finger:
            opposing_timber = timberB
            opposing_inner_face_enum = timberB_inner_face_enum
        else:
            opposing_timber = timberA
            opposing_inner_face_enum = timberA_inner_face_enum

        finger_transform_local = finger_global.transform.to_local_transform(target_timber.transform)
        finger_local = replace(finger_global, transform=finger_transform_local)

        crop_distance = miter_face_width - distance_between_lap_and_outside
        crop_plane = locate_into_face(crop_distance, opposing_inner_face_enum, opposing_timber)

        crop_point_local = target_timber.transform.global_to_local(crop_plane.point)
        crop_normal_local = safe_transform_vector(target_timber.orientation.matrix.T, crop_plane.normal)
        crop_offset_local = safe_dot_product(crop_point_local, crop_normal_local)

        crop_halfspace_local = HalfSpace(
            normal=-crop_normal_local,
            offset=-crop_offset_local,
            label=CutCSGLabel("finger_crop"),
        )

        return Difference(
            base=finger_local,
            subtract=[crop_halfspace_local],
            label=CutCSGLabel("lap_finger_cropped"),
        )

    A_fingers_in_timberA = [
        convert_finger_to_local(all_fingers_global[idx][0], timberA, all_fingers_global[idx][1])
        for idx in A_finger_indices
    ]
    B_fingers_in_timberA = [
        convert_finger_to_local(all_fingers_global[idx][0], timberA, all_fingers_global[idx][1])
        for idx in B_finger_indices
    ]
    A_fingers_in_timberB = [
        convert_finger_to_local(all_fingers_global[idx][0], timberB, all_fingers_global[idx][1])
        for idx in A_finger_indices
    ]
    B_fingers_in_timberB = [
        convert_finger_to_local(all_fingers_global[idx][0], timberB, all_fingers_global[idx][1])
        for idx in B_finger_indices
    ]

    # Step 7.5: Create Keys
    if key_width is None:
        key_width = lap_thickness_final
    if key_thickness is None:
        key_thickness = lap_thickness_final / scalar(3)

    key_depth = miter_face_width / sin(half_angle) - distance_between_lap_and_outside / sin(half_angle)

    keys_in_timberA = []
    keys_in_timberB = []
    key_wedges = []

    num_keys = num_laps - 1
    for i in range(num_keys):
        key_position = marking_position
        z_offset = lap_start_distance_final + (i + 1) * lap_thickness_final
        key_position = key_position + timberA_miter_face_normal * z_offset

        key_orientation_before_rotation = Orientation.from_z_and_y(
            z_direction=diagonal_direction,
            y_direction=-timberA_miter_face_normal,
        )

        key_rotation_sign = -1 if i % 2 == 0 else 1
        key_rotation_angle = key_rotation_sign * atan2(key_thickness, key_width)
        rotation_for_key = Orientation.from_axis_angle(diagonal_direction, radians(key_rotation_angle))
        key_orientation = rotation_for_key * key_orientation_before_rotation
        key_transform_global = Transform(position=key_position, orientation=key_orientation)

        key_size = create_v2(key_width, key_thickness)
        key_prism_global = RectangularPrism(
            size=key_size,
            transform=key_transform_global,
            start_distance=-key_depth,
            end_distance=key_depth,
            label=CutCSGLabel("key_slot"),
        )

        key_transform_local_A = key_transform_global.to_local_transform(timberA.transform)
        key_prism_in_timberA = replace(key_prism_global, transform=key_transform_local_A)
        keys_in_timberA.append(key_prism_in_timberA)

        key_transform_local_B = key_transform_global.to_local_transform(timberB.transform)
        key_prism_in_timberB = replace(key_prism_global, transform=key_transform_local_B)
        keys_in_timberB.append(key_prism_in_timberB)

        key_wedge = Wedge(
            transform=key_transform_global,
            base_width=key_width,
            tip_width=key_width,
            height=key_thickness,
            length=key_depth,
            stickout_length=key_depth * scalar(1, 4),
            assembly_freedom=AssemblyFreedom.translation(-diagonal_direction, freed_after=key_depth * scalar(2)),
            assembly_ordering=Ordering(0, -1),
        )
        key_wedges.append(key_wedge)

    # Step 8: Create rough end cuts and miter half plane cuts
    if timberA_end == TimberEnd.TOP:
        rough_cut_position_A = marking_position + timberA.get_length_direction_global() * finger_size_y
        rough_cut_normal_A_global = timberA.get_length_direction_global()
    else:
        rough_cut_position_A = marking_position - timberA.get_length_direction_global() * finger_size_y
        rough_cut_normal_A_global = -timberA.get_length_direction_global()

    local_normal_A_rough = safe_transform_vector(timberA.orientation.matrix.T, rough_cut_normal_A_global)
    local_offset_A_rough = safe_dot_product(rough_cut_position_A, rough_cut_normal_A_global) - safe_dot_product(
        rough_cut_normal_A_global, timberA.get_bottom_position_global()
    )
    rough_end_cut_A = HalfSpace(normal=local_normal_A_rough, offset=local_offset_A_rough)

    if timberB_end == TimberEnd.TOP:
        rough_cut_position_B = marking_position + timberB.get_length_direction_global() * finger_size_y
        rough_cut_normal_B_global = timberB.get_length_direction_global()
    else:
        rough_cut_position_B = marking_position - timberB.get_length_direction_global() * finger_size_y
        rough_cut_normal_B_global = -timberB.get_length_direction_global()

    local_normal_B_rough = safe_transform_vector(timberB.orientation.matrix.T, rough_cut_normal_B_global)
    local_offset_B_rough = safe_dot_product(rough_cut_position_B, rough_cut_normal_B_global) - safe_dot_product(
        rough_cut_normal_B_global, timberB.get_bottom_position_global()
    )
    rough_end_cut_B = HalfSpace(normal=local_normal_B_rough, offset=local_offset_B_rough)

    intersection_point = marking_position
    normA = safe_normalize_vector(directionA)
    normB = safe_normalize_vector(directionB)
    bisector = safe_normalize_vector(normA + normB)
    plane_normal = cross_product(normA, normB)
    miter_normal = safe_normalize_vector(cross_product(bisector, plane_normal))

    dot_A = safe_dot_product(normA, miter_normal)
    normalA = miter_normal if safe_compare(dot_A, 0, Comparison.GT) else -miter_normal
    local_normalA = safe_transform_vector(timberA.orientation.matrix.T, normalA)
    local_offsetA = safe_dot_product(intersection_point, normalA) - safe_dot_product(
        normalA, timberA.get_bottom_position_global()
    )

    dot_B = safe_dot_product(normB, miter_normal)
    normalB = miter_normal if safe_compare(dot_B, 0, Comparison.GT) else -miter_normal
    local_normalB = safe_transform_vector(timberB.orientation.matrix.T, normalB)
    local_offsetB = safe_dot_product(intersection_point, normalB) - safe_dot_product(
        normalB, timberB.get_bottom_position_global()
    )

    miter_half_plane_A = HalfSpace(normal=local_normalA, offset=local_offsetA, label=CutCSGLabel("miter_plane"))
    miter_half_plane_B = HalfSpace(normal=local_normalB, offset=local_offsetB, label=CutCSGLabel("miter_plane"))

    # Step 9: Combine cuts and return Joint
    if A_fingers_in_timberA:
        miter_cut_A_with_fingers = Difference(
            miter_half_plane_A, A_fingers_in_timberA, label=CutCSGLabel("miter_waste")
        )
    else:
        miter_cut_A_with_fingers = miter_half_plane_A

    voids_in_A = []
    if B_fingers_in_timberA:
        voids_in_A.extend(B_fingers_in_timberA)
    if keys_in_timberA:
        voids_in_A.extend(keys_in_timberA)

    if voids_in_A:
        negative_csg_A = CSGUnion([miter_cut_A_with_fingers] + voids_in_A)
    else:
        negative_csg_A = miter_cut_A_with_fingers

    if B_fingers_in_timberB:
        miter_cut_B_with_fingers = Difference(
            miter_half_plane_B, B_fingers_in_timberB, label=CutCSGLabel("miter_waste")
        )
    else:
        miter_cut_B_with_fingers = miter_half_plane_B

    voids_in_B = []
    if A_fingers_in_timberB:
        voids_in_B.extend(A_fingers_in_timberB)
    if keys_in_timberB:
        voids_in_B.extend(keys_in_timberB)

    if voids_in_B:
        negative_csg_B = CSGUnion([miter_cut_B_with_fingers] + voids_in_B)
    else:
        negative_csg_B = miter_cut_B_with_fingers

    rough_end_cut_A_z = (
        rough_end_cut_A.offset if timberA_end == TimberEnd.TOP else -rough_end_cut_A.offset
    )
    rough_end_cut_B_z = (
        rough_end_cut_B.offset if timberB_end == TimberEnd.TOP else -rough_end_cut_B.offset
    )

    cutA = Cutting(
        timber=timberA,
        maybe_top_end_cut_distance_from_bottom=rough_end_cut_A_z if timberA_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=rough_end_cut_A_z if timberA_end == TimberEnd.BOTTOM else None,
        negative_csg=negative_csg_A,
        label=CutCSGLabel("mitered_lap_cut"),
        assembly_freedom=AssemblyFreedom.translation(
            -directionA, freed_after=timberB.get_size_in_direction_3d(directionA)
        ),
        assembly_ordering=Ordering(0, 0),
    )

    cutB = Cutting(
        timber=timberB,
        maybe_top_end_cut_distance_from_bottom=rough_end_cut_B_z if timberB_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=rough_end_cut_B_z if timberB_end == TimberEnd.BOTTOM else None,
        negative_csg=negative_csg_B,
        label=CutCSGLabel("mitered_lap_cut"),
        assembly_freedom=AssemblyFreedom.translation(
            -directionB, freed_after=timberA.get_size_in_direction_3d(directionB)
        ),
        assembly_ordering=Ordering(0, 0),
    )

    joint_accessories = {}
    for i, wedge in enumerate(key_wedges):
        joint_accessories[f"key_{i}"] = wedge

    return Joint(
        cuttings={
            timberA.ticket.path: cutA,
            timberB.ticket.path: cutB,
        },
        ticket=JointTicket(joint_type="mitered_and_keyed_lap"),
        jointAccessories=joint_accessories,
    )

cut_dovetail_corner_joint

cut_dovetail_corner_joint(arrangement: CornerJointTimberArrangement, distances: List[Numeric], dovetails: List[SingleDovetailSizeParameter]) -> Joint

Creates a dovetail corner joint: a row of interlocking dovetails across a corner.

timber1 keeps the dovetails and timber2 receives the matching sockets, so the two interlock through the corner the way a drawer front interlocks with a drawer side. Each dovetail starts at the shoulder -- the near face of timber2 -- and widens as it reaches depth into timber2, which locks timber1 against being drawn away from the corner. A dovetail whose depth is timber2's full thickness is a through dovetail and shows on timber2's outside face; a shallower one is half blind. Dovetails span timber1's whole thickness through the corner, so the joint assembles by sliding timber1 along timber2's length axis and nothing else.

Dovetails are laid out across timber1's outside face along the axis normal to front_face_on_timber1, measured into the timber from that face.

Both timbers are end cut to the far face of the other, so the corner reads flush on both outside faces.

Parameters:

Name Type Description Default
arrangement CornerJointTimberArrangement

Corner arrangement. timber1 carries the dovetails and timber2 the sockets; the timbers must be face aligned, orthogonal, and flush across the front_face_on_timber1 axis. front_face_on_timber1 must be set -- the dovetail layout is measured from it.

required
distances List[Numeric]

Distance to the center of each dovetail, the first measured from front_face_on_timber1 and each one after from the previous center.

required
dovetails List[SingleDovetailSizeParameter]

Size of each dovetail, one entry per entry in distances.

required

Returns:

Type Description
Joint

Joint object containing the two CutTimbers.

Raises:

Type Description
KumikiArrangementError

If the timbers are not face aligned and orthogonal, or front_face_on_timber1 does not point along the corner plane normal.

AssertionError

If the dovetail parameters do not fit the timbers, overlap each other, or run past either face of the layout axis.

Source code in kumiki/joints/workshop/corner/dovetail_corner_joint.py
def cut_dovetail_corner_joint(arrangement: CornerJointTimberArrangement, distances: List[Numeric], dovetails: List[SingleDovetailSizeParameter]) -> Joint:
    """
    Creates a dovetail corner joint: a row of interlocking dovetails across a corner.

    timber1 keeps the dovetails and timber2 receives the matching sockets, so the two
    interlock through the corner the way a drawer front interlocks with a drawer side.
    Each dovetail starts at the shoulder -- the near face of timber2 -- and widens as it
    reaches `depth` into timber2, which locks timber1 against being drawn away from the
    corner. A dovetail whose depth is timber2's full thickness is a through dovetail and
    shows on timber2's outside face; a shallower one is half blind. Dovetails span
    timber1's whole thickness through the corner, so the joint assembles by sliding
    timber1 along timber2's length axis and nothing else.

    Dovetails are laid out across timber1's outside face along the axis normal to
    `front_face_on_timber1`, measured into the timber from that face.

    Both timbers are end cut to the far face of the other, so the corner reads flush on
    both outside faces.

    Args:
        arrangement: Corner arrangement. timber1 carries the dovetails and timber2 the
            sockets; the timbers must be face aligned, orthogonal, and flush across the
            `front_face_on_timber1` axis. `front_face_on_timber1` must be set -- the
            dovetail layout is measured from it.
        distances: Distance to the center of each dovetail, the first measured from
            `front_face_on_timber1` and each one after from the previous center.
        dovetails: Size of each dovetail, one entry per entry in `distances`.

    Returns:
        Joint object containing the two CutTimbers.

    Raises:
        KumikiArrangementError: If the timbers are not face aligned and orthogonal, or
            `front_face_on_timber1` does not point along the corner plane normal.
        AssertionError: If the dovetail parameters do not fit the timbers, overlap each
            other, or run past either face of the layout axis.
    """
    require_check(arrangement.check_face_aligned_and_orthogonal())
    require_check(arrangement.check_plane_aligned())

    warn_if_arrangement_timbers_imperfect(arrangement)

    front_face_on_dovetail_timber = arrangement.front_face_on_timber1
    assert front_face_on_dovetail_timber is not None, (
        "arrangement.front_face_on_timber1 must be set, the dovetail layout is measured from it"
    )
    assert len(dovetails) > 0, "at least one dovetail is required"
    assert len(distances) == len(dovetails), (
        f"distances and dovetails must be the same length, got {len(distances)} and {len(dovetails)}"
    )

    dovetail_timber = arrangement.timber1
    socket_timber = arrangement.timber2
    dovetail_timber_end = arrangement.timber1_end
    socket_timber_end = arrangement.timber2_end

    dovetail_end_direction_global = dovetail_timber.get_face_direction_global(dovetail_timber_end)
    socket_end_direction_global = socket_timber.get_face_direction_global(socket_timber_end)

    # The face on timber2 the dovetails enter through bounds how deep they can go.
    socket_entry_face = socket_timber.get_closest_oriented_long_face_from_global_direction(-dovetail_end_direction_global)
    socket_timber_thickness = socket_timber.get_size_in_face_normal_axis(socket_entry_face)

    sized_dovetails: List[Tuple[SingleDovetailSizeParameter, Numeric]] = []
    for index, dovetail in enumerate(dovetails):
        depth = socket_timber_thickness if dovetail.depth is None else dovetail.depth
        assert safe_compare(depth, 0, Comparison.GT), (
            f"dovetail {index} depth must be greater than 0, got {depth}"
        )
        assert safe_compare(socket_timber_thickness - depth, 0, Comparison.GE), (
            f"dovetail {index} depth ({depth}) must be <= the timber2 thickness it cuts into ({socket_timber_thickness})"
        )
        assert safe_compare(dovetail.small_width, 0, Comparison.GT), (
            f"dovetail {index} small_width must be greater than 0, got {dovetail.small_width}"
        )
        assert safe_compare(dovetail.angle, 0, Comparison.GE), (
            f"dovetail {index} angle must be non-negative, got {dovetail.angle}"
        )
        assert safe_compare(dovetail.angle - pi / scalar(2), 0, Comparison.LT), (
            f"dovetail {index} angle must be less than 90 degrees, got {dovetail.angle}"
        )
        sized_dovetails.append((dovetail, depth))

    # -------------------------------------------------------------------------
    # Marking space: timber1's outside corner face, laid out from its front face
    # -------------------------------------------------------------------------
    dovetail_timber_inside_face = dovetail_timber.get_closest_oriented_long_face_from_global_direction(-socket_end_direction_global)
    dovetail_timber_outside_face = dovetail_timber_inside_face.to.face().get_opposite_face().to.long_face()
    dovetail_timber_thickness = dovetail_timber.get_size_in_face_normal_axis(dovetail_timber_outside_face)

    front_face_direction_global = dovetail_timber.get_face_direction_global(front_face_on_dovetail_timber)
    layout_dimension = dovetail_timber.get_size_in_face_normal_axis(front_face_on_dovetail_timber)
    half_layout_dimension = layout_dimension / scalar(2)

    socket_timber_front_face = socket_timber.get_closest_oriented_long_face_from_global_direction(front_face_direction_global)
    assert safe_equality_test(
        socket_timber.get_size_in_face_normal_axis(socket_timber_front_face), layout_dimension
    ) and safe_equality_test(
        safe_dot_product(get_center_point_on_face_global(socket_timber_front_face, socket_timber), front_face_direction_global),
        safe_dot_product(get_center_point_on_face_global(front_face_on_dovetail_timber, dovetail_timber), front_face_direction_global),
    ), "timber1 and timber2 must be flush across the front_face_on_timber1 axis for a dovetail corner joint"

    # Profile x runs across the outside face, see chop_profile_on_timber_face.
    layout_direction_global = cross_product(
        dovetail_end_direction_global,
        dovetail_timber.get_face_direction_global(dovetail_timber_outside_face),
    )
    front_face_sits_at_negative_profile_x = safe_compare(
        safe_dot_product(front_face_direction_global, layout_direction_global), 0, Comparison.LT
    )

    def profile_x_of(distance_from_front_face: Numeric) -> Numeric:
        """Profile x of a distance measured into timber1 from front_face_on_timber1."""
        if front_face_sits_at_negative_profile_x:
            return -half_layout_dimension + distance_from_front_face
        return half_layout_dimension - distance_from_front_face

    # The shoulder is timber2's near face: where the dovetails leave timber1's body.
    socket_entry_plane = scribe_face_plane_onto_centerline(
        face=socket_entry_face.to.face(),
        face_timber=socket_timber,
    )
    shoulder_distance_from_end = mark_distance_from_end_along_centerline(
        socket_entry_plane, dovetail_timber, dovetail_timber_end
    ).distance
    assert safe_compare(shoulder_distance_from_end, 0, Comparison.GT), (
        "timber1 must reach past timber2's near face to be dovetailed into it"
    )

    # -------------------------------------------------------------------------
    # Dovetail profiles, narrow at the shoulder and flaring into timber2
    # -------------------------------------------------------------------------
    dovetail_csgs: List[CutCSG] = []
    distance_from_front_face = scalar(0)
    previous_dovetail_edge = scalar(0)
    for index, ((dovetail, depth), distance) in enumerate(zip(sized_dovetails, distances)):
        distance_from_front_face = distance_from_front_face + distance
        half_small_width = dovetail.small_width / scalar(2)
        half_large_width = half_small_width + depth * tan(dovetail.angle)

        assert safe_compare(distance_from_front_face - half_large_width - previous_dovetail_edge, 0, Comparison.GE), (
            "dovetail 0 crosses front_face_on_timber1"
            if index == 0
            else f"dovetail {index} overlaps dovetail {index - 1}"
        )
        previous_dovetail_edge = distance_from_front_face + half_large_width
        assert safe_compare(layout_dimension - previous_dovetail_edge, 0, Comparison.GE), (
            f"dovetail {index} runs past the face opposite front_face_on_timber1"
        )

        center_x = profile_x_of(distance_from_front_face)
        profile = [
            create_v2(center_x - half_small_width, scalar(0)),
            create_v2(center_x + half_small_width, scalar(0)),
            create_v2(center_x + half_large_width, depth),
            create_v2(center_x - half_large_width, depth),
        ]

        dovetail_csgs.append(chop_profile_on_timber_face(
            timber=dovetail_timber,
            end=dovetail_timber_end,
            face=dovetail_timber_outside_face.to.face(),
            profile=profile,
            depth=dovetail_timber_thickness,
            profile_y_offset_from_end=shoulder_distance_from_end,
            label=CutCSGLabel.NoLabel(),
        ))

    # A tail's faces belong to the timber that keeps it, so they are named on copies
    # made for the dovetail timber. The sockets are cut with the plain extrusions:
    # the walls they leave are the receiving timber's own surfaces, not the tail's.
    dovetails_csg = SolidUnion(
        [replace(csg, feature_overrides=_DOVETAIL_FACE_OVERRIDES) for csg in dovetail_csgs],
        label=CutCSGLabel("dovetails"),
    )
    sockets_csg = SolidUnion(
        [adopt_csg(dovetail_timber.transform, socket_timber.transform, csg) for csg in dovetail_csgs],
        label=CutCSGLabel("dovetail_sockets"),
    )

    # Everything past the shoulder comes away from timber1 except the dovetails. A half
    # space rather than a prism, so material outside the perfect timber within goes too.
    shoulder_half_space = chop_timber_end_with_half_plane(
        timber=dovetail_timber,
        end=dovetail_timber_end,
        distance_from_end_to_cut=shoulder_distance_from_end,
        label=CutCSGLabel("shoulder"),
        is_shoulder=True,
    )
    dovetail_timber_negative_csg = Difference(
        shoulder_half_space, [dovetails_csg], label=CutCSGLabel("dovetail_waste"),
    )

    # -------------------------------------------------------------------------
    # End cuts: each timber is cut to the far face of the other
    # note that techincally timber1 could have a shorter maybe end cut if all the tenons are half blind but we don't bother with that. please preserve this comment
    # -------------------------------------------------------------------------
    socket_timber_far_face_point_global = get_center_point_on_face_global(
        socket_entry_face.to.face().get_opposite_face(), socket_timber
    )
    dovetail_timber_end_cut_distance_from_bottom = safe_dot_product(
        socket_timber_far_face_point_global - dovetail_timber.get_bottom_position_global(),
        dovetail_timber.get_length_direction_global(),
    )

    dovetail_timber_outside_face_point_global = get_center_point_on_face_global(
        dovetail_timber_outside_face, dovetail_timber
    )
    socket_timber_end_cut_distance_from_bottom = safe_dot_product(
        dovetail_timber_outside_face_point_global - socket_timber.get_bottom_position_global(),
        socket_timber.get_length_direction_global(),
    )

    # The dovetails only leave their sockets out through timber2's end face.
    dovetail_withdraw_direction_global = dovetail_timber.get_face_direction_global(dovetail_timber_outside_face)

    dovetail_timber_cutting = Cutting(
        timber=dovetail_timber,
        maybe_top_end_cut_distance_from_bottom=dovetail_timber_end_cut_distance_from_bottom if dovetail_timber_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=dovetail_timber_end_cut_distance_from_bottom if dovetail_timber_end == TimberEnd.BOTTOM else None,
        negative_csg=dovetail_timber_negative_csg,
        label=CutCSGLabel("dovetail_cut"),
        assembly_freedom=AssemblyFreedom.translation(
            dovetail_withdraw_direction_global, freed_after=dovetail_timber_thickness
        ),
    )

    socket_timber_cutting = Cutting(
        timber=socket_timber,
        maybe_top_end_cut_distance_from_bottom=socket_timber_end_cut_distance_from_bottom if socket_timber_end == TimberEnd.TOP else None,
        maybe_bottom_end_cut_distance_from_bottom=socket_timber_end_cut_distance_from_bottom if socket_timber_end == TimberEnd.BOTTOM else None,
        negative_csg=sockets_csg,
        label=CutCSGLabel("dovetail_socket_cut"),
        assembly_freedom=AssemblyFreedom.translation(
            -dovetail_withdraw_direction_global, freed_after=dovetail_timber_thickness
        ),
    )

    return Joint(
        cuttings={
            "dovetail_timber": dovetail_timber_cutting,
            "socket_timber": socket_timber_cutting,
        },
        ticket=JointTicket(joint_type="dovetail_corner"),
        jointAccessories={},
    )