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()