Source code for fabex.joinery.finger

from math import (
    pi,
)

from shapely.geometry import (
    Point,
)

import bpy

from .mortise import mortise

from ..constants import DT  # DT = Bit diameter tolerance

from ..utilities.compare_utils import angle
from ..utilities.logging_utils import log
from ..utilities.simple_utils import (
    active_name,
    duplicate,
    mirror_x,
    union,
    move,
    join_multiple,
    duplicate,
    difference,
    make_active,
    remove_multiple,
    rename,
    difference,
)


[docs] def finger(diameter, stem=2): """Create a joint shape based on the specified diameter and stem. This function generates a 3D joint shape using Blender's curve operations. It calculates the dimensions of a rectangle and an ellipse based on the provided diameter and stem parameters. The function then creates these shapes, duplicates and mirrors them, and performs boolean operations to form the final joint shape. The resulting object is named and cleaned up to ensure no overlapping vertices remain. Args: diameter (float): The diameter of the tool for joint creation. stem (float?): The amount of radius the stem or neck of the joint will have. Defaults to 2. Returns: None: This function does not return any value. """ RESOLUTION = 12 # Data resolution cube_sx = diameter * DT * (2 + stem - 1) cube_ty = diameter * DT cube_sy = 2 * diameter * DT circle_radius = diameter * DT / 2 c1x = cube_sx / 2 c2x = cube_sx / 2 c2y = 3 * circle_radius c1y = circle_radius bpy.ops.curve.simple( align="WORLD", location=(0, cube_ty, 0), rotation=(0, 0, 0), Simple_Type="Rectangle", Simple_width=cube_sx, Simple_length=cube_sy, use_cyclic_u=True, edit_mode=False, ) bpy.context.active_object.name = "ftmprect" bpy.ops.curve.simple( align="WORLD", location=(c2x, c2y, 0), rotation=(0, 0, 0), Simple_Type="Ellipse", Simple_a=circle_radius, Simple_b=circle_radius, Simple_sides=4, use_cyclic_u=True, edit_mode=False, shape="3D", ) bpy.context.active_object.name = "ftmpcirc_add" bpy.context.object.data.resolution_u = RESOLUTION bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") duplicate() mirror_x() union("ftmp") rename("ftmp", "_sum") rc1 = circle_radius bpy.ops.curve.simple( align="WORLD", location=(c1x, c1y, 0), rotation=(0, 0, 0), Simple_Type="Ellipse", Simple_a=circle_radius, Simple_b=rc1, Simple_sides=4, use_cyclic_u=True, edit_mode=False, shape="3D", ) bpy.context.active_object.name = "_circ_delete" bpy.context.object.data.resolution_u = RESOLUTION bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") duplicate() mirror_x() union("_circ") difference("_", "_sum") bpy.ops.object.curve_remove_doubles() rename("_sum", "_puzzle")
[docs] def fingers(diameter, inside, amount=1, stem=1): """Create a specified number of fingers for a joint tool. This function generates a set of fingers based on the provided diameter and tolerance values. It calculates the necessary translations for positioning the fingers and duplicates them if more than one is required. Additionally, it creates a receptacle using a silhouette offset from the fingers, allowing for precise joint creation. Args: diameter (float): The diameter of the tool used for joint creation. inside (float): The tolerance in the joint receptacle. amount (int?): The number of fingers to create. Defaults to 1. stem (float?): The amount of radius the stem or neck of the joint will have. Defaults to 1. """ xtranslate = -(4 + 2 * (stem - 1)) * (amount - 1) * diameter * DT / 2 finger(diameter, stem=stem) # generate male finger active_name("puzzlem") move(x=xtranslate, y=-0.00002) if amount > 1: # duplicate translate the amount needed (faster than generating new) for i in range(amount - 1): bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"}, TRANSFORM_OT_translate={"value": ((4 + 2 * (stem - 1)) * diameter * DT, 0, 0.0)}, ) union("puzzle") active_name("fingers") bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") # Receptacle is made using the silhouette offset from the fingers if inside > 0: bpy.ops.object.silhouette_offset(offset=inside, style="1") active_name("receptacle") move(y=-inside)
[docs] def finger_amount(space, size): """Calculates the amount of fingers needed from the available space vs the size of the finger Args: space (float):available distance to cover size (float): size of the finger """ finger_amt = space / size if (finger_amt % 1) != 0: finger_amt = round(finger_amt) + 1 if (finger_amt % 2) != 0: finger_amt = round(finger_amt) + 1 return finger_amt
[docs] def finger_pair(name, dx=0, dy=0): """Creates a duplicate set of fingers. Args: name (str): name of original finger dx (float): x offset dy (float): y offset """ make_active(name) xpos = (dx / 2) * 1.006 ypos = 1.006 * dy / 2 bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"}, TRANSFORM_OT_translate={"value": (xpos, ypos, 0.0)}, ) active_name("_finger_pair") make_active(name) bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"}, TRANSFORM_OT_translate={"value": (-xpos, -ypos, 0.0)}, ) active_name("_finger_pair") join_multiple("_finger_pair") bpy.ops.object.select_all(action="DESELECT") return bpy.context.active_object
[docs] def horizontal_finger(length, thickness, finger_play, amount, center=True): """Generates an interlocking horizontal finger pair _wfa and _wfb. _wfa is centered at 0,0 _wfb is _wfa offset by one length Args: length (float): Length of mortise thickness (float): thickness of material amount (int): quantity of fingers finger_play (float): tolerance for proper fit center (bool): centered of not """ if center: for i in range(amount): if i == 0: mortise(length, thickness, finger_play, 0, thickness / 2) active_name("_width_finger") else: mortise(length, thickness, finger_play, i * 2 * length, thickness / 2) active_name("_width_finger") mortise(length, thickness, finger_play, -i * 2 * length, thickness / 2) active_name("_width_finger") else: for i in range(amount): mortise(length, thickness, finger_play, length / 2 + 2 * i * length, 0) active_name("_width_finger") join_multiple("_width_finger") active_name("_wfa") bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"}, TRANSFORM_OT_translate={"value": (length, 0.0, 0.0)}, ) active_name("_wfb")
[docs] def vertical_finger(length, thickness, finger_play, amount): """Generates an interlocking horizontal finger pair _vfa and _vfb. _vfa is starts at 0,0 _vfb is _vfa offset by one length Args: length (float): Length of mortise thickness (float): thickness of material amount (int): quantity of fingers finger_play (float): tolerance for proper fit """ for i in range(amount): mortise(length, thickness, finger_play, 0, i * 2 * length + length / 2, rotation=pi / 2) active_name("_height_finger") join_multiple("_height_finger") active_name("_vfa") bpy.ops.object.duplicate_move( OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"}, TRANSFORM_OT_translate={"value": (0, -length, 0.0)}, ) active_name("_vfb")
[docs] def fixed_finger(loop, loop_length, finger_size, finger_thick, finger_tolerance, base=False): """distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences Args: loop (list of tuples): takes in a shapely shape loop_length (float): length of loop finger_size (float): size of the mortise finger_thick (float): thickness of the material finger_tolerance (float): minimum finger tolerance base (bool): if base exists, it will join with it """ coords = list(loop.coords) old_mortise_angle = 0 distance = finger_size / 2 j = 0 log.info(f"Joinery Loop Length {round(loop_length * 1000)}mm") for i, p in enumerate(coords): if i == 0: p_start = p if p != p_start: not_start = True else: not_start = False pd = loop.project(Point(p)) if not_start: while distance <= pd: mortise_angle = angle(oldp, p) mortise_angle_difference = abs(mortise_angle - old_mortise_angle) mad = 1 + 6 * min(mortise_angle_difference, pi / 4) / ( pi / 4 ) # factor for tolerance for the finger if base: mortise(finger_size, finger_thick, finger_tolerance * mad, distance, 0, 0) active_name("_base") else: mortise_point = loop.interpolate(distance) mortise( finger_size, finger_thick, finger_tolerance * mad, mortise_point.x, mortise_point.y, mortise_angle, ) j += 1 distance = j * 2 * finger_size + finger_size / 2 old_mortise_angle = mortise_angle oldp = p if base: join_multiple("_base") active_name("base") move(x=finger_size) else: join_multiple("_mort") active_name("mortise")
[docs] def variable_finger( loop, loop_length, min_finger, finger_size, finger_thick, finger_tolerance, adaptive, base=False, double_adaptive=False, ): """Distributes mortises of a fixed distance. Dynamically changes the finger tolerance with the angle differences Args: loop (list of tuples): takes in a shapely shape loop_length (float): length of loop finger_size (float): size of the mortise finger_thick (float): thickness of the material min_finger (float): minimum finger size finger_tolerance (float): minimum finger tolerance adaptive (float): angle threshold to reduce finger size base (bool): join with base if true double_adaptive (bool): uses double adaptive algorithm if true """ coords = list(loop.coords) old_mortise_angle = 0 distance = min_finger / 2 finger_sz = min_finger oldfinger_sz = min_finger hpos = [] # hpos is the horizontal positions of the middle of the mortise # slope_array(loop) log.info(f"Joinery Loop Length {round(loop_length * 1000)}mm") for i, p in enumerate(coords): if i == 0: p_start = p if p != p_start: not_start = True else: not_start = False pd = loop.project(Point(p)) if not_start: while distance <= pd: mortise_angle = angle(oldp, p) mortise_angle_difference = abs(mortise_angle - old_mortise_angle) mad = 1 + 6 * min(mortise_angle_difference, pi / 4) / ( pi / 4 ) # factor for tolerance for the finger # move finger by the factor mad greater with larger angle difference distance += mad * finger_tolerance mortise_point = loop.interpolate(distance) if mad > 2 and double_adaptive: hpos.append(distance) # saves the mortise center hpos.append(distance + finger_sz) # saves the mortise center if base: mortise( finger_sz, finger_thick, finger_tolerance * mad, distance + finger_sz, 0, 0 ) active_name("_base") else: mortise( finger_sz, finger_thick, finger_tolerance * mad, mortise_point.x, mortise_point.y, mortise_angle, ) if i == 1: # put a mesh cylinder at the first coordinates to indicate start remove_multiple("start_here") bpy.ops.mesh.primitive_cylinder_add( radius=finger_thick / 2, depth=0.025, enter_editmode=False, align="WORLD", location=(mortise_point.x, mortise_point.y, 0), scale=(1, 1, 1), ) active_name("start_here_mortise") old_distance = distance old_mortise_point = mortise_point finger_sz = finger_size next_angle_difference = pi # adaptive finger length start while finger_sz > min_finger and next_angle_difference > adaptive: # while finger_sz > min_finger and next_angle_difference > adaptive: # reduce the size of finger by a percentage... the closer to 1.0, the slower finger_sz *= 0.95 distance = old_distance + 3 * oldfinger_sz / 2 + finger_sz / 2 mortise_point = loop.interpolate(distance) # get the next mortise point next_mortise_angle = angle( (old_mortise_point.x, old_mortise_point.y), (mortise_point.x, mortise_point.y), ) # calculate next angle next_angle_difference = abs(next_mortise_angle - mortise_angle) oldfinger_sz = finger_sz old_mortise_angle = mortise_angle oldp = p if base: join_multiple("_base") active_name("base") else: log.info("Placeholder") join_multiple("_mort") active_name("variable_mortise") return hpos