2

As a relative newcomer to python (but not to programming in general) I tried my hands at making custom meshes using blender python templates and tutorials, but now I am stuck at a rather cosmetic issue.

For a specific application, I want to make meshes that contain duplicates of themselves at different scales. Imagine it like a matryoshka doll.

As an example, I have tried to implement the basic functionality I want by building on the "operator_mesh_add.py" template that comes with blender. The code is attached at the end of my question. It should work if you copy it into the blender text editor and run it.

Each of the inner boxes is supposed to have an adjustable relative scale (between 0.0 and 1.0) that is saved in a CollectionProperty. If you try to add a mesh object of this type to the scene, you will see the pop-up UI containing all the properties of the box. "Width", "Height" and "Depth" are simple FloatProperties and can be adjusted here, but the "Planes" CollectionProperty only shows its number of items (which is zero, if you start from default values), but no way to see their scale property or add new ones.

I was able to get around this issue by making a new panel in the "Tool" section of the 3D viewport UI that holds a "Planes" UI list. Adding new planes will give you an adjustable scale for each of them. Once you are happy with the list, you can add a "Scale Box" mesh object which shows the kind of geometry I want, but with no way to adjust its planes through the list after creation. To properly see the result, you should switch to either x-ray or wireframe mode.

I could probably link the behaviour of the created mesh to the list in the panel, but that would be rather awkward, as it would affect every mesh created this way simultaneously. Ideally, I would want the mesh adjustable through a scales list once it has been created, but to remain independent from other mesh objects.

Now as I stated in my header: is there a way to add this functionality to the pop-up UI? Or do I have to find out how to design my own mesh object adder and pop-up from scratch? If so, I would be happy about recommendations for tutorials and examples.

I am guessing that it all hinges on how flexible this method is:

from bpy_extras import object_utils
object_utils.object_data_add(context, mesh, operator=self, name="ScaleBox")

This is my working example:

bl_info = {
    "name" : "Scale Box",
    "author" : "eSemmel",
    "version" : (1, 0),
    "blender" : (2, 93, 4),
    "location" : "View3d > Tool",
    "warning" : "",
    "wiki_url" : "",
    "category" : "Add Mesh",
}

import bpy import bmesh from bpy_extras.object_utils import AddObjectHelper

from bpy.props import ( IntProperty, FloatProperty, CollectionProperty, )

define the container for the scale property

class PlanesClass(bpy.types.PropertyGroup): """Contains a plane scale""" scale: FloatProperty( name="Scale", description="Scale of a Box Plane", min=0.0, max=1.0, default=1.0, )

def add_box(width, height, depth, planes): """ This function takes inputs and returns vertex and face arrays. no actual mesh data creation is done here. """

core_verts = [
    (+1.0, +1.0, -1.0),
    (+1.0, -1.0, -1.0),
    (-1.0, -1.0, -1.0),
    (-1.0, +1.0, -1.0),
    (+1.0, +1.0, +1.0),
    (+1.0, -1.0, +1.0),
    (-1.0, -1.0, +1.0),
    (-1.0, +1.0, +1.0),
]

core_faces = [
    (0, 1, 2, 3),
    (4, 7, 6, 5),
    (0, 4, 5, 1),
    (1, 5, 6, 2),
    (2, 6, 7, 3),
    (4, 0, 3, 7),
]

# get the float values as scales of the inner planes
scales = [plane.scale for plane in planes]
# scale 1.0 always exists as the outermost plane and does not have to be specified
if not scales:
    scales = [1.0]
elif 1.0 not in scales:
    scales.append(1.0)

# initialize lists
n_core = len(core_verts)
verts = [()] * len(scales) * n_core

# apply size
for scale_index, scale in enumerate(scales):
    for i, v in enumerate(core_verts):
        verts[i + scale_index * n_core] = v[0] * width * scale, v[1] * depth * scale, v[2] * height * scale

# define faces
faces = [tuple(face_it + scale_it * n_core for face_it in core_face) for scale_it in range(len(scales)) for core_face in core_faces]
return verts, faces


class AddBox(bpy.types.Operator, AddObjectHelper): """Add a scale box mesh""" bl_idname = "mesh.primitive_scale_box_add" bl_label = "Add Scale Box" bl_options = {'REGISTER', 'UNDO'}

width: FloatProperty(
    name="Width",
    description="Box Width",
    min=0.01, max=100.0,
    default=1.0,
)
height: FloatProperty(
    name="Height",
    description="Box Height",
    min=0.01, max=100.0,
    default=1.0,
)
depth: FloatProperty(
    name="Depth",
    description="Box Depth",
    min=0.01, max=100.0,
    default=1.0,
)
planes: CollectionProperty(
    type=PlanesClass,
    name="Planes",
    description="Box Planes (scales)",
)

# fill the planes at creation from the collection saved in the scene
def invoke(self, context, event):
    self.planes.clear()
    for plane in context.scene.scalebox_planes:
        new_plane = self.planes.add()
        new_plane.scale = plane.scale
    return self.execute(context)

def execute(self, context):

    verts_loc, faces = add_box(
        self.width,
        self.height,
        self.depth,
        self.planes,
    )

    mesh = bpy.data.meshes.new("ScaleBox")

    bm = bmesh.new()

    for v_co in verts_loc:
        bm.verts.new(v_co)

    bm.verts.ensure_lookup_table()
    for f_idx in faces:
        bm.faces.new([bm.verts[i] for i in f_idx])

    bm.to_mesh(mesh)
    mesh.update()

    # add the mesh as an object into the scene with this utility module
    from bpy_extras import object_utils
    object_utils.object_data_add(context, mesh, operator=self, name="ScaleBox")

    return {'FINISHED'}

make an add menu entry for the scale box

def menu_func(self, context): self.layout.operator(AddBox.bl_idname, icon='MESH_CUBE')

panel class for adding a scale box

class AddBoxPanel(bpy.types.Panel): """Main Panel for adding scale boxes""" bl_label = "Scale Box Panel" bl_idname = "PT_AddScaleBox" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Tool"

def draw(self, context):
    layout = self.layout
    row = layout.row()
    row.label(text="Options")


panel within panel

class PlanesPanel(bpy.types.Panel): """Sub panel for adding planes to the scale box""" bl_label = "Planes" bl_idname = "PT_AddPlanes" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Tool" bl_parent_id = "PT_AddScaleBox" bl_options = {'HIDE_HEADER'}

def draw(self, context):
    layout = self.layout
    row = layout.row()
    row.label(text="Planes", icon='FACESEL')
    row = layout.row()
    row.template_list("Planes_UI_list", "", context.scene, "scalebox_planes", context.scene, "scalebox_planes_index")
    row = layout.row()
    row.operator("scalebox_planes.new_item", text="New")
    row = layout.row()
    row.operator("scalebox_planes.delete_item", text="Delete")
    row = layout.row()
    row.operator("mesh.primitive_scale_box_add")


UI_List

class Planes_UI_list(bpy.types.UIList): """UI List for planes scales""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.label(text="", icon='FACESEL') layout.prop(item, "scale") elif self.layout_type in {'GRID'}: layout.alignment='CENTER' layout.label(text="", icon='FACESEL')

class LIST_OT_NewItem(bpy.types.Operator): """Add a new plane scale to the list""" bl_idname="scalebox_planes.new_item" bl_label="Add a new plane scale"

def execute(self, context):
    context.scene.scalebox_planes.add()

    return {'FINISHED'}

class LIST_OT_DeleteItem(bpy.types.Operator): """Delete a plane scale from the list""" bl_idname="scalebox_planes.delete_item" bl_label="Delete a plane scale"

@classmethod
def poll(cls, context):
    return context.scene.scalebox_planes

def execute(self, context):
    scalebox_planes = context.scene.scalebox_planes
    index = context.scene.scalebox_planes_index
    scalebox_planes.remove(index)
    context.scene.scalebox_planes_index = min(max(0, index - 1), len(scalebox_planes) - 1)

    return {'FINISHED'}


def register(): bpy.utils.register_class(PlanesClass) bpy.types.Scene.scalebox_planes = CollectionProperty(type=PlanesClass, name="Planes", description="Box Planes (scales)") bpy.types.Scene.scalebox_planes_index = IntProperty(name="Planes Index", description="Index of the box plane", default = 0) bpy.utils.register_class(Planes_UI_list) bpy.utils.register_class(LIST_OT_NewItem) bpy.utils.register_class(LIST_OT_DeleteItem) bpy.utils.register_class(AddBox) bpy.utils.register_class(AddBoxPanel) bpy.utils.register_class(PlanesPanel) bpy.types.VIEW3D_MT_mesh_add.append(menu_func)

def unregister(): bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) bpy.utils.unregister_class(PlanesPanel) bpy.utils.unregister_class(AddBoxPanel) bpy.utils.unregister_class(AddBox) bpy.utils.unregister_class(LIST_OT_DeleteItem) bpy.utils.unregister_class(LIST_OT_NewItem) bpy.utils.unregister_class(Planes_UI_list) bpy.utils.unregister_class(PlanesClass)

if name == "main": register()

eSemmel
  • 23
  • 3

1 Answers1

1

The redo panel displays only first class properties by default. If you want to display the last item of a CollectionProperty you have to override the draw method of the operator.

enter image description here

    #...
planes: CollectionProperty(
    type=PlanesClass,
    name="Planes",
    description="Box Planes (scales)",
)

# fill the planes at creation from the collection saved in the scene
def invoke(self, context, event):
    self.planes.clear()
    for plane in context.scene.scalebox_planes:
        new_plane = self.planes.add()
        new_plane.scale = plane.scale
    return self.execute(context)

def draw(self, context):
    layout = self.layout
    row = layout.row()
    row.prop(self, "width")
    row = layout.row()
    row.prop(self.planes[-1], "scale") # Get the latest item
    #...

But this won't update the item of the UIList since operator properties are encapsulated and planes is a new instance of the class. If you want to change both properties at the same time you can implement an update function or use setter/getters to change the property of the UIList too: https://docs.blender.org/api/current/bpy.props.html?highlight=setter#getter-setter-example

Additional Notes

pyCod3R
  • 1,926
  • 4
  • 15
  • 1
    Thank you so much! I was stuck on how to access the pop-up UI to make changes, but if I can simply use the draw function of the object, that solves all the problems and I don't even need the tool panel anymore. Thanks also for the additional links. I will have a read. – eSemmel Jan 16 '22 at 22:53