blendercam/scripts/addons/cam/curvecamcreate.py

1577 wiersze
55 KiB
Python

"""BlenderCAM 'curvecamcreate.py' © 2021, 2022 Alain Pelletier
Operators to create a number of predefined curve objects.
"""
from math import (
degrees,
hypot,
pi,
radians
)
from shapely.geometry import (
LineString,
MultiLineString,
)
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
)
from bpy.types import Operator
from . import (
involute_gear,
joinery,
puzzle_joinery,
simple,
utils,
)
class CamCurveHatch(Operator):
"""Perform Hatch Operation on Single or Multiple Curves""" # by Alain Pelletier September 2021
bl_idname = "object.curve_hatch"
bl_label = "CrossHatch Curve"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
angle: FloatProperty(
name="Angle",
default=0,
min=-pi/2,
max=pi/2,
precision=4,
subtype="ANGLE",
)
distance: FloatProperty(
name="Spacing",
default=0.015,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
offset: FloatProperty(
name="Margin",
default=0.001,
min=-1.0,
max=3.0,
precision=4,
unit="LENGTH",
)
height: FloatProperty(
name="Height",
default=0.000,
min=-1.0,
max=1.0,
precision=4,
unit="LENGTH",
)
amount: IntProperty(
name="Amount",
default=10,
min=1,
max=10000,
)
hull: BoolProperty(
name="Convex Hull",
default=False,
)
contour: BoolProperty(
name="Contour Curve",
default=False,
)
contour_separate: BoolProperty(
name="Contour Separate",
default=False,
)
pocket_type: EnumProperty(
name='Type Pocket',
items=(
('BOUNDS', 'Makes a bounds rectangle', 'Makes a bounding square'),
('POCKET', 'Pocket', 'Makes a pocket inside a closed loop')
),
description='Type of pocket',
default='BOUNDS',
)
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type in ['CURVE', 'FONT']
def draw(self, context):
layout = self.layout
layout.prop(self, 'angle')
layout.prop(self, 'distance')
layout.prop(self, 'offset')
layout.prop(self, 'height')
layout.prop(self, 'pocket_type')
if self.pocket_type == 'POCKET':
if self.hull:
layout.prop(self, 'hull')
layout.prop(self, 'contour')
if self.contour:
layout.prop(self, 'contour_separate')
else:
layout.prop(self, 'hull')
if self.contour:
layout.prop(self, 'contour')
def execute(self, context):
simple.remove_multiple("crosshatch")
ob = context.active_object
ob.select_set(True)
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
depth = ob.location[2]
if self.hull:
bpy.ops.object.convex_hull()
simple.active_name('crosshatch_hull')
from shapely import affinity
shapes = utils.curveToShapely(bpy.context.active_object)
for s in shapes.geoms:
coords = []
minx, miny, maxx, maxy = s.bounds
minx -= self.offset
miny -= self.offset
maxx += self.offset
maxy += self.offset
centery = (miny + maxy) / 2
height = maxy - miny
width = maxx - minx
centerx = (minx+maxx) / 2
diagonal = hypot(width, height)
simple.add_bound_rectangle(
minx, miny, maxx, maxy, 'crosshatch_bound')
amount = int(2*diagonal/self.distance) + 1
for x in range(amount):
distance = x * self.distance - diagonal
coords.append(((distance, diagonal + 0.5),
(distance, -diagonal - 0.5)))
# create a multilinestring shapely object
lines = MultiLineString(coords)
rotated = affinity.rotate(
lines, self.angle, use_radians=True) # rotate using shapely
translated = affinity.translate(
rotated, xoff=centerx, yoff=centery) # move using shapely
simple.make_active('crosshatch_bound')
bounds = simple.active_to_shapely_poly()
if self.pocket_type == 'BOUNDS':
# Shapely detects intersections with the square bounds
xing = translated.intersection(bounds)
else:
xing = translated.intersection(s.buffer(self.offset))
# Shapely detects intersections with the original curve or hull
utils.shapelyToCurve('crosshatch_lines', xing, self.height)
# remove temporary shapes
simple.remove_multiple('crosshatch_bound')
simple.remove_multiple('crosshatch_hull')
simple.select_multiple('crosshatch')
bpy.ops.object.editmode_toggle()
bpy.ops.curve.select_all(action='SELECT')
bpy.ops.curve.subdivide()
bpy.ops.object.editmode_toggle()
simple.join_multiple('crosshatch')
simple.remove_doubles()
# add contour
if self.contour:
simple.deselect()
bpy.context.view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.object.silhouete_offset(offset=self.offset)
if self.contour_separate:
simple.active_name('contour_hatch')
simple.deselect()
else:
simple.active_name('crosshatch_contour')
simple.join_multiple('crosshatch')
simple.remove_doubles()
return {'FINISHED'}
class CamCurvePlate(Operator):
"""Perform Generates Rounded Plate with Mounting Holes""" # by Alain Pelletier Sept 2021
bl_idname = "object.curve_plate"
bl_label = "Sign Plate"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
radius: FloatProperty(
name="Corner Radius",
default=.025,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
width: FloatProperty(
name="Width of Plate",
default=0.3048,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
height: FloatProperty(
name="Height of Plate",
default=0.457,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hole_diameter: FloatProperty(
name="Hole Diameter",
default=0.01,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hole_tolerance: FloatProperty(
name="Hole V Tolerance",
default=0.005,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hole_vdist: FloatProperty(
name="Hole Vert Distance",
default=0.400,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hole_hdist: FloatProperty(
name="Hole Horiz Distance",
default=0,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hole_hamount: IntProperty(
name="Hole Horiz Amount",
default=1,
min=0,
max=50,
)
resolution: IntProperty(
name="Spline Resolution",
default=50,
min=3,
max=150,
)
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',
)
def draw(self, context):
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')
def execute(self, context):
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))
simple.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))
simple.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))
simple.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))
simple.active_name("_circ_RT")
bpy.context.object.data.resolution_u = self.resolution
# select the circles for the four corners
simple.select_multiple("_circ")
# perform hull operation on the four corner circles
utils.polygonConvexHull(context)
simple.active_name("plate_base")
simple.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
simple.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))
simple.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))
simple.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))
simple.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))
simple.active_name("_circ_RT")
bpy.context.object.data.resolution_u = self.resolution
simple.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)
simple.active_name("_base")
simple.difference("_", "_base")
simple.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)
simple.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)
simple.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)
simple.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)
simple.active_name("_bev_RT")
bpy.context.object.data.resolution_u = self.resolution
simple.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)
simple.active_name("_base")
simple.difference("_", "_base")
simple.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))
simple.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))
simple.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
simple.select_multiple("_hole")
utils.polygonConvexHull(context)
simple.active_name("plate_hole")
simple.move(y=-self.hole_vdist / 2)
simple.duplicate(y=self.hole_vdist)
simple.remove_multiple("_hole") # remove temporary holes
simple.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)
simple.duplicate()
bpy.context.object.location[0] = dist
simple.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
simple.duplicate()
bpy.context.object.location[0] = -dist
else:
simple.duplicate()
bpy.context.object.location[0] = dist
simple.duplicate()
bpy.context.object.location[0] = -dist
simple.join_multiple("plate_hole") # join the holes together
# select everything starting with plate_
simple.select_multiple("plate_")
# Make the plate base active
bpy.context.view_layer.objects.active = bpy.data.objects['plate_base']
# Remove holes from the base
utils.polygonBoolean(context, "DIFFERENCE")
simple.remove_multiple("plate_") # Remove temporary base and holes
simple.remove_multiple("_")
simple.active_name("plate")
bpy.context.active_object.select_set(True)
bpy.ops.object.curve_remove_doubles()
return {'FINISHED'}
class CamCurveFlatCone(Operator):
"""Perform Generates Rounded Plate with Mounting Holes""" # by Alain Pelletier Sept 2021
bl_idname = "object.curve_flat_cone"
bl_label = "Cone Flat Calculator"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
small_d: FloatProperty(
name="Small Diameter",
default=.025,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
large_d: FloatProperty(
name="Large Diameter",
default=0.3048,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
height: FloatProperty(
name="Height of Cone",
default=0.457,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
tab: FloatProperty(
name="Tab Witdh",
default=0.01,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
intake: FloatProperty(
name="Intake Diameter",
default=0,
min=0,
max=0.200,
precision=4,
unit="LENGTH",
)
intake_skew: FloatProperty(
name="Intake Skew",
default=1,
min=0.1,
max=4,
)
resolution: IntProperty(
name="Resolution",
default=12,
min=5,
max=200,
)
def execute(self, context):
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)
simple.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')
simple.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')
simple.move(x=ab-3*self.intake/2)
simple.rotate(angle/2)
bpy.context.object.data.resolution_u = self.resolution
simple.union("_segment")
simple.active_name('flat_cone')
return {'FINISHED'}
class CamCurveMortise(Operator):
"""Generates Mortise Along a Curve""" # by Alain Pelletier December 2021
bl_idname = "object.curve_mortise"
bl_label = "Mortise"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
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",
)
min_finger_size: FloatProperty(
name="Minimum Finger Size",
default=0.0025,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
plate_thickness: FloatProperty(
name="Drawer Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
unit="LENGTH",
)
side_height: FloatProperty(
name="Side Height",
default=0.05,
min=0.001,
max=3.0,
unit="LENGTH",
)
flex_pocket: FloatProperty(
name="Flex Pocket",
default=0.004,
min=0.000,
max=1.0,
unit="LENGTH",
)
top_bottom: BoolProperty(
name="Side Top & Bottom Fingers",
default=True,
)
opencurve: BoolProperty(
name="OpenCurve",
default=False,
)
adaptive: FloatProperty(
name="Adaptive Angle Threshold",
default=0.0,
min=0.000,
max=2,
subtype="ANGLE",
unit="ROTATION",
)
double_adaptive: BoolProperty(
name="Double Adaptive Pockets",
default=False,
)
@classmethod
def poll(cls, context):
return context.active_object is not None and (context.active_object.type in ['CURVE', 'FONT'])
def execute(self, context):
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')
simple.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)
simple.remove_multiple("-converted")
utils.shapelyToCurve('-converted_curve', line, 0.0)
shapes = utils.curveToShapely(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)
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)
simple.remove_multiple('_')
return {'FINISHED'}
class CamCurveInterlock(Operator):
"""Generates Interlock Along a Curve""" # by Alain Pelletier December 2021
bl_idname = "object.curve_interlock"
bl_label = "Interlock"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
finger_size: FloatProperty(
name="Finger Size",
default=0.015,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
plate_thickness: FloatProperty(
name="Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
unit="LENGTH",
)
opencurve: BoolProperty(
name="OpenCurve",
default=False,
)
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',
)
finger_amount: IntProperty(
name="Finger Amount",
default=2,
min=1,
max=100,
)
tangent_angle: FloatProperty(
name="Tangent Deviation",
default=0.0,
min=0.000,
max=2,
subtype="ANGLE",
unit="ROTATION",
)
fixed_angle: FloatProperty(
name="Fixed Angle",
default=0.0,
min=0.000,
max=2,
subtype="ANGLE",
unit="ROTATION",
)
def execute(self, context):
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
simple.duplicate()
obj = context.active_object
bpy.ops.object.convert(target='MESH')
simple.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)
simple.remove_multiple("-converted")
utils.shapelyToCurve('-converted_curve', line, 0.0)
shapes = utils.curveToShapely(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'}
class CamCurveDrawer(Operator):
"""Generates Drawers""" # by Alain Pelletier December 2021 inspired by The Drawinator
bl_idname = "object.curve_drawer"
bl_label = "Drawer"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
depth: FloatProperty(
name="Drawer Depth",
default=0.2,
min=0,
max=1.0,
precision=4,
unit="LENGTH",
)
width: FloatProperty(
name="Drawer Width",
default=0.125,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
height: FloatProperty(
name="Drawer Height",
default=0.07,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
finger_size: FloatProperty(
name="Maximum Finger Size",
default=0.015,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.000045,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
finger_inset: FloatProperty(
name="Finger Inset",
default=0.0,
min=0.0,
max=0.01,
precision=4,
unit="LENGTH",
)
drawer_plate_thickness: FloatProperty(
name="Drawer Plate Thickness",
default=0.00477,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
drawer_hole_diameter: FloatProperty(
name="Drawer Hole Diameter",
default=0.02,
min=0.00001,
max=0.5,
precision=4,
unit="LENGTH",
)
drawer_hole_offset: FloatProperty(
name="Drawer Hole Offset",
default=0.0,
min=-0.5,
max=0.5,
precision=4,
unit="LENGTH",
)
overcut: BoolProperty(
name="Add Overcut",
default=False,
)
overcut_diameter: FloatProperty(
name="Overcut Tool Diameter",
default=0.003175,
min=-0.001,
max=0.5,
precision=4,
unit="LENGTH",
)
def draw(self, context):
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')
def execute(self, context):
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)
simple.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)
simple.make_active('_wfa')
fronth = bpy.context.active_object
simple.make_active('_back')
finger_pair.select_set(True)
fronth.select_set(True)
bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE')
simple.remove_multiple("_finger_pair")
simple.active_name("drawer_back")
simple.remove_doubles()
simple.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))
simple.active_name("_circ")
front_hole = bpy.context.active_object
simple.make_active('drawer_back')
front_hole.select_set(True)
bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE')
simple.active_name("drawer_front")
simple.remove_doubles()
simple.add_overcut(self.overcut_diameter, self.overcut)
# place back and front side by side
simple.make_active('drawer_front')
bpy.ops.transform.transform(
mode='TRANSLATION', value=(0.0, 2 * self.height, 0.0, 0.0))
simple.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)
simple.make_active('_side')
finger_pair.select_set(True)
fronth.select_set(True)
bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE')
simple.active_name("drawer_side")
simple.remove_doubles()
simple.add_overcut(self.overcut_diameter, self.overcut)
simple.remove_multiple('_finger_pair')
# make bottom
simple.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)})
simple.active_name("_wfb0")
joinery.finger_pair("_wfb0", 0, self.depth -
self.drawer_plate_thickness)
simple.active_name('_bot_fingers')
simple.difference('_bot', '_bottom')
simple.rotate(pi/2)
joinery.finger_pair("_wfb0", 0, self.width -
self.drawer_plate_thickness - self.finger_inset * 2)
simple.active_name('_bot_fingers')
simple.difference('_bot', '_bottom')
simple.active_name("drawer_bottom")
simple.remove_doubles()
simple.add_overcut(self.overcut_diameter, self.overcut)
# cleanup all temp polygons
simple.remove_multiple("_")
# move side and bottom to location
simple.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))
simple.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))
simple.select_multiple('drawer')
return {'FINISHED'}
class CamCurvePuzzle(Operator):
"""Generates Puzzle Joints and Interlocks""" # by Alain Pelletier December 2021
bl_idname = "object.curve_puzzle"
bl_label = "Puzzle Joints"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
diameter: FloatProperty(
name="Tool Diameter",
default=0.003175,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
finger_tolerance: FloatProperty(
name="Finger Play Room",
default=0.00005,
min=0,
max=0.003,
precision=4,
unit="LENGTH",
)
finger_amount: IntProperty(
name="Finger Amount",
default=1,
min=0,
max=100,
)
stem_size: IntProperty(
name="Size of the Stem",
default=2,
min=1,
max=200,
)
width: FloatProperty(
name="Width",
default=0.100,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
height: FloatProperty(
name="Height or Thickness",
default=0.025,
min=0.005,
max=3.0,
precision=4,
unit="LENGTH",
)
angle: FloatProperty(
name="Angle A",
default=pi/4,
min=-10,
max=10,
subtype="ANGLE",
unit="ROTATION",
)
angleb: FloatProperty(
name="Angle B",
default=pi/4,
min=-10,
max=10,
subtype="ANGLE",
unit="ROTATION",
)
radius: FloatProperty(
name="Arc Radius",
default=0.025,
min=0.005,
max=5,
precision=4,
unit="LENGTH",
)
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',
)
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',
)
base_gender: EnumProperty(
name='Base Gender',
items=(
('MF', 'Male - Receptacle', 'Male - Receptacle'),
('F', 'Receptacle', 'Receptacle'),
('M', 'Male', 'Male')
),
description='Type of interlock',
default='M',
)
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',
)
mitre: BoolProperty(
name="Add Mitres",
default=False,
)
twist_lock: BoolProperty(
name="Add TwistLock",
default=False,
)
twist_thick: FloatProperty(
name="Twist Thickness",
default=0.0047,
min=0.001,
max=3.0,
precision=4,
unit="LENGTH",
)
twist_percent: FloatProperty(
name="Twist Neck",
default=0.3,
min=0.1,
max=0.9,
precision=4,
)
twist_keep: BoolProperty(
name="Keep Twist Holes",
default=False,
)
twist_line: BoolProperty(
name="Add Twist to Bar",
default=False,
)
twist_line_amount: IntProperty(
name="Amount of Separators",
default=2,
min=1,
max=600,
)
twist_separator: BoolProperty(
name="Add Twist Separator",
default=False,
)
twist_separator_amount: IntProperty(
name="Amount of Separators",
default=2,
min=2,
max=600,
)
twist_separator_spacing: FloatProperty(
name="Separator Spacing",
default=0.025,
min=-0.004,
max=1.0,
precision=4,
unit="LENGTH",
)
twist_separator_edge_distance: FloatProperty(
name="Separator Edge Distance",
default=0.01,
min=0.0005,
max=0.1,
precision=4,
unit="LENGTH",
)
tile_x_amount: IntProperty(
name="Amount of X Fingers",
default=2,
min=1,
max=600,
)
tile_y_amount: IntProperty(
name="Amount of Y Fingers",
default=2,
min=1,
max=600,
)
interlock_amount: IntProperty(
name="Interlock Amount on Curve",
default=2,
min=0,
max=200,
)
overcut: BoolProperty(
name="Add Overcut",
default=False,
)
overcut_diameter: FloatProperty(
name="Overcut Tool Diameter",
default=0.003175,
min=-0.001,
max=0.5,
precision=4,
unit="LENGTH",
)
def draw(self, context):
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')
def execute(self, context):
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
simple.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))
simple.remove_multiple('_tmp')
# convert coordinates to shapely LineString datastructure
line = LineString(coords)
simple.remove_multiple("_")
if self.interlock_type == 'FINGER':
puzzle_joinery.finger(
self.diameter, self.finger_tolerance, stem=self.stem_size)
simple.rename('_puzzle', 'receptacle')
puzzle_joinery.finger(self.diameter, 0, stem=self.stem_size)
simple.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.arcbararc(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.arcbar(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)
simple.remove_doubles()
simple.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)
simple.remove_doubles()
simple.add_overcut(self.overcut_diameter, self.overcut)
return {'FINISHED'}
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
bl_idname = "object.curve_gear"
bl_label = "Gears"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
tooth_spacing: FloatProperty(
name="Distance per Tooth",
default=0.010,
min=0.001,
max=1.0,
precision=4,
unit="LENGTH",
)
tooth_amount: IntProperty(
name="Amount of Teeth",
default=7,
min=4,
)
spoke_amount: IntProperty(
name="Amount of Spokes",
default=4,
min=0,
)
hole_diameter: FloatProperty(
name="Hole Diameter",
default=0.003175,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
rim_size: FloatProperty(
name="Rim Size",
default=0.003175,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
hub_diameter: FloatProperty(
name="Hub Diameter",
default=0.005,
min=0,
max=3.0,
precision=4,
unit="LENGTH",
)
pressure_angle: FloatProperty(
name="Pressure Angle",
default=radians(20),
min=0.001,
max=pi/2,
precision=4,
subtype="ANGLE",
unit="ROTATION",
)
clearance: FloatProperty(
name="Clearance",
default=0.00,
min=0,
max=0.1,
precision=4,
unit="LENGTH",
)
backlash: FloatProperty(
name="Backlash",
default=0.0,
min=0.0,
max=0.1,
precision=4,
unit="LENGTH",
)
rack_height: FloatProperty(
name="Rack Height",
default=0.012,
min=0.001,
max=1,
precision=4,
unit="LENGTH",
)
rack_tooth_per_hole: IntProperty(
name="Teeth per Mounting Hole",
default=7,
min=2,
)
gear_type: EnumProperty(
name='Type of Gear',
items=(
('PINION', 'Pinion', 'Circular Gear'),
('RACK', 'Rack', 'Straight Rack')
),
description='Type of gear',
default='PINION',
)
def draw(self, context):
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')
def execute(self, context):
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'}