A parametric mesh object in 2.5. Working example with some critical remarks

This is a long post with a working and commented code example to add a parametric object to a scene. Code at the end, full code as a single file at my site http://www.swineworld.org/blender/blender25/25example.py (because this code is so long it doesn’t show up in the post if i add it as a whole :().

This is based on the discussion in http://blenderartists.org/forum/showthread.php?p=1454863 but I added the following:

  • a class to add an extra menu item in the menu at the top of the 3d windows (actually that is at the bottom of the info or user preferences window), this is horrendously ugly but needed as long as we don’t have a script registration system. see comments below.

  • a way to use properties (things you define for all objects but that may have different values for individual object).

  • an operator that changes mesh geometry by manipulating vertices directly (i.e without using operators)

Once this script is run you will have an extra menu item at the top of your screen that has a single entry called ‘Gear’. If you click that an empty mesh object will be created. Now because we have defined a BoolProperty geartype every object will have that property but its default is False. Only if we define an empty mesh via our menu, we set this property to True.

Now in the the properties window there will be a panel called ‘Gears’ if and only if the property geartype is True for the active object. We check that with the poll() method.
The panel consists of little more than a button to select the number of teeth (another property we defined) and a pushbutton that replaces the geometry of the mesh.

This all might seem nice, but it is wrong on many levels:

  • adding a menu entry that way is not only ugly, it prevents adding more menu items because it replaces a menu instead of adding an entry. This a minor concern as no doubt some script registration mechanism will appear in the near future,

  • what I really wanted to have is a way to add popup menus where I can ask the user for parameters and generate the mesh accordingly. But popups are not there yet (note that all existing parametric meshes like cylinders and such just drop in a default one). What is now implemented with properties is a kludge: It works but properties are supposed to be animatable whereas parameters for meshes shouldn’t (well maybe, but we’d have to hook up to a frame change event and I have no idea how to do that yet)

  • properties are also a poor surrogate for subclasses: what I really want is a subclass of Mesh, with its own instance variables. It is possible to subclass Mesh but there is now way to register is as an Blender Object type (registering gives an error).

  • add_geometry() is (for me) a strange way to manipulate the vertices. I really would like Mesh.verts (and .faces and .edges) to have a richer interface with extend() and append() methods. Besides add_geometry() seems to fail silently if not in Object mode (I’ll check if can make that one reproducible and submit it as a bug)

Nevertheless, already a lot can be accomplished using the current implementation of the API and moreover it seems reasonably fast and quite stable.


# this part defines  a properties panel in the object context
# the properties it uses are defined at the end of this file for every
# object.
class ObjectButtonsPanel(bpy.types.Panel):
 __space_type__ = "PROPERTIES"
 __region_type__ = "WINDOW"
 __context__ = "object"
class OBJECT_PT_gears(ObjectButtonsPanel):
 __label__ = "Gears"
 
 def draw_header(self, context):
  layout = self.layout
  layout.itemL(text="", icon='ICON_PHYSICS')
 
 def draw(self, context):
  """
  Draw an integer selector and a push button
  """
  layout = self.layout
  layout.itemO("gears", text="Replace Mesh")
  layout.itemR(context.active_object,"teeth")
 
 def poll(self,context):
  # we use the geartype property to see if we should display anything at all
  return context.active_object.geartype

This is the actual operator that modifes the mesh.


from math import cos,sin,pi
 
class OBJECT_OT_gears(bpy.types.Operator):
 """
 Replace mesh data with a Gear of the specifed number of teeth.
 
 I know this is nowhere near a gear but the object of this exercise is to add geometry to
 a mesh object, not to make realistic gears.
 """
 
 __idname__ = "gears"
 
 def invoke(self, context, event):
  # actually, we should check a lot of context here, for now we just catch exceptions
  try:
   # we are going to mangle the mesh data instead of replacing it wholesale
   me=context.active_object.data
   # we used the teeth property here we defined on every object
   n=context.active_object.teeth
 
   # we wont be calculating any edges: we will use mesh.update()
   nverts= n*4
   nfaces= n*2
 
   # we want to restore the mode later. We will have to use operators
   # because context.mode is read only. And note: what is called EDIT_MESH
   # in context,mode is called EDIT in mode_set()
   savemode=context.mode
 
   # modifying geometry (like deleting everything) is an editmode op
   bpy.ops.object.mode_set(mode='EDIT')
   bpy.ops.mesh.delete(type='ALL')
 
   # but adding (blank) geometry is oddly enough a objectmode op
   # note that addgeometry adds new vertices all with coords (0,0,0)
   bpy.ops.object.mode_set(mode='OBJECT')
   me.add_geometry(nverts,0,nfaces)
 
   for i in range(n): # calculate vertices
    x1,y1 = cos(i*2*pi/n),sin(i*2*pi/n)
    x2,y2 = 0.5*cos((i+0.5)*2*pi/n),0.5*sin((i+0.5)*2*pi/n)
    me.verts[i*4  ].co=(x1,y1,0)
    me.verts[i*4+1].co=(x2,y2,0)
    me.verts[i*4+2].co=(x2,y2,1)
    me.verts[i*4+3].co=(x1,y1,1)
   for i in range(n-1): # calculate faces
    me.faces[i*2  ].verts=[ i*4+j for j in range(4)]
    me.faces[i*2+1].verts=[ i*4+j for j in (1,2,7,4)]
   # last face is an exception to get edge winding right
   i=n-1
   me.faces[i*2  ].verts=[ i*4+j for j in range(4)]
   me.faces[i*2+1].verts=[ i*4+2,i*4+1,0,3 ]
 
   # this should calculate the edges for us 
   me.update()
   print([ tuple(e.verts) for e in me.faces])
   # restore previous mode
   if savemode=='EDIT_MESH':
    bpy.ops.object.mode_set(mode='EDIT')
   elif savemode=='OBJECT':
    bpy.ops.object.mode_set(mode='OBJECT') 
  except Exception as e:
   print(e)
 
  return('FINISHED',)

This part is were we redfine the menu and define properties

 
# we redefine and (later) reregister the user preferences header just to add an extra menu
# entry. This a crude way but needed because there is yet no way to add a menu item dynmically
class INFO_HT_header(bpy.types.Header):
 __space_type__ = "INFO"
 def draw(self, context):
  layout = self.layout
 
  st = context.space_data
  scene = context.scene
  rd = scene.render_data
 
  row = layout.row(align=True)
  row.template_header()
  if context.area.show_menus:
   sub = row.row(align=True)
   sub.itemM("INFO_MT_file")
   sub.itemM("INFO_MT_add")
   sub.itemM("INFO_MT_addextra")
   if rd.use_game_engine:
    sub.itemM("INFO_MT_game")
   else:
    sub.itemM("INFO_MT_render")
   sub.itemM("INFO_MT_help")
  layout.template_ID(context.window, "screen", new="screen.new", unlink="screen.delete")
  layout.template_ID(context.screen, "scene", new="scene.new", unlink="scene.delete")
  if rd.multiple_engines:
   layout.itemR(rd, "engine", text="")
  layout.itemS()
  layout.template_operator_search()
  layout.template_running_jobs()
  layout.itemL(text=scene.statistics())
# our extra menu with just one entry: to add a Gear meshobject   
class INFO_MT_addextra(bpy.types.Menu):
 __space_type__ = "INFO"
 __label__ = "AddExtra"
 def draw(self, context):
  layout = self.layout
  layout.operator_context = "EXEC_SCREEN"
  layout.itemO("OBJECT_OT_addgear", text="Gear", icon='ICON_OUTLINER_OB_MESH')
 
class OBJECT_OT_addgear(bpy.types.Operator):
 __idname__ = "OBJECT_OT_addgear"
 def execute(self, context):
  #this adds an empty mesh
  bpy.ops.object.object_add(type = "MESH")
 
  ob = context.active_object
 
  # both Object and Mesh are called Gear. Blender takes care of uniqueness
  ob.name="Gear"
  ob.data.name="Gear"
 
  # geartype is a property we define later on. It is False by default so
  # only our Gear objects defined by the methos we are in here have it set to True
  ob.geartype=True
 
  return('FINISHED',)
# define new properties, all Objects have them (or the default value if they don't actually have them
# the geartype property is used in absence of proper subclassing
bpy.types.Object.BoolProperty( attr="geartype", name="Geartype", description="This is a Gear", default=False)
bpy.types.Object.IntProperty( attr="teeth",
      name="Number of teeth",
      description="Number of teeth on gear",
      min=4,max=30,default=12)
 
# finally register all types and operators    
bpy.ops.add(OBJECT_OT_addgear)
bpy.types.register(INFO_HT_header)
bpy.types.register(INFO_MT_addextra)
bpy.ops.add(OBJECT_OT_gears)
bpy.types.register(OBJECT_PT_gears) 

great job, works really quite well.
Certainly a very interesting & useful script.
thanks.

Remember that bmesh is coming soon, so py api will change to reflect that.

Very nice example script. Would you mind if I linked to it from this table of contents?

You say that you’d like to have pop-up messages, but I think it would be really nice if we could change the parameters after the gear is added. Like how the default blender objects (like uvsphere) will be implemented. Currently they can’t be changed yet, but for example the subdivide tool already works like this. When you run it, it automatically subdivides the mesh and then will show its parameters at the bottoms of the tools panel. So you can then change the number of cuts in realtime.
If you find out how that’s done, don’t hesitate to let me know ;).

I second your other remarks. Especially a richer interface for dealing with meshes would be welcome (similar interfaces for other types, like armatures, would be welcome as well). Though I guess the developers already know about this. They’re doing a lot of (great) work).

Of course not, go ahead.

You say that you’d like to have pop-up messages, but I think it would be really nice if we could change the parameters after the gear is added. Like how the default blender objects (like uvsphere) will be implemented. Currently they can’t be changed yet, but for example the subdivide tool already works like this. When you run it, it automatically subdivides the mesh and then will show its parameters at the bottoms of the tools panel. So you can then change the number of cuts in realtime.
If you find out how that’s done, don’t hesitate to let me know ;).

I like the idea of adding such a mesh tool to the view3d toolbox. Oddly enough I didn’t think of it until demohero asked the question about a toolbar. In principle the toolbox is just like any other window so it is possible to add panels to it. I’ll check it out.

I second your other remarks. Especially a richer interface for dealing with meshes would be welcome (similar interfaces for other types, like armatures, would be welcome as well). Though I guess the developers already know about this. They’re doing a lot of (great) work).

In last weekends developer notes Ton asserted an even greater need: good docs. So the developers certainly know the priorities :wink: But I am not complaining, this puzzlework is actually good fun.

Indeed, just replacing the first few lines by the following adds it to the toolbox:


class View3DPanel(bpy.types.Panel):
 __space_type__ = "VIEW_3D"
 __region_type__ = "TOOLS"
class OBJECT_PT_gears(View3DPanel):

However, there is no way (as far as I know), to add it to an existing panel within the toolbox. What I would like to find out how to react to the change of a property immediately like subdivide does and how to set sequence points (or whatever they are called) in the undo stack.

That’s indeed the way to add it to the tools panel (replacing the current content).
What I was actually talking about (and I think what you mean in the last part of your post), is the panel directly underneath the tools panel. It is still part of the sidebar that shows when you press the T-key, but it displays the last action you did and if possible the parameters associated with it (for example a vector when you move an object).

An item is added to the undo stack every time you enter or exit editmode, for example: bpy.ops.object.mode_set(mode=‘EDIT’)
This is pretty basic, but it is a start for now. For a quick test, run this armature script and afterwards press ctrl+z a few times. You’ll see that each time the mode is changed, there is a sequence point (or whatever it’s called). No idea how to prevent these points from being added though.

Thank you both very much. These scripts are very useful.

By the way, in ( .blender ) folder I deleted “ui” and “scripts” folder.

Here is the result:

http://img124.imageshack.us/img124/6439/im1.jpg

http://img124.imageshack.us/img124/6053/im2u.jpg

http://img361.imageshack.us/img361/523/im3.jpg

http://img354.imageshack.us/img354/5439/im4.jpg

As you can see in the pictures, nearly all the buttons and properties vanished, but some menus still there. Why?

Sorry for this newbie’s strange questions. :smiley:

Well, we’re all pretty new to this 2.5 stuff:D

Anyway, an interesting experiment to delete all those scripts. Why parts of the UI persist is (as far as I can tell) because because everthing the python scripts do, is to add panels, headers and menus, whereas the basic framework (i.e. the ‘spaces’ and ‘regions’) are provided by the Blender core. AFAIK it is not possible to define new spaces or regions using Python.

i ran this script but cannot see where the news things are added !

can you show a pic showing wehre the new things are added
it would be easier to understand and make it work

Thanks

hmm this fails for me on the
bpy.ops.object.object_add(type = “MESH”)

line,
obviously it cannot find the operator…

SystemError: bpy.ops.call: operator "OBJECT_OT_object_add"could not be found

Which revision/build are you using? It (still) works for me in revision 23050 ,

EDIT: and it bombs using r23161 :slight_smile:
to fix it: replace the object_add() with


bpy.ops.object.add(type='MESH')

Somehow the faces aren’t created anymore for me (revision 23406). I already changed the object_add() operator and I get the following report in the console:

StructRNA - Attribute (setattr): MeshFace.verts: array length cannot be changed to 4

Everything used to work fine for me in revision 22647.

the verts property of a face object is changed to a dynamic array. In this case one that can be of length 3 or 4. Unfortunately the code to support it is virtually non-existent (no append() function is implemented for example) and assigning a list of length 4 to a list of length 3 fails because there is a check for the length still in place …

This is end of story for me: till this either gets fixed or BMesh is implemented it is a waste of time to try and do dynamic changes to meshes in Python .

You’re probably right. I’m waiting with porting some of my own scripts until BMesh is implemented.
One complete rewrite a year is more than enough. No need to make life complicated by first rewriting for 2.5 and then rewriting again for BMesh.
There’s still more than enough to experiment with :slight_smile:

I got it to work again. The problem with changing the list of vertices in a face can be resolved by using the foreach_set() method of the faces attribute of a mesh. import_obj.py provided the crucial examples.

I updated the script, you find it here:
http://www.swineworld.org/blender/blender25/25example23662.py

the interface of foreach_set for raw vertex lists is bit odd:


me.faces.foreach_set("verts_raw",list_of_indices)

list_of_indices is a long list of indices, 4 for each face you added with add_geometry() so if you have a list of tuples instead you’ll to unpack them first. My script starts with suitable unpack functions copied from import_obj.py

Whether a face is a triangle or a quad is determined by each fourth index: if it is 0 then its a triangle. (this is bound to change with the BMesh implementation of course)
0 is a valid index in itself so if a triangle or a quad refers to this index it cannot be in the fourth position. The unpack function I copied takes care of that as well by reshuffling the indices if needed.

Another thing that changed recently is that the functionality was added to the menu system to dynamically add menu items. So it was no longer neccesary for me to replace the whole Add menu but now I can simply append to the add mesh submenu:


import dynamic_menu
menu_func = lambda self, context: self.layout.itemO("OBJECT_OT_addgear", text="Add Gear ...") 
menu_item = dynamic_menu.add(bpy.types.INFO_MT_mesh_add, menu_func) 

A great addition to the windowing API, although if you run the code twice, two identical entries are added to the menu but that’s a minor point.