First Macro drawing for FreeCad is just to check it can be done easily. It works. Here is the first Macro:
import FreeCAD as App
import Part
import math
try:
from PySide import QtCore
except ImportError:
from PySide6 import QtCore
doc_name = "Chain_Sprocket_13T_15T_Simulation"
doc = App.newDocument(doc_name)
# --- 1. System Parameters ---
t1 = 13 # Left Sprocket Teeth
t2 = 15 # Right Sprocket Teeth
pitch = 9.525
roller_diameter = 6.35
bore_diameter = 10.0
plate_thickness = 5.3
num_links = 32 # Increased chain length to accommodate larger sprocket
# Chain Link Specs
inner_width = 5.72
pin_diameter = 3.28
link_plate_thickness = 1.3
# Calculated Radii
R1 = pitch / (2 * math.sin(math.pi / t1))
R2 = pitch / (2 * math.sin(math.pi / t2))
root1 = R1 - (roller_diameter / 2.0)
root2 = R2 - (roller_diameter / 2.0)
# --- 2. Iterative Solver for Exact Center Distance (C) ---
# A closed chain requires a geometrically perfect loop length.
L_target = num_links * pitch
C = 85.0 # Initial guess
for _ in range(50): # Newton-Raphson method for exact distance
theta = math.asin((R2 - R1) / C)
L_calc = 2 * C * math.cos(theta) + R1 * (math.pi - 2*theta) + R2 * (math.pi + 2*theta)
C -= (L_calc - L_target) / (2 * math.cos(theta))
# Final Path Geometry Variables
theta = math.asin((R2 - R1) / C)
L_str = C * math.cos(theta)
L_arc1 = R1 * (math.pi - 2 * theta)
L_arc2 = R2 * (math.pi + 2 * theta)
L_total = L_arc1 + L_arc2 + 2 * L_str
# Tangent Points defining the path layout
T1top = App.Vector(-R1 * math.sin(theta), R1 * math.cos(theta), 0)
T1bot = App.Vector(-R1 * math.sin(theta), -R1 * math.cos(theta), 0)
T2top = App.Vector(C - R2 * math.sin(theta), R2 * math.cos(theta), 0)
T2bot = App.Vector(C - R2 * math.sin(theta), -R2 * math.cos(theta), 0)
# --- 3. Dynamic Sprocket Generator ---
def create_sprocket(name, teeth, R_pitch, R_root):
R_outer = R_pitch + 2.3 # Scaled OD
base_cylinder = Part.makeCylinder(R_outer, plate_thickness)
scale_y = 0.85
# Parameterized Wedge Cutter to adapt to different radii
p_root = App.Vector(R_root, 0, 0)
p_A = App.Vector(R_pitch - 1.6, 2.77 * scale_y, 0)
p_B = App.Vector(R_pitch - 1.6, -2.77 * scale_y, 0)
p_M_up = App.Vector(R_pitch + 1.75, 4.5 * scale_y, 0)
p_M_low = App.Vector(R_pitch + 1.75, -4.5 * scale_y, 0)
p_3 = App.Vector(R_pitch + 5.1, 5.5 * scale_y, 0)
p_4 = App.Vector(R_pitch + 5.1, -5.5 * scale_y, 0)
arc_root = Part.Arc(p_B, p_root, p_A).toShape()
arc_up = Part.Arc(p_A, p_M_up, p_3).toShape()
arc_low = Part.Arc(p_4, p_M_low, p_B).toShape()
line_out = Part.LineSegment(p_3, p_4).toShape()
wedge_wire = Part.Wire([arc_root, arc_up, line_out, arc_low])
wedge_solid = Part.Face(wedge_wire).extrude(App.Vector(0, 0, plate_thickness + 2.0))
wedge_solid.translate(App.Vector(0, 0, -1.0))
sprocket = base_cylinder
for i in range(teeth):
cutter = wedge_solid.copy()
cutter.rotate(App.Vector(0, 0, 0), App.Vector(0, 0, 1), i * (360.0 / teeth))
sprocket = sprocket.cut(cutter)
bore = Part.makeCylinder(bore_diameter / 2.0, plate_thickness + 2.0)
bore.translate(App.Vector(0, 0, -1.0))
obj = doc.addObject("Part::Feature", name)
obj.Shape = sprocket.cut(bore)
obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
return obj
sprocket_1 = create_sprocket("Sprocket_13T", t1, R1, root1)
sprocket_2 = create_sprocket("Sprocket_15T", t2, R2, root2)
sprocket_2.Placement.Base = App.Vector(C, 0, 0)
# --- 4. Chain Link Geometry ---
def create_link_plate(thickness, z_offset):
r = 4.0
cyl1 = Part.makeCylinder(r, thickness, App.Vector(0,0,z_offset))
cyl2 = Part.makeCylinder(r, thickness, App.Vector(pitch,0,z_offset))
box = Part.makeBox(pitch, r*2, thickness, App.Vector(0, -r, z_offset))
return cyl1.fuse(cyl2).fuse(box)
inner_plate_z1 = plate_thickness/2 + inner_width/2
inner_plate_z2 = plate_thickness/2 - inner_width/2 - link_plate_thickness
p1 = create_link_plate(link_plate_thickness, inner_plate_z1)
p2 = create_link_plate(link_plate_thickness, inner_plate_z2)
r1 = Part.makeCylinder(roller_diameter/2, inner_width, App.Vector(0,0, plate_thickness/2 - inner_width/2))
r2 = Part.makeCylinder(roller_diameter/2, inner_width, App.Vector(pitch,0, plate_thickness/2 - inner_width/2))
inner_link_shape = p1.fuse(p2).fuse(r1).fuse(r2)
outer_plate_z1 = inner_plate_z1 + link_plate_thickness + 0.2
outer_plate_z2 = inner_plate_z2 - link_plate_thickness - 0.2
op1 = create_link_plate(link_plate_thickness, outer_plate_z1)
op2 = create_link_plate(link_plate_thickness, outer_plate_z2)
pin_len = (outer_plate_z1 - outer_plate_z2) + link_plate_thickness + 1.0
pin1 = Part.makeCylinder(pin_diameter/2, pin_len, App.Vector(0,0, outer_plate_z2 - 0.5))
pin2 = Part.makeCylinder(pin_diameter/2, pin_len, App.Vector(pitch,0, outer_plate_z2 - 0.5))
outer_link_shape = op1.fuse(op2).fuse(pin1).fuse(pin2)
links = []
for i in range(num_links):
link_obj = doc.addObject("Part::Feature", f"Link_{i:02d}")
link_obj.Shape = inner_link_shape if i % 2 == 0 else outer_link_shape
link_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7) if i % 2 == 0 else (0.5, 0.5, 0.5)
links.append(link_obj)
# --- 5. Complex Tangent Kinematics ---
def get_path_pos(d):
d = d % L_total
if d < L_arc1:
# Wrap CCW around S1
angle = math.pi/2 + theta + (d / R1)
return App.Vector(R1 * math.cos(angle), R1 * math.sin(angle), 0)
elif d < L_arc1 + L_str:
# Straight bottom return
dist = d - L_arc1
return T1bot + (T2bot - T1bot) * (dist / L_str)
elif d < L_arc1 + L_str + L_arc2:
# Wrap CCW around S2
dist = d - (L_arc1 + L_str)
angle = -math.pi/2 - theta + (dist / R2)
return App.Vector(C + R2 * math.cos(angle), R2 * math.sin(angle), 0)
else:
# Straight top feed
dist = d - (L_arc1 + L_str + L_arc2)
return T2top + (T1top - T2top) * (dist / L_str)
# --- 6. Animation Engine ---
global anim_offset
anim_offset = 0.0
# Initial phase alignments to nest teeth into the chain
offset_1 = math.degrees(theta) - 13.8
offset_2 = math.degrees(-theta) + 12.0
def update_simulation():
global anim_offset
anim_offset += 1.0 # Simulation speed.
# Track discrete link paths
for i, link in enumerate(links):
d1 = (i * pitch + anim_offset) % L_total
d2 = ((i + 1) * pitch + anim_offset) % L_total
pos1 = get_path_pos(d1)
pos2 = get_path_pos(d2)
yaw = math.degrees(math.atan2(pos2.y - pos1.y, pos2.x - pos1.x))
link.Placement = App.Placement(pos1, App.Rotation(App.Vector(0,0,1), yaw))
# Rotate independently according to varied geometries and chain speed
sprocket_1.Placement.Rotation = App.Rotation(App.Vector(0,0,1), math.degrees(anim_offset / R1) + offset_1)
sprocket_2.Placement.Rotation = App.Rotation(App.Vector(0,0,1), math.degrees(anim_offset / R2) + offset_2)
App.Gui.updateGui()
doc.recompute()
try:
import FreeCADGui as Gui
Gui.activeDocument().activeView().viewAxometric()
Gui.SendMsgToActiveView("ViewFit")
App.sim_timer = QtCore.QTimer()
App.sim_timer.timeout.connect(update_simulation)
App.sim_timer.start(10)
App.Console.PrintMessage("Simulation running ........ type 'App.sim_timer.stop()' in Python console to halt.\n")
except Exception:
pass
GOAT INDUSTRIES
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.