First Blender 3D Add-on
This post is a contribution to The 32-Bit Cafe's Community Code Jam #5 for August 4-17, 2024.
Other than this website, I haven't done any meaty coding projects for quite some time. Roughly two years ago I wanted to create 3D virtual environments for my 2D traditional media and digital artworks using Blender 3D, and hopefully with semi-passable renderings at the onset.
During the initial experiments, creating the picture frames each and every time had become a pain point, so some level of automation was needed for the base frame creation, one that would allow real-time sizing to match the dimensions of the original source artwork.
Eventually I began to research Blender Add-on creation and after a week or so I was finally able to get a prototype running and uploaded it then to my now defunct Github account.
Revisiting the Project
Fast-forward to 2024, roughly two years later, and I got the itch to revisit this project but had recently migrated from Windows to Debian Linux and misplaced the original code unfortunately.
After a rather exhaustive search of old USB hard drives and flash drives, I finally located an early version of the code, originally written for Blender v3.1 Fortunately, it still works — well, sort of — in version v4.1 at the time of this writing.
The screenshot above shows a barren 3D view with a new mesh created by the add-on which provides, in the lower left corner, a widget to adjust height, width, frame thickness, and frame depth dynamically, thus making it simple to match the dimensions of the source art. Once the original dimensions are set, the frame mesh can be gussied up with various adornments as shown in the sample rendering below.
Sample Rendering
A minor goal earlier this year was to emulate a traditional media pen and ink on relatively thin printer paper inside a recessed picture frame hung upon a wall. Unfortunately, my texturing and lighting skills are rather grim, but I think it got the point across. But who knows, faux alligator skin textured picture frames may become all the rage in the art world circa 2025!
The Code
Since this was a learning project, I'm unsure if the code adheres to Blender 3D Add-on coding best practices. My original intent was for a quick and dirty tool to get the job done, but I felt it might be self-explanatory enough as a good jumping point for those wishing to create their own Blender Add-on for the first time.
FYI: Right now the adjustment widget is not popping up consistently for reasons unknown. It may have something to do with Blender versions, but more research is needed.
In the original version, all the code was contained within one file named picture_frame_starter.py, and that was the only file used for installation in the Blender Preferences Add-ons section.
bl_info = {
'name': 'Picture Frame Starter',
'version': (0, 1),
'author': 'Todd Presta',
'blender': (3, 1, 0),
'category': 'Mesh',
'location': 'Operator Search'
}
import bpy
class PictureFrameStarterPanel(bpy.types.Panel):
bl_label = 'Picture Frame Starter'
bl_idname = 'PANEL_PT_picture_frame_starter'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'TP'
def draw(self, context):
layout = self.layout
self.layout.label(text="Dimensions")
box = layout.box()
row = box.row()
row.prop(context.scene, 'frame_width')
row = box.row()
row.prop(context.scene, 'frame_height')
row = box.row()
row.prop(context.scene, 'frame_depth')
row = box.row()
row.prop(context.scene, 'frame_bar_width')
self.layout.operator(PictureFrameStarterOp.bl_idname, text='Generate')
class MESH_OT_picture_frame_starter(bpy.types.Operator):
"""Create a starter picture frame"""
bl_idname = 'mesh.picture_frame_starter'
bl_label = 'Picture Frame Starter'
bl_options = {'REGISTER', 'UNDO'}
frame_width: bpy.props.FloatProperty (
name = 'Frame Width',
default = 8.0
)
frame_height: bpy.props.FloatProperty (
name = 'Frame Height',
default = 10.0
)
frame_depth: bpy.props.FloatProperty (
name = 'Frame Depth',
default = 1.0
)
frame_bar_width: bpy.props.FloatProperty (
name = 'Frame Bar Width',
default = 2.0
)
def execute(self, context):
"""Generate Base Frame"""
generator = PictureFrameStarterGenerator (
self.frame_width, self.frame_height,
self.frame_depth, self.frame_bar_width
)
generator.generate()
return {"FINISHED"}
class PictureFrameStarterGenerator:
"""This class was developed as part of a self-directed Blender 3D API
(in v3.1.2) learning project and generates a simple four-bar starter picture
frame mesh based on passed-in width, height, depth, and bar width. Calculations
begin with the inner surfaces of the frame and then expand outward by frame
bar width. Starts with planar left frame, mirrors it, bridges the
some edges, and then it extrudes all faces by depth value.
"""
COLL_NM = 'Picture Frame Starter'
MESH_NM = 'frame_starter_mesh'
OBJ_NM = 'frame_starter_obj'
"""JOINS: A hack to predetermine the correct edges to bridge based
based on the calculated vertices until we can figure out how to
do it programmatically :)
"""
JOINS = (1, 3, 5, 7)
def __init__(self, w, h, d, bw):
self.w = w
self.h = h
self.d = d
self.bw = bw
def _calc_verts(self):
"""Calculate the vertex positions of the leftmost vertical
frame bar which would actually be the right side of the frame
from behind. Assumes 3D cursor in center of world and no
other objects selected.
B
|\ A
| |
|/ C
D
Spelling out the calcs below for clarity :)
"""
frm_depth = -self.d/2
pt_A = (-(self.w)/2, frm_depth, self.h/2)
pt_B = (pt_A[0]-self.bw, frm_depth, (self.h/2)+self.bw)
pt_C = (-(self.w)/2, frm_depth, -(self.h/2))
pt_D = (pt_C[0]-self.bw, frm_depth, pt_C[2]-self.bw)
self.verts = [ pt_A, pt_B, pt_C, pt_D ]
def generate(self):
"""Generates a rectangular picture frame with four frame
bars each meeting at 45 degree angles, centered in the 3D
view and in it's own Scene Collection.
"""
# Calculate verts
self._calc_verts()
# Create frame mesh
frame_mesh = bpy.data.meshes.new(self.MESH_NM)
frame_mesh.from_pydata(self.verts, [], [])
frame_mesh.update()
# Create frame object for mesh
frame_base_obj = bpy.data.objects.new(self.OBJ_NM, frame_mesh)
# Create Collection - add frame
frame_base_coll = bpy.data.collections.new(self.COLL_NM)
bpy.context.scene.collection.children.link(frame_base_coll)
frame_base_coll.objects.link(frame_base_obj)
# Set the frame active
bpy.context.view_layer.objects.active = bpy.data.objects[self.OBJ_NM]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.edge_face_add()
# Mirror vertical frame bar with modifier
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.object.modifiers.new(name="MM",type='MIRROR')
bpy.ops.object.modifier_apply(modifier="MM")
# Go into EDIT mode with all edges deselected
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False,type='EDGE')
bpy.ops.mesh.select_all(action='DESELECT')
# Select and bridge top edges of both vertical frame bars
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.active_object.data.edges[self.JOINS[0]].select = True
bpy.context.active_object.data.edges[self.JOINS[2]].select = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.bridge_edge_loops()
bpy.ops.mesh.select_all(action='DESELECT')
# Select and bridge bottom edges of vertical frame bars
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.active_object.data.edges[self.JOINS[1]].select = True
bpy.context.active_object.data.edges[self.JOINS[3]].select = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.bridge_edge_loops()
"""Select all edges and extrude, then deselect
TODO: Determine if more params needed for extrude method.
"""
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.extrude_region_move(
TRANSFORM_OT_translate = {
"value":(-0, self.d, 0)
}
)
bpy.ops.mesh.select_all(action='DESELECT')
def register():
bpy.utils.register_class(MESH_OT_picture_frame_starter)
def unregister():
bpy.utils.unregister_class(MESH_OT_picture_frame_starter)
if __name__ == '__main__':
register()
"""
MIT License
Copyright (c) 2022 Todd Presta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
From the original README.txt
The add-on is launched via the Operator Search Pop-up Menu and will create
a simple and configurable 8-unit x 10-unit (inner dimensions) four-bar picture
frame with two-unit width frame bars and one unit in depth. Afterward, the
edges can be beveled or contours created with additional loop cuts to gussy up
the frame a bit if desired.
The add-on requires "Developer Extras" to be enabled in Blender
Preferences -> Interface -> Display.
Welp, that's about it :)
