Property for each individual face

Hi,

I’m currently working on an import/export script for Blender 2.69 and I need to be able to assign a couple of properties to each face/polygon. This isn’t actually my biggest problem since I’ve figgured this out:
bpy.types.MeshPolygon.my_prop = True

My biggest problem is that I want to be able to select a face and then being able to set my_prop for this face to True/False through the UI in the tools panel. This works great for objects, like this:

import bpy
from bpy.props import *

bpy.types.Object.my_prop = BoolProperty(default = True)

class Test(bpy.types.Panel):
    bl_label = "Test"
    bl_idname = "test_panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    
    def draw(self, context):
        row = self.layout.row()
        row.prop(context.object, "my_prop", text = "My prop")

bpy.utils.register_class(Test)

I basically want the exact same thing, except for faces/polygons instead of objects. Is it possible?

MeshPolygon type is not an ID type and thus doesn’t support bpy.props properties.

You can use
bpy.context.object.data.polygon_layers_int

instead. Or custom data layers with bmesh module:
http://www.blender.org/documentation/blender_python_api_2_69_1/bmesh.html#customdata-access

Note that custom data may transfer to new faces (e.g. extrude).

I’ve been reading about bmesh and I’ve got no clue on how to use it. It doesn’t make sense to make a bmesh from a mesh, and then copy the bmesh data back into the mesh. What’s up with that? If you’ve got any example code regarding bmesh and custom layers, that would be very helpful! I can’t seem find much info about it on the internet.

Why the code below work? It says “property not found: MeshPolygon.my_prop” in the console, but I’ve obviously set my_prop.

import bpy
from bpy.props import *

bpy.types.MeshPolygon.my_prop = 'test'

class Test(bpy.types.Panel):
    bl_label = "Test"
    bl_idname = "test_panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    
    @classmethod
    def poll(self, context):
        return context.object is not None and context.object.type == 'MESH' and context.mode == 'EDIT_MESH'
    
    def draw(self, context):
        row = self.layout.row()
        row.prop(context.object.data.polygons[0], "my_prop")

bpy.utils.register_class(Test)

Read this:

This API gives access the blenders internal mesh editing api, featuring geometry connectivity data and access to editing operations such as split, separate, collapse and dissolve.

The features exposed closely follow the C API, giving python access to the functions used by blenders own mesh editing tools.
For an overview of BMesh data types and how they reference each other see: BMesh Design Document .

Source

If you access a mesh in editmode via bmesh.from_edit_mesh(), there is no conversion between mesh and bmesh (mesh is actually a bmesh, but in the context of modelling system, bmesh module is a python module to access bmesh functions and data).

bpy.types.MeshPolygon is not any of the supported types:

Custom properties can be added to any subclass of an ID, Bone and PoseBone.

Source

Here’s a minimal comparison between standard API vs bmesh module custom data access:

import bpy
import bmesh
from bpy import data as D
from bpy import context as C

f = C.object.data.polygon_layers_float.new("my flop")
f.data[0].value = 1

print(f.data[0].value) # 1.0
print(f.data[1].value # 0.0), the default

bm = bmesh.new()
bm.from_mesh(C.object.data)
l = bm.faces.layers.float['my flop'] # layer created via standard API above

print(bm.faces[0][l]) # 1.0
print(bm.faces[1][l]) # 1.0

# create int layer in bmesh module mannor
li = bm.faces.layers.int.new("custom_int_layer")

bm.faces[5][li] = 44
print(bm.faces[5][li]) # 44
print(bm.faces[6][li]) # 0, the default

Thanks, but I still can’t make it visible in the user interface. Well, I’ve successfully made it visible in the UI, but I can’t set the value. This is what I’ve come up with so far:

import bpy
import bmesh

class MyPanel(bpy.types.Panel):
    bl_label = 'My panel'
    bl_idname = 'test.my_panel'
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    
    @classmethod
    def poll(self, context):
        return context.object is not None and context.mode == 'EDIT_MESH'

    def draw(self, context):
        mesh = context.object.data
        bm = bmesh.from_edit_mesh(mesh)
        try:
            layer = bm.faces.layers.float['test']
        except:
            layer = bm.faces.layers.float.new('test')
        bm.faces[0][layer] = 1
        face_index = bm.faces.active.index
        
        row = self.layout.row()
        row.label('Face index: '+str(face_index))
        row = self.layout.row()
        row.label(str(bm.faces[face_index][layer]))

bpy.utils.register_class(MyPanel)

In order to make it possible to set the “property”, I need to use row.prop() instead of row.label(). How do I do this? I mean, row.prop() expect one AnyType(?) and one string.

bm properties are not bpy.props properties, you can’t add the former to the UI.

Here’s a working solution, but i really discourage it, since it redraws the list on every scene update (~20 times per second?).
See the data tab in mesh edit mode, and select one or multiple faces:

import bpy
import bmesh

class MeshFaceLayerList(bpy.types.UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        #ob = data

        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.label(item.name)
            layout.prop(item, "value", text="", emboss=False)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label(text="")


class MESH_OT_face_layer_add(bpy.types.Operator):
    """Tooltip"""
    bl_label = "Add Face Layer"
    bl_idname = "mesh.face_layer_add"
    
    @classmethod
    def poll(cls, context):
        return (context.object is not None and
                context.object.type == 'MESH' and
                context.object.mode == 'EDIT')

    def execute(self, context):
        bm = bmesh.from_edit_mesh(context.object.data)
        try:
            bm.faces.layers.float['test']
        except KeyError:
            bm.faces.layers.float.new('test')
        else:
            return {'CANCELLED'}
            
        return {'FINISHED'}
    
    
class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "data"

    @classmethod
    def poll(cls, context):
        return (context.object is not None and
                context.object.type == 'MESH' and
                context.object.mode == 'EDIT')

    def draw(self, context):
        layout = self.layout
        wm = context.window_manager
        
        ob = context.object
        me = ob.data
        bm = bmesh.from_edit_mesh(me)

        try:
            l = bm.faces.layers.float['test']
        except KeyError:
            layout.label("No custom data layer 'test'.")
            layout.operator("mesh.face_layer_add")
            return
        
        wm.mesh_face_layer.faces.clear()
        for f in bm.faces:
            if f.select:
                item = wm.mesh_face_layer.faces.add()
                item.name = "Face #%i" % f.index
                item.index = f.index
                item.value = f[l]
                
        layout.template_list("MeshFaceLayerList", "", wm.mesh_face_layer, "faces", wm.mesh_face_layer, "index")


def upd(self, context):
    try:
        me = context.object.data
        bm = bmesh.from_edit_mesh(me)
        bm.faces[self.index][bm.faces.layers.float['test']] = self.value
        bmesh.update_edit_mesh(me, tessface=False, destructive=False)
    except:
        pass

class MeshFaceLayerFace(bpy.types.PropertyGroup):
    value = bpy.props.FloatProperty(update=upd)
    index = bpy.props.IntProperty()

    
class MeshFaceLayer(bpy.types.PropertyGroup):
    index = bpy.props.IntProperty()
    faces = bpy.props.CollectionProperty(type=MeshFaceLayerFace)

def register():
    bpy.utils.register_module(__name__)
    bpy.types.WindowManager.mesh_face_layer = bpy.props.PointerProperty(type=MeshFaceLayer, options={'SKIP_SAVE'})


def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.WindowManager.mesh_face_layer


if __name__ == "__main__":
    register()


That’s something similar to what I want. I actually don’t really need a list of each selected face, I might have been a little unclear about that. Anyway, your script helped me a lot, it made me think outside the box a little. I succesfully made a script that seems to work rather well. Here it is:

import bpy
import bmesh
from bpy.props import *

class FaceProperties(bpy.types.PropertyGroup):
    index = IntProperty(default = -1)
    my_prop = BoolProperty(default = True)
    
bpy.utils.register_class(FaceProperties)
bpy.types.WindowManager.current_face = PointerProperty(type = FaceProperties)

class FacePropertiesPanel(bpy.types.Panel):
    bl_label = "Face properties"
    bl_idname = "face_properties"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    
    @classmethod
    def poll(cls, context):
        return context.mode == 'EDIT_MESH'
    
    def draw(self, context):
        me = context.object.data
        bm = bmesh.from_edit_mesh(me)
        
        try:
            my_prop_layer = bm.faces.layers.int['my_prop']
        except:
            my_prop_layer = bm.faces.layers.int.new('my_prop')
            for face in bm.faces:
                face[my_prop_layer] = 1
        
        if bm.faces.active.index != context.window_manager.current_face.index:
            context.window_manager.current_face.index = bm.faces.active.index
            context.window_manager.current_face.my_prop = False if bm.faces.active[my_prop_layer] is 0 else True
        
        # Sets the transparency, my_prop and doublesided for the current face.
        bm.faces.active[my_prop_layer] = 0 if context.window_manager.current_face.my_prop is False else 1
        
        # Updates the mesh
        bmesh.update_edit_mesh(me)
        
        # Draws the panel.
        row = self.layout.row()
        row.label('Current face: '+str(context.window_manager.current_face.index))
        row = self.layout.row()
        row.prop(context.window_manager.current_face, "my_prop", text = "My prop")

bpy.utils.register_class(FacePropertiesPanel)

Please, let me know if there’s an easier way of accomplishing this. I’m pretty sure there is. I like clean, straight-forward code which does exactly what you want in just a few lines so this looks a bit messy to me.

Right now you’ll need to select each face, one by one, and set the properties. I’ll make an operator which let’s you apply the properties to all selected faces, this will probably be rather easy.

Thanks for all your help! :slight_smile:

there’s no easier solution IMO, I tried Mesh.polygon_layers_int to get rid of bmesh from / to mesh, but it works in object mode only. So should be fine!