How to use python to convert text into a mesh and then separate each letter?

I’d like to write a python script to do an effect on text that is selected.

First I’d like to convert the text into a mesh and then separate the letters into their own standalone mesh that can be animated independently. When I do this manually, I use the convert to mesh menu option ( bpy.ops.objects.convert() ). Then I go into edit mode, select the left most letter using a box, then hit P ( bpy.ops.mesh.separate() ). I do that last part for each letter until they are all separated.

The problem is that I don’t know of a python API way to select an individual letter. I have to do that part by eye now. Is there an API way to discover separate “sub-meshes” within a mesh? Or do I have to do some sort of algorithm where I figure it by iterating through all the faces and put them into sets or something?

(BTW… Heads up… I’m new to blender, but know python and programming pretty well)

you can use Mesh > Vertices > Separate > By loose parts
bpy.ops.mesh.separate(type=‘LOOSE’)

but it really depends on the font. If a letter consists of multiple, separated parts, it will get separated of course. You could rejoin the parts, but not determine what belongs together with python.

Another way would be to get the text of the font object with python and create a font object for every letter, then turn it into mesh. The problem here would be to determine where to place the single letters…

The effect I’m trying to produce is similar to what is done in this video. Notice how the letters in word “RUGBY” appear by falling into place from left to right. It’s as if each one is attached with a hinge at the top edge of the letter.

That effect could be accomplished using RE:Lay and RE:Phrase together. RE:Phrase for the text layout and specifying the words as well as adjusting the rotational axis point.

Then use RE:Lay for transferring a delayed version of the same animation throughout each RE:Phase character.

What is “RE:Lay” and “RE:Phrase” are those scrips somebody wrote that I can download? Or is it part of blender already?

Thanks… I’ll check that out.

Meanwhile, I was working on trying to do this manually (this is an exercise for to learn blender too) and I think I almost got it done. However, I get the following error:

“Runime error: bpy.ops.marker.add.poll() expected an timeline/animation area to be active”

The line within my script that it is trying to run when this happens is bpy.ops.marker.add(). I’m not sure what “poll()” is. What I was trying to do what follow the steps when I manually animate these objects. I move them, add a frame, move them again, add another frame, etc. Any ideas?

You know… I should post the entire script as I have it now. That way you guys get context:
import bpy
import math

selected=bpy.context.scene.objects.active

location=selected.location
dimensions=selected.dimensions

bpy.context.scene.cursor_location=(location.x,location.y+dimensions.y,location.z)
bpy.ops.object.origin_set(type=‘ORIGIN_CURSOR’)

camera=bpy.context.scene.camera.location
initialAngle=math.atan( (camera.z-location.z)/(camera.y-location.y) )
bpy.ops.transform.rotate(value = initialAngle, axis=(1,0,0))

bpy.ops.object.convert()
bpy.ops.mesh.separate(type=‘LOOSE’)

selected=bpy.context.selected_objects.copy();
length=len(selected)
last=selected.pop()
selected.insert(0,last)

bpy.ops.object.select_all(action=‘DESELECT’)
currentFrame=bpy.context.scene.frame_current

bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=True
frameOffset=0
for x in selected:
bpy.ops.marker.add()
bpy.context.scene.frame_set(frameOffset)
x.select=True
bpy.context.scene.frame_set(frameOffset+5)
bpy.ops.transform.rotate(value = -initialAngle, axis=(1,0,0))
bpy.ops.marker.add()
bpy.context.scene.frame_set(frameOffset+3)
bpy.ops.transform.rotate(value = 5math.pi/180, axis=(1,0,0))
bpy.ops.marker.add()
bpy.context.scene.frame_set(frameOffset+3)
bpy.ops.transform.rotate(value = -8
math.pi/180, axis=(1,0,0))
bpy.ops.marker.add()
bpy.context.scene.frame_set(frameOffset+3)
bpy.ops.transform.rotate(value = -3*matn.pi/180, axis=(1,0,0))
bpy.ops.marker.add()
x.select=False
frameOffset=frameOffset+10
bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=False

So it crashes in the first line within the loop. So the loop logic may be wrong or animate goofy since it hasn’t been tested yet. I post the whole thing for any additional comments or maybe you guys can see things that I’m doing stupidly.

using operators is easy, but i totally discourage it! They are slow and limited. Whenever available, I prefer low-level function API.

e.g. timeline markers:

markers = bpy.context.scene.timeline_markers
m = markers.new("A new marker")
m.frame = frameOffset # any int value

Here is an example BLEND file that demonstrates RE:Lay transferring an animation from itself to a list of managed targets. The transfer of the animation can precede or follow the original source object. The f-curve data sampled from the source object can be time stretched as well. These time settings can be adjusted while the animation is running to tune it in. In this example BLEND file I have set the last letter, “E” to slow time down so it takes a little longer to fall into place.

The RED arrows show which entry is linked to what object in the scene. The time controls can be adjusted for each entry and are highlighted in yellow.

Attachments


266_relay_moves_letters.blend (153 KB)

CodemanX,

I appreciate the response. I guess I would prefer to whatever API is the most stable. If the operators change from version to version, then I agree I shouldn’t use them. If the low level API is less stable then operators then I’d think I’d be better off using them. I’m too new to blender to know the history on that. I’m not particular concerned with the speed since this isn’t a runtime/real-time sorta thing but something that is run once at user command to set up the animations. I guess if I were to run this script on an entire paragraph then I would be more concerned. But your point is well taken.

I tried your suggestion, and I was finally able to get it to work. Here is my script in case anybody wants to try it out:

import bpy
import math

def createFrameAt(frame):
markers = bpy.context.scene.timeline_markers
m = markers.new("")
m.frame = frame
bpy.context.scene.frame_current=m.frame

selected=bpy.context.scene.objects.active

location=selected.location
dimensions=selected.dimensions

bpy.context.scene.cursor_location=(location.x,location.y+dimensions.y,location.z)
bpy.ops.object.origin_set(type=‘ORIGIN_CURSOR’)

camera=bpy.context.scene.camera.location
bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=True
initialFrame=bpy.context.scene.frame_current
frameOffset=initialFrame

bpy.ops.object.convert()
bpy.ops.mesh.separate(type=‘LOOSE’)

createFrameAt(frameOffset)
initialAngle=math.atan( (camera.z-location.z)/(camera.y-location.y) )
bpy.ops.transform.rotate(value = initialAngle, axis=(1,0,0))

selected=bpy.context.selected_objects.copy();
#length=len(selected)
last=selected.pop()
selected.insert(0,last)

bpy.ops.object.select_all(action=‘DESELECT’)

#selected[0].select=True

for x in selected:
x.select=True
createFrameAt(frameOffset+2)
bpy.ops.transform.rotate(value = -initialAngle, axis=(1,0,0))

createFrameAt(frameOffset+4)
bpy.ops.transform.rotate(value = 10*math.pi/180, axis=(1,0,0))   


createFrameAt(frameOffset+8)
bpy.ops.transform.rotate(value = -15*math.pi/180, axis=(1,0,0))


createFrameAt(frameOffset+10)
bpy.ops.transform.rotate(value = 5*math.pi/180, axis=(1,0,0))


x.select=False
frameOffset=frameOffset+2

bpy.context.scene.frame_current=initialFrame
bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=False

To use it, add some text (all caps looks best, I think), select it, and then run this script. I also think it’s looks best if you make the camera straight above the grid, pointing right down at it. (so at like (0,0,10) rotated (0,0,0))

CodemanX,

I appreciate the response. I guess I would prefer to whatever API is the most stable. If the operators change from version to version, then I agree I shouldn’t use them. If the low level API is less stable then operators then I’d think I’d be better off using them. I’m too new to blender to know the history on that. I’m not particular concerned with the speed since this isn’t a runtime/real-time sorta thing but something that is run once at user command to set up the animations. I guess if I were to run this script on an entire paragraph then I would be more concerned. But your point is well taken.

I tried your suggestion, and I was finally able to get it to work. Here is my script in case anybody wants to try it out:

import bpy
import math

def createFrameAt(frame):

[INDENT=2]markers = bpy.context.scene.timeline_markers
m = markers.new("")
m.frame = frame
bpy.context.scene.frame_current=m.frame [/INDENT]

selected=bpy.context.scene.objects.active

location=selected.location
dimensions=selected.dimensions

bpy.context.scene.cursor_location=(location.x,location.y+dimensions.y,location.z)
bpy.ops.object.origin_set(type=‘ORIGIN_CURSOR’)

camera=bpy.context.scene.camera.location
bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=True
initialFrame=bpy.context.scene.frame_current
frameOffset=initialFrame

bpy.ops.object.convert()
bpy.ops.mesh.separate(type=‘LOOSE’)

createFrameAt(frameOffset)
initialAngle=math.atan( (camera.z-location.z)/(camera.y-location.y) )
bpy.ops.transform.rotate(value = initialAngle, axis=(1,0,0))

selected=bpy.context.selected_objects.copy();
last=selected.pop()
selected.insert(0,last)

bpy.ops.object.select_all(action=‘DESELECT’)

for x in selected:

[INDENT=2]x.select=True
createFrameAt(frameOffset+2)
bpy.ops.transform.rotate(value = -initialAngle, axis=(1,0,0))

createFrameAt(frameOffset+4)
bpy.ops.transform.rotate(value = 10*math.pi/180, axis=(1,0,0))   

createFrameAt(frameOffset+8)
bpy.ops.transform.rotate(value = -15*math.pi/180, axis=(1,0,0))

createFrameAt(frameOffset+10)
bpy.ops.transform.rotate(value = 5*math.pi/180, axis=(1,0,0))

x.select=False
frameOffset=frameOffset+2[/INDENT]

bpy.context.scene.frame_current=initialFrame
bpy.data.scenes[“Scene”].tool_settings.use_keyframe_insert_auto=False

To use it, add some text (all caps looks best, I think), select it, and then run this script. I also think it’s looks best if you make the camera straight above the grid, pointing right down at it. (so at like (0,0,10) rotated (0,0,0))

Atom,

I downloaded your blender file, and looked at it, but I’m afraid, your post is a bit over my head at the moment. I’m not sure what f-curves are yet. I suspect that the animation of the square can be applied to the text on the right? And there is a wall of script on the left, was that something you had written for this? Or is that the code for RE:Lay? So that you pick the “parameters on the right” and it applies whatever animation you have created for the square on each letter?

I would prefer to whatever API is the most stable

Let me clarify this a bit: entire API is marked unstable, changes may occur anywhere, but it will most likely stay like it is, at least for the stuff you do - no matter if operators or low-level functions.

The difference is, that operators represent user action and therefore rely on a certain context (e.g. has to run from 3d view and needs an active mesh object). Low-level functions are rather real API, you can pass arguments and you get something back, e.g. a reference to a newly created object. This isn’t the case with operators, they returns whether they finished or aborted, but no objects or the like you can later work with. If you create a new object using an operator, you need to know that it will make that new object the active/context object (bpy.context.object) and use that, otherwise you wouldn’t know what the new object was called etc.

Low-level functions are mostly context-independent, but may require more lines of code compared to operators. However, it’s worth to use the low-level stuff, as it is more flexible, more like programmers expect an API to behave and faster than using OPs (the latter force scene updates, which can make things really slow if you actually needed a single scene update after many operations and not for every single one of them).

Here’s an example of OP vs. low-level:
http://www.blender.org/documentation/blender_python_api_2_66_release/info_quickstart.html#animation