# Using python to create a curve and attach its endpoints with hooks to two spheres

I'm trying to create a curve with python to depict a bond between two atoms (spheres) like so:

bpy.ops.curve.primitive_bezier_curve_add()
obj = bpy.context.object
obj.data.dimensions = '3D'
obj.data.fill_mode = 'FULL'
obj.data.bevel_depth = self.bevel_depth
obj.data.bevel_resolution = self.bevel_resolution
# set first point to centre of atom_1
obj.data.splines[0].bezier_points[0].co = self.atom_1.locations[0]
obj.data.splines[0].bezier_points[0].handle_left_type = 'VECTOR'
# set second point to centre of atom_2
obj.data.splines[0].bezier_points[1].co = self.atom_2.locations[0]
obj.data.splines[0].bezier_points[1].handle_left_type = 'VECTOR'


The curve endpoints seem to get set to local coordinates and not to the centers of the spheres.

How do I add the hooks to attach the endpoints to the two spheres so that the curve ends stay attached to the spheres when animating the movement of the spheres?

Based on the answer linked by Seppo Silaste I have come up with the following example code which you can modify to fit your application:

import bpy

def hookCurve(o1, o2, scn):
spline = curve.splines.new('BEZIER')

p0 = spline.bezier_points[0]
p1 = spline.bezier_points[1]
p0.co = o1.location
p0.handle_right_type = 'VECTOR'
p1.co = o2.location
p1.handle_left_type = 'VECTOR'

m0 = obj.modifiers.new("alpha", 'HOOK')
m0.object = o1
m1 = obj.modifiers.new("beta", 'HOOK')
m1.object = o2

scn.objects.active = obj

# using anything in bpy.ops is a giant pain in the butt
bpy.ops.object.mode_set(mode='EDIT')

# the mode_set() invalidated the pointers, so get fresh ones
p0 = curve.splines[0].bezier_points[0]
p1 = curve.splines[0].bezier_points[1]

p0.select_control_point=True
bpy.ops.object.hook_assign(modifier="alpha")

p0.select_control_point = False
p1.select_control_point = True
bpy.ops.object.hook_assign(modifier="beta")

return obj

o1 = bpy.data.objects['atom 1']
o2 = bpy.data.objects['atom 2']

obj = hookCurve(o1,o2, bpy.context.scene)

• This works! I'd up vote you but I don't have reputation ;o) I'd like to understand: Why does p0 and p1 have to be assigned again? Why create a curve and also an object with the curve? Thank you!! – Melodius Jun 26 '14 at 8:49
• I moved them above the mode_set() call and the hook_assign() did not accomplish its mission. I'm assuming that the mode_set() changes the curve object in a way that invalidates the pointers inside the python objects p0 and p1. This is a common danger with blender's python API (and any other system that uses pointers, which is a lot of them). – Mutant Bob Jun 26 '14 at 14:10
• Oh, and don't worry about upvoting. If this answer is actually correct and solves your problem, click the "accept answer" icon. If it does not, let me know how it fails so I can fix it. – Mutant Bob Jun 26 '14 at 14:33

This is not a direct answer to the question, but rather to the underlying application of the OP.

I occasionally want to display proteins, where the hook method would be terribly slow (calling bpy.ops methods for >10000 bonds...). My current working solution for this specific problem of displaying a bond between two atoms is using constraints, which are much faster to handle with a script.

Here is a (rather lengthy) working example. As a bonus it reuses the data (mesh or beveled curve) for all bonds automatically. So the slow bpy.ops calls only happen once. They could be replaced entirely by creating the base mesh or curve from scratch but are more convenient in this case.

import bpy
import bmesh

def get_bond_data(type='MESH'):
new_bond = None
if type == 'MESH':
data_name = "bond_mesh"
elif type == 'CURVE':
data_name = "bond_curve"

data = bpy.context.blend_data.meshes.get(data_name)
if not data:
# save last selection to restore later
selected = bpy.context.selected_objects
last_active = bpy.context.object
# bond should start at origin and have length 1 in y-direction
if type == 'MESH':
end_fill_type="NOTHING")
new_bond = bpy.context.object
data = new_bond.data
data.name = data_name
# rotate vertices 90 degrees around x, and shift along y axis
bm = bmesh.new()
bm.from_mesh(data)
for vert in bm.verts:
tmp_co = vert.co.copy()
vert.co.y = -tmp_co.z + .5
vert.co.z = tmp_co.y
bm.to_mesh(data)
bm.free()
data.update()

elif type == 'CURVE':
new_bond = bpy.context.object
data = new_bond.data
data.name = data_name

bp = data.splines[0].bezier_points
for i in range(2):
bp[i].co = (0,i,0)
bp[i].handle_left_type = 'VECTOR'
bp[i].handle_right_type = 'VECTOR'

# get bevel object
bevel_name = 'bond_bevel'
bond_bevel = bpy.context.blend_data.objects.get(bevel_name)
if not bond_bevel:
bond_bevel = bpy.context.object
bond_bevel.name = bevel_name
data.bevel_object = bond_bevel

if new_bond:
# finally delete object and reselect old selection
for o in selected:
o.select = True
bpy.context.scene.objects.active = last_active

return data

bond_name = "bond_{}-{}".format(first_atom.name, second_atom.name)
bond_data = get_bond_data(type=type)
new_bond = bpy.data.objects.new(bond_name, bond_data)

c = new_bond.constraints.new('COPY_LOCATION')
c.name = "from"
c.target = first_atom

c = new_bond.constraints.new('STRETCH_TO')
c.name = "to"
c.rest_length = 1.0
c.volume = 'NO_VOLUME'
c.target = second_atom

# scale bond so it looks better
scale = 0.15
new_bond.scale = (scale, 1.0, scale)

a1 = bpy.context.blend_data.objects.get("atom 1")
a2 = bpy.context.blend_data.objects.get("atom 2")

• Hello what version of Blender is this written in? I'm getting an add_bond error: bond_name = "bond_{}-{}".format(first_atom.name, second_atom.name) AttributeError: 'NoneType' object has no attribute 'name' – volvox Dec 10 '15 at 8:50
• @volvox I think it was 2.71 at the time, but it should work in other versions as well. You need to make sure that you have at least to objects in your scene called "atom 1" and "atom 2". – FBA Dec 11 '15 at 9:57

As best I can tell blender does not have what you need for Bezier curves.

It does have something for meshes. There is a hook modifier you can apply to a mesh. You then specify a foreign object and a Vertex Group that will be hooked to that object.

So replace your Bezier curve with a simple mesh rectangle (or tube or whatever) and add the hook modifiers.

If you need python code to build the mesh, vertex groups, and modifiers reply in the comments and I'll cobble together something concrete.

• Someone seems to think it is possible:blender.stackexchange.com/questions/13452/… – Melodius Jun 25 '14 at 16:53
• I guess a cylinder could also work, but it doesn't have end points that I could set to the centers of the spheres. That's why I'm thinking about a curve. – Melodius Jun 25 '14 at 16:55
• Put the ends of the cylinder such that the center of each cap is in the middle of a sphere. Make each cap its own vertex group for the Hook modifier. – Mutant Bob Jun 25 '14 at 17:27
• Interesting, Seppo. I was discouraged because I added the modifier through the menu, not through Ctrl-H. There is no obvious readout in the modifier pane that shows you what curve control points are members of the hook, but it works. – Mutant Bob Jun 25 '14 at 19:12
• The python API for the hook modifier object doesn't have any obvious ways to set which points on the curve are hooked. It looks like the only way to accomplish this is bpy.ops.object.hook_assign() which has all the drawbacks of everything in bpy.ops. But it is probably possible. – Mutant Bob Jun 25 '14 at 19:31

If you want a cylinder to connect two objects a hook modifier probably isn't enough. As the atoms move relative to each other, the caps of the cylinder won't know to rotate to face the other object.

In order to address this shortcoming you can create a cylinder with a 2-bone armature. The cylinder has two vertex groups (one for each cap) to map them to each bone.

The bones then have bone constraints. The first bone constraint is a Copy Location to place the bone in the center of the atom. The second bone constraint is a Track To to point the bone at the other atom.

In order for this to achieve the desired effect the bones must be oriented correctly in edit mode. Notice how the top bone points down and has a roll of 180.

If you need help figuring out the python to construct this sort of arrangement automatically, post a comment and I'll help figure it out.