"""Fabex 'curve_cam_create.py' © 2021, 2022 Alain Pelletier
Operators to create a number of predefined curve objects.
"""
from math import degrees, hypot, pi, radians
from shapely import affinity
from shapely.geometry import (
LineString,
MultiLineString,
box,
)
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
)
from bpy.types import Operator
from .. import (
involute_gear,
joinery,
puzzle_joinery,
)
from ..cam_chunk import (
curve_to_shapely,
polygon_boolean,
polygon_convex_hull,
)
from ..utilities.simple_utils import (
remove_multiple,
select_multiple,
join_multiple,
make_active,
deselect,
active_name,
remove_doubles,
rename,
duplicate,
add_overcut,
move,
difference,
union,
rotate,
)
from ..utilities.shapely_utils import (
shapely_to_curve,
)
[docs]
def generate_crosshatch(context, angle, distance, offset, pocket_shape, join, ob=None):
"""Execute the crosshatch generation process based on the provided context.
Args:
context (bpy.context): The Blender context containing the active object.
angle (float): The angle for rotating the crosshatch pattern.
distance (float): The distance between crosshatch lines.
offset (float): The offset for the bounds or hull.
pocket_shape (str): Determines whether to use bounds, hull, or pocket.
Returns:
shapely.geometry.MultiLineString: The resulting intersection geometry of the crosshatch.
"""
if ob is None:
ob = context.active_object
else:
bpy.context.view_layer.objects.active = ob
ob.select_set(True)
if ob.data.splines and ob.data.splines[0].type == "BEZIER":
bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True)
else:
bpy.ops.object.curve_remove_doubles()
bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
depth = ob.location[2]
shapes = curve_to_shapely(ob)
if pocket_shape == "HULL":
shapes = shapes.convex_hull
coords = []
minx, miny, maxx, maxy = shapes.bounds
minx -= offset
miny -= offset
maxx += offset
maxy += offset
centery = (miny + maxy) / 2
length = maxy - miny
width = maxx - minx
centerx = (minx + maxx) / 2
diagonal = hypot(width, length)
bound_rectangle = box(minx, miny, maxx, maxy)
amount = int(2 * diagonal / distance) + 1
for x in range(amount):
distance_val = x * distance - diagonal
coords.append(((distance_val, diagonal + 0.5), (distance_val, -diagonal - 0.5)))
# Create a multilinestring shapely object
lines = MultiLineString(coords)
rotated = affinity.rotate(lines, angle, use_radians=True) # Rotate using shapely
rotated_minx, rotated_miny, rotated_maxx, rotated_maxy = rotated.bounds
rotated_centerx = (rotated_minx + rotated_maxx) / 2
rotated_centery = (rotated_miny + rotated_maxy) / 2
x_offset = centerx - rotated_centerx
y_offset = centery - rotated_centery
translated = affinity.translate(
rotated, xoff=x_offset, yoff=y_offset, zoff=depth
) # Move using shapely
bounds = bound_rectangle
if pocket_shape == "BOUNDS":
xing = translated.intersection(bounds) # Intersection with bounding box
else:
xing = translated.intersection(
shapes.buffer(offset, join_style=join)
) # Intersection with shapes or hull
# Return the intersection result
return xing
[docs]
class CamCurveHatch(Operator):
"""Perform Hatch Operation on Single or Multiple Curves""" # by Alain Pelletier September 2021
[docs]
bl_idname = "object.curve_hatch"
[docs]
bl_label = "CrossHatch Curve"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
angle: FloatProperty(default=0, min=-pi / 2, max=pi / 2, precision=4, subtype="ANGLE")
[docs]
distance: FloatProperty(default=0.003, min=0, max=3.0, precision=4, unit="LENGTH")
[docs]
offset: FloatProperty(default=0, min=-1.0, max=3.0, precision=4, unit="LENGTH")
[docs]
pocket_shape: EnumProperty(
name="Pocket Shape",
items=(
("BOUNDS", "Bounds Rectangle", "Uses a bounding rectangle"),
("HULL", "Convex Hull", "Uses a convex hull"),
("POCKET", "Pocket", "Uses the pocket shape"),
),
description="Type of pocket shape",
default="POCKET",
)
[docs]
contour: BoolProperty(
name="Contour Curve",
default=False,
)
[docs]
xhatch: BoolProperty(
name="Crosshatch #",
default=False,
)
[docs]
contour_separate: BoolProperty(
name="Contour Separate",
default=False,
)
[docs]
straight: BoolProperty(
name="Overshoot Style",
description="Use overshoot cutout instead of conventional rounded",
default=True,
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and context.active_object.type in ["CURVE", "FONT"]
[docs]
def draw(self, context):
"""Draw the layout properties for the given context."""
layout = self.layout
layout.prop(self, "angle")
layout.prop(self, "distance")
layout.prop(self, "offset")
layout.prop(self, "pocket_shape")
layout.prop(self, "xhatch")
if self.pocket_shape == "POCKET":
layout.prop(self, "straight")
layout.prop(self, "contour")
if self.contour:
layout.prop(self, "contour_separate")
[docs]
def execute(self, context):
if self.straight:
join = 2
else:
join = 1
ob = context.active_object
obname = ob.name
ob.select_set(True)
remove_multiple("crosshatch")
depth = ob.location[2]
xingOffset = self.offset
if self.contour:
xingOffset = self.offset - self.distance / 2 # contour does not touch the crosshatch
xing = generate_crosshatch(
context,
self.angle,
self.distance,
xingOffset,
self.pocket_shape,
join,
)
shapely_to_curve("crosshatch_lines", xing, depth)
if self.xhatch:
make_active(obname)
xingra = generate_crosshatch(
context,
self.angle + pi / 2,
self.distance,
xingOffset,
self.pocket_shape,
join,
)
shapely_to_curve("crosshatch_lines_ra", xingra, depth)
bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="MEDIAN")
join_multiple("crosshatch")
if self.contour:
deselect()
bpy.context.view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.object.silhouette_offset(offset=self.offset)
if self.contour_separate:
active_name("contour_hatch")
deselect()
else:
active_name("crosshatch_contour")
join_multiple("crosshatch")
remove_doubles()
else:
join_multiple("crosshatch")
remove_doubles()
return {"FINISHED"}
[docs]
class CamCurvePlate(Operator):
"""Perform Generates Rounded Plate with Mounting Holes""" # by Alain Pelletier Sept 2021
[docs]
bl_idname = "object.curve_plate"
[docs]
bl_label = "Sign Plate"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
radius: FloatProperty(
name="Corner Radius",
default=0.025,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
[docs]
width: FloatProperty(
name="Width of Plate",
default=0.3048,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
height: FloatProperty(
name="Height of Plate",
default=0.457,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hole_diameter: FloatProperty(
name="Hole Diameter",
default=0.01,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hole_tolerance: FloatProperty(
name="Hole V Tolerance",
default=0.005,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hole_vdist: FloatProperty(
name="Hole Vert Distance",
default=0.400,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hole_hdist: FloatProperty(
name="Hole Horiz Distance",
default=0,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hole_hamount: IntProperty(
name="Hole Horiz Amount",
default=1,
min=0,
max=50,
)
[docs]
resolution: IntProperty(
name="Spline Resolution",
default=50,
min=3,
max=150,
)
[docs]
plate_type: EnumProperty(
name="Type Plate",
items=(
("ROUNDED", "Rounded corner", "Makes a rounded corner plate"),
("COVE", "Cove corner", "Makes a plate with circles cut in each corner "),
("BEVEL", "Bevel corner", "Makes a plate with beveled corners "),
("OVAL", "Elipse", "Makes an oval plate"),
),
description="Type of Plate",
default="ROUNDED",
)
[docs]
def draw(self, context):
"""Draw the UI layout for plate properties.
This method creates a user interface layout for configuring various
properties of a plate, including its type, dimensions, hole
specifications, and resolution. It dynamically adds properties to the
layout based on the selected plate type, allowing users to input
relevant parameters.
Args:
context: The context in which the UI is being drawn.
"""
layout = self.layout
layout.prop(self, "plate_type")
layout.prop(self, "width")
layout.prop(self, "height")
layout.prop(self, "hole_diameter")
layout.prop(self, "hole_tolerance")
layout.prop(self, "hole_vdist")
layout.prop(self, "hole_hdist")
layout.prop(self, "hole_hamount")
layout.prop(self, "resolution")
if self.plate_type in ["ROUNDED", "COVE", "BEVEL"]:
layout.prop(self, "radius")
[docs]
def execute(self, context):
"""Execute the creation of a plate based on specified parameters.
This function generates a plate shape in Blender based on the defined
attributes such as width, height, radius, and plate type. It supports
different plate types including rounded, oval, cove, and bevel. The
function also handles the creation of holes in the plate if specified.
It utilizes Blender's curve operations to create the geometry and
applies various transformations to achieve the desired shape.
Args:
context (bpy.context): The Blender context in which the operation is performed.
Returns:
dict: A dictionary indicating the result of the operation, typically
{'FINISHED'} if successful.
"""
left = -self.width / 2 + self.radius
bottom = -self.height / 2 + self.radius
right = -left
top = -bottom
if self.plate_type == "ROUNDED":
# create base
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(left, bottom, 0),
scale=(1, 1, 1),
)
active_name("_circ_LB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(right, bottom, 0),
scale=(1, 1, 1),
)
active_name("_circ_RB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(left, top, 0),
scale=(1, 1, 1),
)
active_name("_circ_LT")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(right, top, 0),
scale=(1, 1, 1),
)
active_name("_circ_RT")
bpy.context.object.data.resolution_u = self.resolution
# select the circles for the four corners
select_multiple("_circ")
# perform hull operation on the four corner circles
polygon_convex_hull(context)
active_name("plate_base")
remove_multiple("_circ") # remove corner circles
elif self.plate_type == "OVAL":
bpy.ops.curve.simple(
align="WORLD",
location=(0, 0, 0),
rotation=(0, 0, 0),
Simple_Type="Ellipse",
Simple_a=self.width / 2,
Simple_b=self.height / 2,
use_cyclic_u=True,
edit_mode=False,
)
bpy.context.object.data.resolution_u = self.resolution
active_name("plate_base")
elif self.plate_type == "COVE":
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(left - self.radius, bottom - self.radius, 0),
scale=(1, 1, 1),
)
active_name("_circ_LB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(right + self.radius, bottom - self.radius, 0),
scale=(1, 1, 1),
)
active_name("_circ_RB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(left - self.radius, top + self.radius, 0),
scale=(1, 1, 1),
)
active_name("_circ_LT")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.radius,
enter_editmode=False,
align="WORLD",
location=(right + self.radius, top + self.radius, 0),
scale=(1, 1, 1),
)
active_name("_circ_RT")
bpy.context.object.data.resolution_u = self.resolution
join_multiple("_circ")
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.width,
Simple_length=self.height,
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_base")
difference("_", "_base")
rename("_base", "plate_base")
elif self.plate_type == "BEVEL":
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.radius * 2,
Simple_length=self.radius * 2,
location=(left - self.radius, bottom - self.radius, 0),
rotation=(0, 0, 0.785398),
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_bev_LB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.radius * 2,
Simple_length=self.radius * 2,
location=(right + self.radius, bottom - self.radius, 0),
rotation=(0, 0, 0.785398),
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_bev_RB")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.radius * 2,
Simple_length=self.radius * 2,
location=(left - self.radius, top + self.radius, 0),
rotation=(0, 0, 0.785398),
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_bev_LT")
bpy.context.object.data.resolution_u = self.resolution
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.radius * 2,
Simple_length=self.radius * 2,
location=(right + self.radius, top + self.radius, 0),
rotation=(0, 0, 0.785398),
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_bev_RT")
bpy.context.object.data.resolution_u = self.resolution
join_multiple("_bev")
bpy.ops.curve.simple(
align="WORLD",
Simple_Type="Rectangle",
Simple_width=self.width,
Simple_length=self.height,
outputType="POLY",
use_cyclic_u=True,
edit_mode=False,
)
active_name("_base")
difference("_", "_base")
rename("_base", "plate_base")
if self.hole_diameter > 0 or self.hole_hamount > 0:
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.hole_diameter / 2,
enter_editmode=False,
align="WORLD",
location=(0, self.hole_tolerance / 2, 0),
scale=(1, 1, 1),
)
active_name("_hole_Top")
bpy.context.object.data.resolution_u = int(self.resolution / 4)
if self.hole_tolerance > 0:
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.hole_diameter / 2,
enter_editmode=False,
align="WORLD",
location=(0, -self.hole_tolerance / 2, 0),
scale=(1, 1, 1),
)
active_name("_hole_Bottom")
bpy.context.object.data.resolution_u = int(self.resolution / 4)
# select everything starting with _hole and perform a convex hull on them
select_multiple("_hole")
polygon_convex_hull(context)
active_name("plate_hole")
move(y=-self.hole_vdist / 2)
duplicate(y=self.hole_vdist)
remove_multiple("_hole") # remove temporary holes
join_multiple("plate_hole") # join the holes together
# horizontal holes
if self.hole_hamount > 1:
if self.hole_hamount % 2 != 0:
for x in range(int((self.hole_hamount - 1) / 2)):
# calculate the distance from the middle
dist = self.hole_hdist * (x + 1)
duplicate()
bpy.context.object.location[0] = dist
duplicate()
bpy.context.object.location[0] = -dist
else:
for x in range(int(self.hole_hamount / 2)):
dist = (
self.hole_hdist * x + self.hole_hdist / 2
) # calculate the distance from the middle
if (
x == 0
): # special case where the original hole only needs to move and not duplicate
bpy.context.object.location[0] = dist
duplicate()
bpy.context.object.location[0] = -dist
else:
duplicate()
bpy.context.object.location[0] = dist
duplicate()
bpy.context.object.location[0] = -dist
join_multiple("plate_hole") # join the holes together
# select everything starting with plate_
select_multiple("plate_")
# Make the plate base active
bpy.context.view_layer.objects.active = bpy.data.objects["plate_base"]
# Remove holes from the base
polygon_boolean(context, "DIFFERENCE")
remove_multiple("plate_") # Remove temporary base and holes
remove_multiple("_")
active_name("plate")
bpy.context.active_object.select_set(True)
bpy.ops.object.curve_remove_doubles()
return {"FINISHED"}
[docs]
class CamCurveFlatCone(Operator):
"""Generates cone from flat stock""" # by Alain Pelletier Sept 2021
[docs]
bl_idname = "object.curve_flat_cone"
[docs]
bl_label = "Cone Flat Calculator"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
small_d: FloatProperty(
name="Small Diameter",
default=0.025,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
[docs]
large_d: FloatProperty(
name="Large Diameter",
default=0.3048,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
height: FloatProperty(
name="Height of Cone",
default=0.457,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
tab: FloatProperty(
name="Tab Witdh",
default=0.01,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
[docs]
intake: FloatProperty(
name="Intake Diameter",
default=0,
min=0,
max=0.200,
precision=4,
unit="LENGTH",
)
[docs]
intake_skew: FloatProperty(
name="Intake Skew",
default=1,
min=0.1,
max=4,
)
[docs]
resolution: IntProperty(
name="Resolution",
default=12,
min=5,
max=200,
)
[docs]
def execute(self, context):
"""Execute the construction of a geometric shape in Blender.
This method performs a series of operations to create a geometric shape
based on specified dimensions and parameters. It calculates various
dimensions needed for the shape, including height and angles, and then
uses Blender's operations to create segments, rectangles, and ellipses.
The function also handles the positioning and rotation of these shapes
within the 3D space of Blender.
Args:
context: The context in which the operation is executed, typically containing
information about the current
scene and active objects in Blender.
Returns:
dict: A dictionary indicating the completion status of the operation,
typically {'FINISHED'}.
"""
y = self.small_d / 2
z = self.large_d / 2
x = self.height
h = x * y / (z - y)
a = hypot(h, y)
ab = hypot(x + h, z)
b = ab - a
angle = pi * 2 * y / a
# create base
bpy.ops.curve.simple(
Simple_Type="Segment",
Simple_a=ab,
Simple_b=a,
Simple_endangle=degrees(angle),
use_cyclic_u=True,
edit_mode=False,
)
active_name("_segment")
bpy.ops.curve.simple(
align="WORLD",
location=(a + b / 2, -self.tab / 2, 0),
rotation=(0, 0, 0),
Simple_Type="Rectangle",
Simple_width=b - 0.0050,
Simple_length=self.tab,
use_cyclic_u=True,
edit_mode=False,
shape="3D",
)
active_name("_segment")
if self.intake > 0:
bpy.ops.curve.simple(
align="WORLD",
location=(0, 0, 0),
rotation=(0, 0, 0),
Simple_Type="Ellipse",
Simple_a=self.intake,
Simple_b=self.intake * self.intake_skew,
use_cyclic_u=True,
edit_mode=False,
shape="3D",
)
move(x=ab - 3 * self.intake / 2)
rotate(angle / 2)
bpy.context.object.data.resolution_u = self.resolution
union("_segment")
active_name("flat_cone")
return {"FINISHED"}
[docs]
class CamCurveMortise(Operator):
"""Generates Mortise Along a Curve""" # by Alain Pelletier December 2021
[docs]
bl_idname = "object.curve_mortise"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
finger_size: BoolProperty(
name="Kurf Bending only",
default=False,
)
finger_size: FloatProperty(
name="Maximum Finger Size",
default=0.015,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
min_finger_size: FloatProperty(
name="Minimum Finger Size",
default=0.0025,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
[docs]
plate_thickness: FloatProperty(
name="Drawer Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
unit="LENGTH",
)
[docs]
side_height: FloatProperty(
name="Side Height",
default=0.05,
min=0.001,
max=3.0,
unit="LENGTH",
)
[docs]
flex_pocket: FloatProperty(
name="Flex Pocket",
default=0.004,
min=0.000,
max=1.0,
unit="LENGTH",
)
[docs]
top_bottom: BoolProperty(
name="Side Top & Bottom Fingers",
default=True,
)
[docs]
opencurve: BoolProperty(
name="OpenCurve",
default=False,
)
[docs]
adaptive: FloatProperty(
name="Adaptive Angle Threshold",
default=0.0,
min=0.000,
max=2,
step=100,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
double_adaptive: BoolProperty(
name="Double Adaptive Pockets",
default=False,
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (
context.active_object.type in ["CURVE", "FONT"]
)
[docs]
def execute(self, context):
"""Execute the joinery process based on the provided context.
This function performs a series of operations to duplicate the active
object, convert it to a mesh, and then process its geometry to create
joinery features. It extracts vertex coordinates, converts them into a
LineString data structure, and applies either variable or fixed finger
joinery based on the specified parameters. The function also handles the
creation of flexible sides and pockets if required.
Args:
context (bpy.context): The context in which the operation is executed.
Returns:
dict: A dictionary indicating the completion status of the operation.
"""
o1 = bpy.context.active_object
bpy.context.object.data.resolution_u = 60
bpy.ops.object.duplicate()
obj = context.active_object
bpy.ops.object.convert(target="MESH")
active_name("_temp_mesh")
if self.opencurve:
coords = []
for v in obj.data.vertices: # extract X,Y coordinates from the vertices data
coords.append((v.co.x, v.co.y))
# convert coordinates to shapely LineString datastructure
line = LineString(coords)
remove_multiple("-converted")
shapely_to_curve("-converted_curve", line, 0.0)
shapes = curve_to_shapely(o1)
for s in shapes.geoms:
if s.boundary.geom_type == "LineString":
loops = [s.boundary]
else:
loops = s.boundary
for ci, c in enumerate(loops):
if self.opencurve:
length = line.length
else:
length = c.length
print("Loop Length:", length)
if self.opencurve:
loop_length = line.length
else:
loop_length = c.length
print("Line Length:", loop_length)
if self.adaptive > 0.0:
joinery.variable_finger(
c,
length,
self.min_finger_size,
self.finger_size,
self.plate_thickness,
self.finger_tolerance,
self.adaptive,
)
locations = joinery.variable_finger(
c,
length,
self.min_finger_size,
self.finger_size,
self.plate_thickness,
self.finger_tolerance,
self.adaptive,
True,
self.double_adaptive,
)
joinery.create_flex_side(
loop_length, self.side_height, self.plate_thickness, self.top_bottom
)
if self.flex_pocket > 0:
joinery.make_variable_flex_pocket(
self.side_height, self.plate_thickness, self.flex_pocket, locations
)
else:
joinery.fixed_finger(
c, length, self.finger_size, self.plate_thickness, self.finger_tolerance
)
joinery.fixed_finger(
c,
length,
self.finger_size,
self.plate_thickness,
self.finger_tolerance,
True,
)
joinery.create_flex_side(
loop_length, self.side_height, self.plate_thickness, self.top_bottom
)
if self.flex_pocket > 0:
joinery.make_flex_pocket(
length,
self.side_height,
self.plate_thickness,
self.finger_size,
self.flex_pocket,
)
remove_multiple("_")
return {"FINISHED"}
[docs]
class CamCurveInterlock(Operator):
"""Generates Interlock Along a Curve""" # by Alain Pelletier December 2021
[docs]
bl_idname = "object.curve_interlock"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
finger_size: FloatProperty(
name="Finger Size",
default=0.015,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
[docs]
plate_thickness: FloatProperty(
name="Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
unit="LENGTH",
)
[docs]
opencurve: BoolProperty(
name="OpenCurve",
default=False,
)
[docs]
interlock_type: EnumProperty(
name="Type of Interlock",
items=(
("TWIST", "Twist", "Interlock requires 1/4 turn twist"),
("GROOVE", "Groove", "Simple sliding groove"),
("PUZZLE", "Puzzle Interlock", "Puzzle good for flat joints"),
),
description="Type of interlock",
default="GROOVE",
)
[docs]
finger_amount: IntProperty(
name="Finger Amount",
default=2,
min=1,
max=100,
)
[docs]
tangent_angle: FloatProperty(
name="Tangent Deviation",
default=0.0,
min=0.000,
max=2,
step=100,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
fixed_angle: FloatProperty(
name="Fixed Angle",
default=0.0,
min=0.000,
max=2,
step=100,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
def execute(self, context):
"""Execute the joinery operation based on the selected objects in the
context.
This function checks the selected objects in the provided context and
performs different operations depending on the type of the active
object. If the active object is a curve or font and there are selected
objects, it duplicates the object, converts it to a mesh, and processes
its vertices to create a LineString representation. The function then
calculates lengths and applies distributed interlock joinery based on
the specified parameters. If no valid objects are selected, it defaults
to a single interlock operation at the cursor's location.
Args:
context (bpy.context): The context containing selected objects and active object.
Returns:
dict: A dictionary indicating the operation's completion status.
"""
print(len(context.selected_objects), "selected object", context.selected_objects)
if len(context.selected_objects) > 0 and (context.active_object.type in ["CURVE", "FONT"]):
o1 = bpy.context.active_object
bpy.context.object.data.resolution_u = 60
duplicate()
obj = context.active_object
bpy.ops.object.convert(target="MESH")
active_name("_temp_mesh")
if self.opencurve:
coords = []
for v in obj.data.vertices: # extract X,Y coordinates from the vertices data
coords.append((v.co.x, v.co.y))
# convert coordinates to shapely LineString datastructure
line = LineString(coords)
remove_multiple("-converted")
shapely_to_curve("-converted_curve", line, 0.0)
shapes = curve_to_shapely(o1)
for s in shapes.geoms:
if s.boundary.type == "LineString":
loops = [s.boundary]
else:
loops = s.boundary
for ci, c in enumerate(loops):
if self.opencurve:
length = line.length
else:
length = c.length
print("Loop Length:", length)
if self.opencurve:
loop_length = line.length
else:
loop_length = c.length
print("Line Length:", loop_length)
joinery.distributed_interlock(
c,
length,
self.finger_size,
self.plate_thickness,
self.finger_tolerance,
self.finger_amount,
fixed_angle=self.fixed_angle,
tangent=self.tangent_angle,
closed=not self.opencurve,
type=self.interlock_type,
)
else:
location = bpy.context.scene.cursor.location
joinery.single_interlock(
self.finger_size,
self.plate_thickness,
self.finger_tolerance,
location[0],
location[1],
self.fixed_angle,
self.interlock_type,
self.finger_amount,
)
bpy.context.scene.cursor.location = location
return {"FINISHED"}
[docs]
class CamCurveDrawer(Operator):
"""Generates Drawers""" # by Alain Pelletier December 2021 inspired by The Drawinator
[docs]
bl_idname = "object.curve_drawer"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
depth: FloatProperty(
name="Drawer Depth",
default=0.2,
min=0,
max=1.0,
precision=4,
unit="LENGTH",
)
[docs]
width: FloatProperty(
name="Drawer Width",
default=0.125,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
height: FloatProperty(
name="Drawer Height",
default=0.07,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
finger_size: FloatProperty(
name="Maximum Finger Size",
default=0.015,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
[docs]
finger_inset: FloatProperty(
name="Finger Inset",
default=0.0,
min=0.0,
max=0.01,
precision=4,
unit="LENGTH",
)
[docs]
drawer_plate_thickness: FloatProperty(
name="Drawer Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
drawer_hole_diameter: FloatProperty(
name="Drawer Hole Diameter",
default=0.02,
min=0.00001,
max=0.5,
precision=4,
unit="LENGTH",
)
[docs]
drawer_hole_offset: FloatProperty(
name="Drawer Hole Offset",
default=0.0,
min=-0.5,
max=0.5,
precision=4,
unit="LENGTH",
)
[docs]
overcut: BoolProperty(
name="Add Overcut",
default=False,
)
[docs]
overcut_diameter: FloatProperty(
name="Overcut Tool Diameter",
default=0.003175,
min=-0.001,
max=0.5,
precision=4,
unit="LENGTH",
)
[docs]
def draw(self, context):
"""Draw the user interface properties for the object.
This method is responsible for rendering the layout of various
properties related to the object's dimensions and specifications. It
adds properties such as depth, width, height, finger size, finger
tolerance, finger inset, drawer plate thickness, drawer hole diameter,
drawer hole offset, and overcut diameter to the layout. The overcut
diameter property is only added if the overcut option is enabled.
Args:
context: The context in which the drawing occurs, typically containing
information about the current state and environment.
"""
layout = self.layout
layout.prop(self, "depth")
layout.prop(self, "width")
layout.prop(self, "height")
layout.prop(self, "finger_size")
layout.prop(self, "finger_tolerance")
layout.prop(self, "finger_inset")
layout.prop(self, "drawer_plate_thickness")
layout.prop(self, "drawer_hole_diameter")
layout.prop(self, "drawer_hole_offset")
layout.prop(self, "overcut")
if self.overcut:
layout.prop(self, "overcut_diameter")
[docs]
def execute(self, context):
"""Execute the drawer creation process in Blender.
This method orchestrates the creation of a drawer by calculating the
necessary dimensions for the finger joints, creating the base plate, and
generating the drawer components such as the back, front, sides, and
bottom. It utilizes various helper functions to perform operations like
boolean differences and transformations to achieve the desired geometry.
The method also handles the placement of the drawer components in the 3D
space.
Args:
context (bpy.context): The Blender context that provides access to the current scene and
objects.
Returns:
dict: A dictionary indicating the completion status of the operation,
typically {'FINISHED'}.
"""
height_finger_amt = int(joinery.finger_amount(self.height, self.finger_size))
height_finger = (self.height + 0.0004) / height_finger_amt
width_finger_amt = int(joinery.finger_amount(self.width, self.finger_size))
width_finger = (self.width - self.finger_size) / width_finger_amt
# create base
joinery.create_base_plate(self.height, self.width, self.depth)
bpy.context.object.data.resolution_u = 64
bpy.context.scene.cursor.location = (0, 0, 0)
joinery.vertical_finger(
height_finger, self.drawer_plate_thickness, self.finger_tolerance, height_finger_amt
)
joinery.horizontal_finger(
width_finger, self.drawer_plate_thickness, self.finger_tolerance, width_finger_amt * 2
)
make_active("_wfb")
bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")
# make drawer back
finger_pair = joinery.finger_pair(
"_vfa", self.width - self.drawer_plate_thickness - self.finger_inset * 2, 0
)
make_active("_wfa")
fronth = bpy.context.active_object
make_active("_back")
finger_pair.select_set(True)
fronth.select_set(True)
bpy.ops.object.curve_boolean(boolean_type="DIFFERENCE")
remove_multiple("_finger_pair")
active_name("drawer_back")
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
# make drawer front
bpy.ops.curve.primitive_bezier_circle_add(
radius=self.drawer_hole_diameter / 2,
enter_editmode=False,
align="WORLD",
location=(0, self.height + self.drawer_hole_offset, 0),
scale=(1, 1, 1),
)
active_name("_circ")
front_hole = bpy.context.active_object
make_active("drawer_back")
front_hole.select_set(True)
bpy.ops.object.curve_boolean(boolean_type="DIFFERENCE")
active_name("drawer_front")
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
# place back and front side by side
make_active("drawer_front")
bpy.ops.transform.transform(mode="TRANSLATION", value=(0.0, 2 * self.height, 0.0, 0.0))
make_active("drawer_back")
bpy.ops.transform.transform(
mode="TRANSLATION", value=(self.width + 0.01, 2 * self.height, 0.0, 0.0)
)
# make side
finger_pair = joinery.finger_pair("_vfb", self.depth - self.drawer_plate_thickness, 0)
make_active("_side")
finger_pair.select_set(True)
fronth.select_set(True)
bpy.ops.object.curve_boolean(boolean_type="DIFFERENCE")
active_name("drawer_side")
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
remove_multiple("_finger_pair")
# make bottom
make_active("_wfb")
bpy.ops.object.duplicate_move(
OBJECT_OT_duplicate={"linked": False, "mode": "TRANSLATION"},
TRANSFORM_OT_translate={"value": (0, -self.drawer_plate_thickness / 2, 0.0)},
)
active_name("_wfb0")
joinery.finger_pair("_wfb0", 0, self.depth - self.drawer_plate_thickness)
active_name("_bot_fingers")
difference("_bot", "_bottom")
rotate(pi / 2)
joinery.finger_pair(
"_wfb0", 0, self.width - self.drawer_plate_thickness - self.finger_inset * 2
)
active_name("_bot_fingers")
difference("_bot", "_bottom")
active_name("drawer_bottom")
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
# cleanup all temp polygons
remove_multiple("_")
# move side and bottom to location
make_active("drawer_side")
bpy.ops.transform.transform(
mode="TRANSLATION",
value=(self.depth / 2 + 3 * self.width / 2 + 0.02, 2 * self.height, 0.0, 0.0),
)
make_active("drawer_bottom")
bpy.ops.transform.transform(
mode="TRANSLATION",
value=(self.depth / 2 + 3 * self.width / 2 + 0.02, self.width / 2, 0.0, 0.0),
)
select_multiple("drawer")
return {"FINISHED"}
[docs]
class CamCurvePuzzle(Operator):
"""Generates Puzzle Joints and Interlocks""" # by Alain Pelletier December 2021
[docs]
bl_idname = "object.curve_puzzle"
[docs]
bl_label = "Puzzle Joints"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
diameter: FloatProperty(
name="Tool Diameter",
default=0.003175,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.00005,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
[docs]
finger_amount: IntProperty(
name="Finger Amount",
default=1,
min=0,
max=100,
)
[docs]
stem_size: IntProperty(
name="Size of the Stem",
default=2,
min=1,
max=200,
)
[docs]
width: FloatProperty(
name="Width",
default=0.100,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
height: FloatProperty(
name="Height or Thickness",
default=0.025,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
angle: FloatProperty(
name="Angle A",
default=pi / 4,
min=-10,
max=10,
step=500,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
angleb: FloatProperty(
name="Angle B",
default=pi / 4,
min=-10,
max=10,
step=500,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
radius: FloatProperty(
name="Arc Radius",
default=0.025,
min=0.005,
max=5,
precision=4,
unit="LENGTH",
)
[docs]
interlock_type: EnumProperty(
name="Type of Shape",
items=(
("JOINT", "Joint", "Puzzle Joint interlock"),
("BAR", "Bar", "Bar interlock"),
("ARC", "Arc", "Arc interlock"),
("MULTIANGLE", "Multi angle", "Multi angle joint"),
("CURVEBAR", "Arc Bar", "Arc Bar interlock"),
("CURVEBARCURVE", "Arc Bar Arc", "Arc Bar Arc interlock"),
("CURVET", "T curve", "T curve interlock"),
("T", "T Bar", "T Bar interlock"),
("CORNER", "Corner Bar", "Corner Bar interlock"),
("TILE", "Tile", "Tile interlock"),
("OPENCURVE", "Open Curve", "Corner Bar interlock"),
),
description="Type of interlock",
default="CURVET",
)
[docs]
gender: EnumProperty(
name="Type Gender",
items=(
("MF", "Male-Receptacle", "Male and receptacle"),
("F", "Receptacle only", "Receptacle"),
("M", "Male only", "Male"),
),
description="Type of interlock",
default="MF",
)
[docs]
base_gender: EnumProperty(
name="Base Gender",
items=(
("MF", "Male - Receptacle", "Male - Receptacle"),
("F", "Receptacle", "Receptacle"),
("M", "Male", "Male"),
),
description="Type of interlock",
default="M",
)
[docs]
multiangle_gender: EnumProperty(
name="Multiangle Gender",
items=(
("MMF", "Male Male Receptacle", "M M F"),
("MFF", "Male Receptacle Receptacle", "M F F"),
),
description="Type of interlock",
default="MFF",
)
[docs]
mitre: BoolProperty(
name="Add Mitres",
default=False,
)
[docs]
twist_lock: BoolProperty(
name="Add TwistLock",
default=False,
)
[docs]
twist_thick: FloatProperty(
name="Twist Thickness",
default=0.0047,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
twist_percent: FloatProperty(
name="Twist Neck",
default=0.3,
min=0.1,
max=0.9,
precision=4,
)
[docs]
twist_keep: BoolProperty(
name="Keep Twist Holes",
default=False,
)
[docs]
twist_line: BoolProperty(
name="Add Twist to Bar",
default=False,
)
[docs]
twist_line_amount: IntProperty(
name="Amount of Separators",
default=2,
min=1,
max=600,
)
[docs]
twist_separator: BoolProperty(
name="Add Twist Separator",
default=False,
)
[docs]
twist_separator_amount: IntProperty(
name="Amount of Separators",
default=2,
min=2,
max=600,
)
[docs]
twist_separator_spacing: FloatProperty(
name="Separator Spacing",
default=0.025,
min=-0.004,
max=1.0,
precision=4,
unit="LENGTH",
)
[docs]
twist_separator_edge_distance: FloatProperty(
name="Separator Edge Distance",
default=0.01,
min=0.0005,
max=0.1,
precision=4,
unit="LENGTH",
)
[docs]
tile_x_amount: IntProperty(
name="Amount of X Fingers",
default=2,
min=1,
max=600,
)
[docs]
tile_y_amount: IntProperty(
name="Amount of Y Fingers",
default=2,
min=1,
max=600,
)
[docs]
interlock_amount: IntProperty(
name="Interlock Amount on Curve",
default=2,
min=0,
max=200,
)
[docs]
overcut: BoolProperty(
name="Add Overcut",
default=False,
)
[docs]
overcut_diameter: FloatProperty(
name="Overcut Tool Diameter",
default=0.003175,
min=-0.001,
max=0.5,
precision=4,
unit="LENGTH",
)
[docs]
def draw(self, context):
"""Draws the user interface layout for interlock type properties.
This method is responsible for creating and displaying the layout of
various properties related to different interlock types in the user
interface. It dynamically adjusts the layout based on the selected
interlock type, allowing users to input relevant parameters such as
dimensions, tolerances, and other characteristics specific to the chosen
interlock type.
Args:
context: The context in which the layout is being drawn, typically
provided by the user interface framework.
Returns:
None: This method does not return any value; it modifies the layout
directly.
"""
layout = self.layout
layout.prop(self, "interlock_type")
layout.label(text="Puzzle Joint Definition")
layout.prop(self, "stem_size")
layout.prop(self, "diameter")
layout.prop(self, "finger_tolerance")
if self.interlock_type == "TILE":
layout.prop(self, "tile_x_amount")
layout.prop(self, "tile_y_amount")
else:
layout.prop(self, "finger_amount")
if self.interlock_type != "JOINT" and self.interlock_type != "TILE":
layout.prop(self, "twist_lock")
if self.twist_lock:
layout.prop(self, "twist_thick")
layout.prop(self, "twist_percent")
layout.prop(self, "twist_keep")
layout.prop(self, "twist_line")
if self.twist_line:
layout.prop(self, "twist_line_amount")
layout.prop(self, "twist_separator")
if self.twist_separator:
layout.prop(self, "twist_separator_amount")
layout.prop(self, "twist_separator_spacing")
layout.prop(self, "twist_separator_edge_distance")
if self.interlock_type == "OPENCURVE":
layout.prop(self, "interlock_amount")
layout.separator()
layout.prop(self, "height")
if self.interlock_type == "BAR":
layout.prop(self, "mitre")
if self.interlock_type in ["ARC", "CURVEBARCURVE", "CURVEBAR", "MULTIANGLE", "CURVET"] or (
self.interlock_type == "BAR" and self.mitre
):
if self.interlock_type == "MULTIANGLE":
layout.prop(self, "multiangle_gender")
elif self.interlock_type != "CURVET":
layout.prop(self, "gender")
if not self.mitre:
layout.prop(self, "radius")
layout.prop(self, "angle")
if self.interlock_type == "CURVEBARCURVE" or self.mitre:
layout.prop(self, "angleb")
if self.interlock_type in ["BAR", "CURVEBARCURVE", "CURVEBAR", "T", "CORNER", "CURVET"]:
layout.prop(self, "gender")
if self.interlock_type in ["T", "CURVET"]:
layout.prop(self, "base_gender")
if self.interlock_type == "CURVEBARCURVE":
layout.label(text="Width includes 2 radius and thickness")
layout.prop(self, "width")
if self.interlock_type != "TILE":
layout.prop(self, "overcut")
if self.overcut:
layout.prop(self, "overcut_diameter")
[docs]
def execute(self, context):
"""Execute the puzzle joinery process based on the provided context.
This method processes the selected objects in the given context to
perform various types of puzzle joinery operations. It first checks if
there are any selected objects and if the active object is a curve. If
so, it duplicates the object, applies transformations, and converts it
to a mesh. The method then extracts vertex coordinates and performs
different joinery operations based on the specified interlock type.
Supported interlock types include 'FINGER', 'JOINT', 'BAR', 'ARC',
'CURVEBARCURVE', 'CURVEBAR', 'MULTIANGLE', 'T', 'CURVET', 'CORNER',
'TILE', and 'OPENCURVE'.
Args:
context (Context): The context containing selected objects and the active object.
Returns:
dict: A dictionary indicating the completion status of the operation.
"""
curve_detected = False
print(len(context.selected_objects), "selected object", context.selected_objects)
if len(context.selected_objects) > 0 and context.active_object.type == "CURVE":
curve_detected = True
# bpy.context.object.data.resolution_u = 60
duplicate()
bpy.ops.object.transform_apply(location=True)
obj = context.active_object
bpy.ops.object.convert(target="MESH")
bpy.context.active_object.name = "_tempmesh"
coords = []
for v in obj.data.vertices: # extract X,Y coordinates from the vertices data
coords.append((v.co.x, v.co.y))
remove_multiple("_tmp")
# convert coordinates to shapely LineString datastructure
line = LineString(coords)
remove_multiple("_")
if self.interlock_type == "FINGER":
puzzle_joinery.finger(self.diameter, self.finger_tolerance, stem=self.stem_size)
rename("_puzzle", "receptacle")
puzzle_joinery.finger(self.diameter, 0, stem=self.stem_size)
rename("_puzzle", "finger")
if self.interlock_type == "JOINT":
if self.finger_amount == 0: # cannot be 0 in joints
self.finger_amount = 1
puzzle_joinery.fingers(
self.diameter, self.finger_tolerance, self.finger_amount, stem=self.stem_size
)
if self.interlock_type == "BAR":
if not self.mitre:
puzzle_joinery.bar(
self.width,
self.height,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
twist_keep=self.twist_keep,
twist_line=self.twist_line,
twist_line_amount=self.twist_line_amount,
which=self.gender,
)
else:
puzzle_joinery.mitre(
self.width,
self.height,
self.angle,
self.angleb,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
which=self.gender,
)
elif self.interlock_type == "ARC":
puzzle_joinery.arc(
self.radius,
self.height,
self.angle,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
which=self.gender,
)
elif self.interlock_type == "CURVEBARCURVE":
puzzle_joinery.arc_bar_arc(
self.width,
self.radius,
self.height,
self.angle,
self.angleb,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
twist_keep=self.twist_keep,
twist_line=self.twist_line,
twist_line_amount=self.twist_line_amount,
which=self.gender,
)
elif self.interlock_type == "CURVEBAR":
puzzle_joinery.arc_bar(
self.width,
self.radius,
self.height,
self.angle,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
twist_keep=self.twist_keep,
twist_line=self.twist_line,
twist_line_amount=self.twist_line_amount,
which=self.gender,
)
elif self.interlock_type == "MULTIANGLE":
puzzle_joinery.multiangle(
self.radius,
self.height,
pi / 3,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
combination=self.multiangle_gender,
)
elif self.interlock_type == "T":
puzzle_joinery.t(
self.width,
self.height,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
combination=self.gender,
base_gender=self.base_gender,
)
elif self.interlock_type == "CURVET":
puzzle_joinery.curved_t(
self.width,
self.height,
self.radius,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
combination=self.gender,
base_gender=self.base_gender,
)
elif self.interlock_type == "CORNER":
puzzle_joinery.t(
self.width,
self.height,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
tneck=self.twist_percent,
tthick=self.twist_thick,
combination=self.gender,
base_gender=self.base_gender,
corner=True,
)
elif self.interlock_type == "TILE":
puzzle_joinery.tile(
self.diameter,
self.finger_tolerance,
self.tile_x_amount,
self.tile_y_amount,
stem=self.stem_size,
)
elif self.interlock_type == "OPENCURVE" and curve_detected:
puzzle_joinery.open_curve(
line,
self.height,
self.diameter,
self.finger_tolerance,
self.finger_amount,
stem=self.stem_size,
twist=self.twist_lock,
t_neck=self.twist_percent,
t_thick=self.twist_thick,
which=self.gender,
twist_amount=self.interlock_amount,
twist_keep=self.twist_keep,
)
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
if self.twist_lock and self.twist_separator:
joinery.interlock_twist_separator(
self.height,
self.twist_thick,
self.twist_separator_amount,
self.twist_separator_spacing,
self.twist_separator_edge_distance,
finger_play=self.finger_tolerance,
percentage=self.twist_percent,
)
remove_doubles()
add_overcut(self.overcut_diameter, self.overcut)
return {"FINISHED"}
[docs]
class CamCurveGear(Operator):
"""Generates Involute Gears // version 1.1 by Leemon Baird, 2011, Leemon@Leemon.com
http://www.thingiverse.com/thing:5505""" # ported by Alain Pelletier January 2022
[docs]
bl_idname = "object.curve_gear"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
tooth_spacing: FloatProperty(
name="Distance per Tooth",
default=0.010,
min=0.001,
max=1.0,
precision=4,
unit="LENGTH",
)
[docs]
tooth_amount: IntProperty(
name="Amount of Teeth",
default=7,
min=4,
)
[docs]
spoke_amount: IntProperty(
name="Amount of Spokes",
default=4,
min=0,
)
[docs]
hole_diameter: FloatProperty(
name="Hole Diameter",
default=0.003175,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
rim_size: FloatProperty(
name="Rim Size",
default=0.003175,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
hub_diameter: FloatProperty(
name="Hub Diameter",
default=0.005,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
[docs]
pressure_angle: FloatProperty(
name="Pressure Angle",
default=radians(20),
min=0.001,
max=pi / 2,
precision=4,
step=100,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
clearance: FloatProperty(
name="Clearance",
default=0.00,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
[docs]
backlash: FloatProperty(
name="Backlash",
default=0.0,
min=0.0,
max=0.1,
precision=4,
unit="LENGTH",
)
[docs]
rack_height: FloatProperty(
name="Rack Height",
default=0.012,
min=0.001,
max=1,
precision=4,
unit="LENGTH",
)
[docs]
rack_tooth_per_hole: IntProperty(
name="Teeth per Mounting Hole",
default=7,
min=2,
)
[docs]
gear_type: EnumProperty(
name="Type of Gear",
items=(("PINION", "Pinion", "Circular Gear"), ("RACK", "Rack", "Straight Rack")),
description="Type of gear",
default="PINION",
)
[docs]
def draw(self, context):
"""Draw the user interface properties for gear settings.
This method sets up the layout for various gear parameters based on the
selected gear type. It dynamically adds properties to the layout for
different gear types, allowing users to input specific values for gear
design. The properties include gear type, tooth spacing, tooth amount,
hole diameter, pressure angle, and backlash. Additional properties are
displayed if the gear type is 'PINION' or 'RACK'.
Args:
context: The context in which the layout is being drawn.
"""
layout = self.layout
layout.prop(self, "gear_type")
layout.prop(self, "tooth_spacing")
layout.prop(self, "tooth_amount")
layout.prop(self, "hole_diameter")
layout.prop(self, "pressure_angle")
layout.prop(self, "backlash")
if self.gear_type == "PINION":
layout.prop(self, "clearance")
layout.prop(self, "spoke_amount")
layout.prop(self, "rim_size")
layout.prop(self, "hub_diameter")
elif self.gear_type == "RACK":
layout.prop(self, "rack_height")
layout.prop(self, "rack_tooth_per_hole")
[docs]
def execute(self, context):
"""Execute the gear generation process based on the specified gear type.
This method checks the type of gear to be generated (either 'PINION' or
'RACK') and calls the appropriate function from the `involute_gear`
module to create the gear or rack with the specified parameters. The
parameters include tooth spacing, number of teeth, hole diameter,
pressure angle, clearance, backlash, rim size, hub diameter, and spoke
amount for pinion gears, and additional parameters for rack gears.
Args:
context: The context in which the execution is taking place.
Returns:
dict: A dictionary indicating that the operation has finished with a key
'FINISHED'.
"""
if self.gear_type == "PINION":
involute_gear.gear(
mm_per_tooth=self.tooth_spacing,
number_of_teeth=self.tooth_amount,
hole_diameter=self.hole_diameter,
pressure_angle=self.pressure_angle,
clearance=self.clearance,
backlash=self.backlash,
rim_size=self.rim_size,
hub_diameter=self.hub_diameter,
spokes=self.spoke_amount,
)
elif self.gear_type == "RACK":
involute_gear.rack(
mm_per_tooth=self.tooth_spacing,
number_of_teeth=self.tooth_amount,
pressure_angle=self.pressure_angle,
height=self.rack_height,
backlash=self.backlash,
tooth_per_hole=self.rack_tooth_per_hole,
hole_diameter=self.hole_diameter,
)
return {"FINISHED"}