The plan is to create a simulated FreeCAD model so that all the geometry can be checked before going ahead into the fabrication stage. Parts will be made and arranged by means of Python macros created with the help of an LLM. So far so good.
Currently creating a motion simulated CAD drawing with an LLM
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
The plan is to create a simulated FreeCAD model so that all the geometry can be checked before going ahead into the fabrication stage. Parts will be made and arranged by means of Python macros created with the help of an LLM. So far so good.
All the basic features of the mechanism are now complete. The machine now accepts a seedling into one of the carousel tubes and plants it accurately into the soil. The seedling produces a nice yellow flower.

Nearly 3 weeks into the project and the main components for the system are now in place, with a second duck bill ready to put in position. There's also some kind of flap disc needed under the carousel tubes to quickly release the seedlings into the duck bill hopper. The single carousel should be able to feed both duck bill systems simultaneously, which makes the design simpler.

The duck bill is now opened by a hydraulic master cylinder and slave, similar to a vehicle clutch. Cables would have been a bit too tightly curved. The pushrod on the master cylinder wont tolerate sideways forces, so it needs an inline lateral interface for it to work of the cam disc. The duck bills need a couple of springs to bring them back to the closed position. The axle between the two drive wheels had to be cut out and raised up via chain and sprockets (coloured green) to allow the main duck bill arm to move without putting a horrible kink in it. The LLM managed to get a flexible pipe between the master and slave, which is pretty cool.

This mechanism was created and tuned with an LLM to primarily try and get the transplanting distance as close to 285mm as possible, it's currently 289.6 and make sure the duck bill, shown in blue, does not try and 'plough the earth' ie it's speed when it penetrates the soil is as close to zero relative to the soil in the left - right axis. Other than that, I was able to do further tuning to get the tip of the duck bill to trace an oval pathway shown in dashed blue and get that trace upright rather than slanting to one side.
The whole of this diagram is a Macro and is basically created through simple maths, though far too extensive for any normal human to put together manually. Any of the components shown can be exported as .dxf etc for laser cutting or other formats for 3D printing.
Compared to trying to create this mechanism by 'normal' CAD usage, I'd estimate using the LLM is now about 100 times faster than manually trying to tune the various arm lengths. For example, the LLM was able to move the various pivot points to get the duck bill tip pathway vertical and get the duck bill to match the soil speed in minutes rather than days.
The code is now about 10,000 lines long, mostly occupied by a 8,000 line base 64 encoded chunk representing another imported file. Without this import, the code would be about 3,000 lines long. See the Github repo for the latest files, just import the .FCMacro files as a macro into FreeCAD and you can see and the animation as above. If you want to edit the file - use an LLM !!
Part of a planting machine this assembly was modeled with the help of an LLM by using Python macros. The Macro is mechanically reproducible in real life and real, accurate dimensions can be extracted from it by importing it into FreeCAD. All this is only 850 lines of code !!!!!!!
The video below was produced from the macro by the LLM using Blender, which it installed on my PC in a Python environment. Took 12 hours to render 300 frames.
# Standalone comparison: complete closed and 20-degree mechanism in side view.
# Both poses share identical fixed pivot locations. All dimensions in mm.
import FreeCAD as App
import os
# Root profile trimmed for at least 1 mm band clearance through 25 degrees.
# 0 = outer hole; 1, 2, 3 move inward in 15 mm steps.
# Selecting another hole also relocates the rear mount to retain a vertical start.
CLEVIS_HOLE_INDEX = 0
def build_pose(requested_angle):
# FreeCAD macro: four-panel demi-cone; dimensions in mm.
# Standalone: uses only built-in FreeCAD/Part and Python modules.
# Dimensions refer to the theoretical sheet mid-surface.
import math
import os
import FreeCAD as App
import Part
SLANT_LENGTH = 180.0
RIM_CHORD = 50.0 # straight chord, NOT circular arc length
THICKNESS = 2.0
SLOT_WIDTH = 0.3 # nominal material removed before bending
TIP_KEEP = 30.0
RIM_KEEP = 20.0 # measured along each bend from its rim corner
SLOTS_PER_BEND = 3
PANELS = 4
FLANGE_WIDTH = 25.0 # horizontal distance normal to each straight rim edge
FLANGE_THICKNESS = 2.0
BRACKET_WIDTH = 20.0
BRACKET_LENGTH = 60.0 # extended 10 mm below the lowest hole
BRACKET_THICKNESS = 3.0
BRACKET_TOP_RADIUS = 5.0
BRACKET_HOLE_DIAMETER = 6.0
BRACKET_HOLE_FROM_TOP = 10.0
BRACKET_HOLE_SPACING = 15.0 # centre-to-centre downwards
BRACKET_HOLE_COUNT = 3
BRACKET_BELOW_FLANGE = 3.0
MOUNT_SLOT_LENGTH = 20.2 # along Y, perpendicular to open-back line
MOUNT_SLOT_WIDTH = 3.2 # along X
MOUNT_SLOT_X = 63.5 # mirrored centres at +/- X
MOUNT_SLOT_Y = 45.0 # from open-back line Y=0; estimated from screenshot
# Blank means save beside this macro (fallback: FreeCAD macro directory).
OUTPUT_DIR = ""
# Illustrative female rod ends, not a manufacturer's dimensioned part.
ROD_END_EYE_OD = 16.0
ROD_END_HOUSING_WIDTH = 6.0
ROD_END_BALL_WIDTH = 8.0
ROD_END_BALL_RADIUS = 5.0
ROD_END_EYE_TO_MOUTH = 25.0
ROD_END_SHANK_OD = 9.0
ROD_THREAD_DIAMETER = 6.0
ROD_THREAD_ENGAGEMENT = 10.0
ROD_END_SPACER = 1.0
PIVOT_OFFSET = 20.0 # each half outward: 40 mm additional separation
PIVOT_BOSS_RADIUS = 12.0 # 9 mm material beyond the 6 mm hole
PLAIN_BAR_LENGTH = 150.0
PLAIN_BAR_DIAMETER = 6.0
CYL_BASE_Y=-201.0 # corrected cylinder: retain 7 mm initial extension
CYL_BASE_X=63.5
CYL_BASE_HEIGHT_OFFSET=0.0 # rear pin level with clevis pin at zero degrees
CYL_STROKE=25.0 # retained operating envelope from previous model
CYL_REAR_PIN_DIAMETER=10.0 # catalogue rear bore is 10; clevis pin stays 6
RIGHT_TIE_SPACER=1.0 # standard tie-rod mounting restored; clevis uses other top hole
OPENING_ANGLE_DEG = requested_angle # original (+Y) half; driven half solved from fixed links
L = SLANT_LENGTH
C = RIM_CHORD
if PANELS != 4 or SLOTS_PER_BEND < 1:
raise ValueError("This macro requires four panels and at least one slot.")
if not (0 < C < 2*L and 0 < TIP_KEEP < L and 0 < RIM_KEEP < L-TIP_KEEP and THICKNESS > 0 and SLOT_WIDTH > 0):
raise ValueError("Invalid dimensions.")
alpha = 2 * math.asin(C / (2*L))
radius = C / (2*math.sin(math.pi/(2*PANELS)))
if radius >= L:
raise ValueError("Rim too large for the sloping length.")
height = math.sqrt(L*L - radius*radius)
# Equal slot lengths and equal internal bridges; both end margins stay intact.
pitch_piece = (L - TIP_KEEP - RIM_KEEP) / (2*SLOTS_PER_BEND - 1)
intervals = [(TIP_KEEP + 2*i*pitch_piece,
TIP_KEEP + (2*i+1)*pitch_piece) for i in range(SLOTS_PER_BEND)]
angles = [math.pi/2 + (i-PANELS/2)*alpha for i in range(PANELS+1)]
V = App.Vector
origin = V(0,0,0)
rim = [V(L*math.cos(a),...
Read more »

This device is called a Duck's Bill and moves down to the ground and releases the seedling enclosed within it at exactly the right moment. Some designs use a cable to do this and using a hydraulic actuator does seem like a bit of an over kill.
To see the mechanism, just copy the following code into a python file and upload into FreeCAD as a macro. If you want to remove the second, 20 degrees overlay, upload the file to LLM and ask it: "Remove the 20 degrees overlay".
# Standalone comparison: complete closed and 20-degree mechanism in side view.
# Both poses share identical fixed pivot locations. All dimensions in mm.
import FreeCAD as App
import os
# 0 = outer hole; 1, 2, 3 move inward in 15 mm steps.
# Selecting another hole also relocates the rear mount to retain a vertical start.
CLEVIS_HOLE_INDEX = 0
def build_pose(requested_angle):
# FreeCAD macro: four-panel demi-cone; dimensions in mm.
# Standalone: uses only built-in FreeCAD/Part and Python modules.
# Dimensions refer to the theoretical sheet mid-surface.
import math
import os
import FreeCAD as App
import Part
SLANT_LENGTH = 180.0
RIM_CHORD = 50.0 # straight chord, NOT circular arc length
THICKNESS = 2.0
SLOT_WIDTH = 0.3 # nominal material removed before bending
TIP_KEEP = 30.0
RIM_KEEP = 20.0 # measured along each bend from its rim corner
SLOTS_PER_BEND = 3
PANELS = 4
FLANGE_WIDTH = 25.0 # horizontal distance normal to each straight rim edge
FLANGE_THICKNESS = 2.0
BRACKET_WIDTH = 20.0
BRACKET_LENGTH = 60.0 # extended 10 mm below the lowest hole
BRACKET_THICKNESS = 3.0
BRACKET_TOP_RADIUS = 5.0
BRACKET_HOLE_DIAMETER = 6.0
BRACKET_HOLE_FROM_TOP = 10.0
BRACKET_HOLE_SPACING = 15.0 # centre-to-centre downwards
BRACKET_HOLE_COUNT = 3
BRACKET_BELOW_FLANGE = 3.0
MOUNT_SLOT_LENGTH = 20.2 # along Y, perpendicular to open-back line
MOUNT_SLOT_WIDTH = 3.2 # along X
MOUNT_SLOT_X = 63.5 # mirrored centres at +/- X
MOUNT_SLOT_Y = 45.0 # from open-back line Y=0; estimated from screenshot
# Blank means save beside this macro (fallback: FreeCAD macro directory).
OUTPUT_DIR = ""
# Illustrative female rod ends, not a manufacturer's dimensioned part.
ROD_END_EYE_OD = 16.0
ROD_END_HOUSING_WIDTH = 6.0
ROD_END_BALL_WIDTH = 8.0
ROD_END_BALL_RADIUS = 5.0
ROD_END_EYE_TO_MOUTH = 25.0
ROD_END_SHANK_OD = 9.0
ROD_THREAD_DIAMETER = 6.0
ROD_THREAD_ENGAGEMENT = 10.0
ROD_END_SPACER = 1.0
PIVOT_OFFSET = 20.0 # each half outward: 40 mm additional separation
PIVOT_BOSS_RADIUS = 12.0 # 9 mm material beyond the 6 mm hole
PLAIN_BAR_LENGTH = 150.0
PLAIN_BAR_DIAMETER = 6.0
CYL_BASE_Y=-201.0 # corrected cylinder: retain 7 mm initial extension
CYL_BASE_X=63.5
CYL_BASE_HEIGHT_OFFSET=0.0 # rear pin level with clevis pin at zero degrees
CYL_STROKE=25.0 # retained operating envelope from previous model
CYL_REAR_PIN_DIAMETER=10.0 # catalogue rear bore is 10; clevis pin stays 6
RIGHT_TIE_SPACER=1.0 # standard tie-rod mounting restored; clevis uses other top hole
OPENING_ANGLE_DEG = requested_angle # original (+Y) half; driven half solved from fixed links
L = SLANT_LENGTH
C = RIM_CHORD
if PANELS != 4 or SLOTS_PER_BEND < 1:
raise ValueError("This macro requires four panels and at least one slot.")
if not (0 < C < 2*L and 0 < TIP_KEEP < L and 0 < RIM_KEEP < L-TIP_KEEP and THICKNESS > 0 and SLOT_WIDTH > 0):
raise ValueError("Invalid dimensions.")
alpha = 2 * math.asin(C / (2*L))
radius = C / (2*math.sin(math.pi/(2*PANELS)))
if radius >= L:
raise ValueError("Rim too large for the sloping length.")
height = math.sqrt(L*L - radius*radius)
# Equal slot lengths and equal internal bridges; both end margins stay intact.
pitch_piece = (L - TIP_KEEP - RIM_KEEP) / (2*SLOTS_PER_BEND - 1)
intervals = [(TIP_KEEP + 2*i*pitch_piece,
TIP_KEEP + (2*i+1)*pitch_piece) for i in range(SLOTS_PER_BEND)]
angles = [math.pi/2 + (i-PANELS/2)*alpha for i in range(PANELS+1)]
V = App.Vector
origin = V(0,0,0)
rim = [V(L*math.cos(a), L*math.sin(a), 0) for a in angles]
def face...
Read more »
The first PLANTINATOR log was a simple test: could a Python macro generate sprockets and a moving chain in FreeCAD without having to draw and position everything manually? That established a useful starting point. The model has now grown into a more detailed wheel and chain assembly, with a dedicated Qt5 application for inspecting the geometry and movement.
The eventual objective is a transplanting machine. This simplified duckbill assembly illustrates the direction of the project:
The immediate work is on the supporting drive mechanism and the tools for developing it. The current CAD code includes two lugged wheels on a shared shaft, three sprockets, and individual chain components. The wheels include stepped discs, rims, bosses, lug holes, chamfers, and simplified locking fasteners. The current sprocket combination is 28, 15, and 21 teeth, with a 58-pitch chain at 12.7 mm pitch—an extension of the earlier two-sprocket experiment.
Much of the interest here is in how the machine is being designed. With an LLM such as GPT Astra helping to write and revise the Python, a mechanical change can start as an ordinary description: increase the wheel spacing, alter a lug dimension, or keep the wheel locked to the rotation of its driving sprocket. The resulting code expresses those requirements through parameters, geometry functions, and placement calculations.
That is particularly useful when one change affects several parts. Moving the wheels further apart also affects the shared shaft. Changing a repeated lug feature should update every lug consistently. Encoding those relationships makes revisions repeatable and avoids having to select and reposition each affected object by hand. FreeCAD has its own parametric tools, of course; Python provides another practical way to manage the repetition and mathematical relationships in this assembly.
The LLM still needs clear instructions and its output needs checking. Specifying which dimensions can change, which relationships must remain fixed, and which direction an offset refers to is far more useful than asking it to “improve the mechanism.” Small changes followed by visual inspection make errors easier to spot.
The dedicated Qt5 viewer is intended to make that inspection loop quicker. Repeatedly opening FreeCAD, running the macro, and arranging the view adds friction when the immediate question is simply whether a change looks right through a complete rotation. The viewer puts playback and camera controls alongside the mechanism, so it is easier to concentrate on the moving geometry.
It uses FreeCAD’s Part geometry kernel to build the solids and Pivy/Coin with OpenGL to display their tessellated surfaces. Holes and chamfers are therefore part of the geometry being shown. Once the scene is built, playback updates component positions and rotations rather than constructing every solid again for each frame.
The controls include pause, reverse motion through signed speed, stepping by one chain pitch, and a position slider. There are standard camera views, orbit and zoom controls, plus close-ups for the lugs and chain. These should make it easier to examine awkward positions instead of judging the mechanism from one attractive angle.
There is also a route back into the full FreeCAD environment. Running the supplied `FreeCAD_Simulator.FCMacro` creates native document objects from the bundled model. The document can then be saved as an `.FCStd` file and developed further, adding the supporting frame, bearing mounts, brackets, and other static parts around the mechanism.
One detail to keep straight: the standalone viewer’s dimension loader reads numeric parameters from a file; it does not execute arbitrary geometry code. Accepted parameter changes need to be carried into the bundled source before generating the matching FreeCAD document.
The next stage is to continue refining the geometry and checking its movement before committing to fabrication. The animation...
Read more »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,...
Read more »
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates
Jesal Mehta
JP Gleyzes
Brandon Piner