todd presta ddot com

First Blender 3D Add-on

by: Todd Presta
Created:

This post is a contribution to The 32-Bit Cafe's Community Code Jam #5 for August 4-17, 2024.

88x31 image for 32-bit Cafe Code Jam #5

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.

Screenshot of Blender UI showing my picture frame add-on.

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!

LOL 3 Nouns Prompt game 6 for (Elf or Puma), bathroom, and roller skates. From view of bathroom stalls with the middle one occupied and showing only elf shoe roller skates.

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