graphic:python:blender

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
Next revisionBoth sides next revision
graphic:python:blender [2024/01/24 10:01] – [Blender variable naming standards] yinggraphic:python:blender [2024/04/09 10:46] – [My related dev notes] ying
Line 393: Line 393:
 ref:  ref: 
   * https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons   * https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons
 +  * https://developer.blender.org/docs/release_notes/2.80/python_api/addons/
   * https://s-nako.work/2020/12/blender-addon-naming-rules/   * https://s-nako.work/2020/12/blender-addon-naming-rules/
 +  * https://b3d.interplanety.org/en/class-naming-conventions-in-blender-2-8-python-api/
  
-  * Class name convention is: UPPER_CASE_{SEPARATOR}_mixed_case+  * **Class name convention**: UPPER_CASE_{SEPARATOR}_mixed_case
     * UPPER_CASE: normally the ADDON_NAME, or a Unique ID_WORD that make it not crashing into other existing class names     * UPPER_CASE: normally the ADDON_NAME, or a Unique ID_WORD that make it not crashing into other existing class names
     * {SEPARATOR}     * {SEPARATOR}
Line 407: Line 409:
       * _Check_Selection       * _Check_Selection
       * _check_selection       * _check_selection
-  * like  [A-Z][A-Z0-9_]*_MT_[A-Za-z0-9_]+ +    * like  [A-Z][A-Z0-9_]*_MT_[A-Za-z0-9_]+ 
-  * if not follow above: you get warning "doesn't contain '_PT_' with prefix & suffix" +    * if not follow above: you get warning "doesn't contain '_PT_' with prefix & suffix" 
- +  * **operator ID name convention** (feel like a username for your operator inside blender )
-  * operator ID name convention (feel like a username for your operator inside blender )+
     * category_name.operator_task_like_name     * category_name.operator_task_like_name
       * custom.addon_name_task_name (snake_case like PEP function)       * custom.addon_name_task_name (snake_case like PEP function)
Line 416: Line 417:
       * class: ADDON_NAME_OT_check_selection       * class: ADDON_NAME_OT_check_selection
       * bl_idname: addon_name.check_selection       * bl_idname: addon_name.check_selection
-      +  * **other same as PEP**
-  * other same as PEP+
     * obj = "BigBox"     * obj = "BigBox"
     * obj_max_count = 2     * obj_max_count = 2
Line 423: Line 423:
     * class ClassName()     * class ClassName()
  
 +Example of class name, balancing between Python PEP
 +  * example addon, <code python>
 +class BUILDMAKER_OT_CreateAsset(bpy.types.Operator):
 +    bl_idname = "custom.buildmaker_create_asset"
 +    
 +class CreateAsset(bpy.types.Operator):
 +    bl_idname = "BUILDMAKER_OT_CreateAsset"
 +
 +class BUILDMAKER_OT_createAsset(bpy.types.Operator):
 +
 +# 2.8x onwards, property
 +class MyOperator(bpy.types.Operator):
 +    value: IntProperty()
 +
 +class ToolProperties(bpy.types.PropertyGroup):
 +    commandLineMode: bpy.props.BoolProperty(
 +        name = "Command line mode",
 +        description = "The option specifies if the addon is used in the command line mode",
 +        default = False
 +    ) 
 +    
 +class ToolProps(bpy.types.PropertyGroup):
 +    email : StringProperty(
 +        name="email",
 +        description="User email",
 +        default=""
 +    )
 +class TOOL_PROPS(bpy.types.PropertyGroup):
 +    lon: FloatProperty()
 +    lat: FloatProperty()
 +class BUILDMAKER_PG_color(PropertyGroup):
 +    color: FloatVectorProperty(subtype='COLOR', min=0, max=1, update=updColor, size=4)
 +</code>
 ===== common blender api commands ===== ===== common blender api commands =====
  
Line 542: Line 575:
 **UI** **UI**
   * bpy.types.Panel   * bpy.types.Panel
 +    * the panel display order is determined by the order bpy.utils.register_class processes
   * bpy.types.UIList   * bpy.types.UIList
   * bpy.types.Operator   * bpy.types.Operator
Line 600: Line 634:
 ... ...
 layout.template_list(type_name, list_id, ptr, propname, active, rows, maxrows, ..) # a list widget layout.template_list(type_name, list_id, ptr, propname, active, rows, maxrows, ..) # a list widget
 +</code>
 +  * long label solution and auto wrap <code python>
 +import bpy
 +import textwrap
 +
 +long_text = """
 +        a long text  long text  long text
 +        b test
 +        c long text  long text  long text  long text  long text  long
 +        d text  long text  long text  long text """
 +
 +def get_max_label_width():
 +    # Get the 3D View area
 +    for area in bpy.context.screen.areas:
 +        if area.type == 'VIEW_3D':
 +            break
 +    # Calculate the width of the panel
 +    panel_width = 280 # default value
 +    for region in area.regions:
 +        if region.type == 'UI':
 +            panel_width = region.width
 +            return panel_width
 +            break
 +    # Calculate the maximum width of the label
 +    uifontscale = 9 * context.preferences.view.ui_scale
 +    max_label_width = int(panel_width // uifontscale)
 +    return max_label_width
 +
 +def create_long_label(layout, long_text, icon='',indent=''):
 +    max_label_width = get_max_label_width()
 +    first_icon_line_done = False
 +    rest_line_indent = indent
 +    if icon == '':
 +        first_icon_line_done = True
 +        rest_line_indent = ''
 +    # Split the text into lines and format each line
 +    for line in long_text.splitlines():
 +        # Remove leading and trailing whitespace
 +        line = line.strip()
 +        # Split the line into chunks that fit within the maximum label width
 +        for chunk in textwrap.wrap(line, width=max_label_width):
 +            if not first_icon_line_done:
 +                layout.label(text=chunk, icon=icon)
 +                first_icon_line_done = True
 +            else:
 +                layout.label(text=rest_line_indent+chunk)
 +
 +class MyPanel(bpy.types.Panel):
 +    bl_idname = "OBJECT_PT_my_panel"
 +    bl_label = "My Panel"
 +    bl_space_type = "VIEW_3D"
 +    bl_region_type = "UI"
 +    
 +    def draw(self, context):
 +        layout = self.layout
 +        create_long_label(layout, long_text, "KEYTYPE_JITTER_VEC",   ')
 +        
 +
 +# Register the panel
 +bpy.utils.register_class(MyPanel)
 </code> </code>
 ===== Blender preference ===== ===== Blender preference =====
Line 785: Line 879:
     * (Darkfall studio - tut,shorts,anime) https://www.youtube.com/@DarkfallBlender     * (Darkfall studio - tut,shorts,anime) https://www.youtube.com/@DarkfallBlender
     * (Luca Chirichella - blender tut) https://www.youtube.com/@RedKProduction     * (Luca Chirichella - blender tut) https://www.youtube.com/@RedKProduction
 +
 +====== My Blender addon template ======
 +
 +<code python>
 +import os
 +
 +import bpy
 +from bpy.types import AddonPreferences, PropertyGroup, UIList, Operator
 +from bpy.props import (
 +    StringProperty,
 +    IntProperty,
 +    CollectionProperty,
 +    EnumProperty,
 +    PointerProperty,
 +)
 +
 +bl_info = {
 +    "name": "UniversalTool",
 +    "description": "A demo template tool shows assets import and publish",
 +    "author": "Ying",
 +    "version": (1, 0, 0),
 +    "blender": (3, 2, 0),
 +    "location": "3D View > Tools",
 +    "category": "Object",
 +}
 +
 +
 +# =======================================
 +#  preference
 +# =======================================
 +class UniversalAddonPreferences(AddonPreferences):
 +    bl_idname = "UniversalTool"
 +
 +    asset_author_name: StringProperty(
 +        name="Asset Author Name",
 +        default="",
 +        description="Enter the name of the asset author",
 +    )
 +
 +    def draw(self, context):
 +        layout = self.layout
 +        layout.prop(self, "asset_author_name")
 +
 +
 +# =======================================
 +#  properties
 +# =======================================
 +class VariationItem(PropertyGroup):
 +    name: StringProperty(name="Name")
 +    variation_type: StringProperty(name="Type")
 +
 +
 +class VariationProps(PropertyGroup):
 +    variation_type_enum_items = [
 +        ("choice", "Visible Choice random", ""),
 +        ("option", "Visible Option random", ""),
 +        ("stack", "Visibile Stack end random", ""),
 +        ("linear", "Value Linear random", ""),
 +        ("step", "Value Step random", ""),
 +        ("material", "Value Material random", ""),
 +    ]
 +    variation_type: EnumProperty(
 +        items=variation_type_enum_items,
 +        name="Variation Type",
 +        description="Variation Type",
 +        default="choice",
 +    )
 +
 +
 +class PublishSetting(PropertyGroup):
 +    publish_type_enum_items = [
 +        ("new_asset", "New Asset", ""),
 +        ("update_asset", "Asset Update", ""),
 +    ]
 +    publish_name: StringProperty(
 +        name="Name", description="Asset name", default="", maxlen=1024
 +    )
 +    publish_version: IntProperty(
 +        name="Version", description="Version number.", default=1, min=1, max=9999
 +    )
 +    publish_dir: StringProperty(
 +        name="Publish Dir",
 +        description=("Asset Publish Directory."),
 +        subtype="DIR_PATH",
 +    )
 +    publish_type: EnumProperty(
 +        items=publish_type_enum_items,
 +        name="Publish Type",
 +        description="Publish Type",
 +        default="new_asset",
 +    )
 +
 +
 +# =======================================
 +#  operators
 +# =======================================
 +class AddCube(bpy.types.Operator):
 +    bl_idname = "custom.universal_add_cube"
 +    bl_label = "Simple Add Cube"
 +    bl_options = {"REGISTER", "UNDO" # Enable undo for this operator.
 +
 +    def execute(self, context):
 +        bpy.ops.mesh.primitive_cube_add()
 +        return {"FINISHED"}
 +
 +
 +# example of AddObject operator class without UNIVERSAL_OT_AddObject naming pattern
 +class AddObject(Operator):
 +    bl_idname = "custom.universal_add_object"
 +    bl_label = "Add Object"
 +
 +    def execute(self, context):
 +        obj = bpy.context.active_object
 +        wm = context.window_manager
 +        if obj:
 +            item = wm.variation_list.add()
 +            item.name = ",".join([x.name for x in context.selected_objects])
 +            item.variation_type = wm.variation_props.variation_type
 +        return {"FINISHED"}
 +
 +
 +class UNIVERSAL_OT_RemoveObject(Operator):
 +    bl_idname = "custom.universal_remove_object"
 +    bl_label = "Remove Object"
 +    bl_options = {"REGISTER"}
 +
 +    @classmethod
 +    def poll(cls, context):
 +        wm = context.window_manager
 +        return len(wm.variation_list) > 0
 +
 +    def execute(self, context):
 +        wm = context.window_manager
 +        idx = wm.variation_list_index
 +        wm.variation_list.remove(idx)
 +        return {"FINISHED"}
 +
 +
 +class PublishAsset(Operator):
 +    bl_idname = "custom.universal_publish_asset"
 +    bl_label = "Publish Asset"
 +
 +    @classmethod
 +    def poll(self, context):
 +        wm = context.window_manager
 +        publish_prop = wm.publish_setting_props
 +        return (
 +            os.path.isdir(publish_prop.publish_dir)
 +            and publish_prop.publish_name.strip() != ""
 +        )
 +
 +    def execute(self, context):
 +        wm = context.window_manager
 +        publish_prop = wm.publish_setting_props
 +        print(publish_prop.publish_dir)
 +        if publish_prop.publish_type != "new_asset":
 +            self.report({"ERROR"}, "Publish type must be new. (demo of error)")
 +            return {"FINISHED"}
 +        self.report({"INFO"}, "Publish successfully")
 +        return {"FINISHED"}
 +
 +
 +# =======================================
 +#  UI List
 +# =======================================
 +class UNIVERSAL_UL_VariationList(UIList):
 +    def draw_item(
 +        self, context, layout, data, item, icon, active_data, active_propname, index
 +    ):
 +        custom_icon = "BOOKMARKS"
 +        if self.layout_type in {"DEFAULT", "COMPACT"}:
 +            split = layout.split(factor=0.6)
 +            split.label(text=item.name, icon=custom_icon)
 +            split.label(text=item.variation_type)
 +        elif self.layout_type in {"GRID"}:
 +            layout.alignment = "CENTER"
 +            layout.label(text=item.name, icon=custom_icon)
 +            layout.label(text=item.variation_type)
 +
 +
 +# =======================================
 +#  panels
 +# =======================================
 +# UNIVERSAL_PT_VariationPanel: blender naming format
 +# VariationPanel: Python naming format results warning message
 +#    Warning: 'VariationPanel' does not contain '_PT_' with prefix and suffix
 +class UNIVERSAL_PT_VariationPanel(bpy.types.Panel):
 +    bl_space_type = "VIEW_3D"
 +    bl_region_type = "UI"
 +    bl_context = ""
 +    bl_category = "Universal"  # SIDE the tab name
 +    bl_label = "Universal Tool Panel"  # tab tab > panel name
 +
 +    def draw(self, context):
 +        wm = context.window_manager
 +        layout = self.layout
 +        publish_prop = wm.publish_setting_props
 +
 +        layout.operator("custom.universal_add_cube")
 +        layout.label(text="Variation Object List:", icon="EXPORT")
 +
 +        row = layout.row()
 +        row.template_list(
 +            listtype_name="UNIVERSAL_UL_VariationList",
 +            list_id="",
 +            dataptr=wm,
 +            propname="variation_list",
 +            active_dataptr=wm,
 +            active_propname="variation_list_index",
 +        )
 +
 +        layout.prop(wm.variation_props, "variation_type")
 +        row = layout.row()
 +        row.operator("custom.universal_add_object", icon="ADD", text="Add Object")
 +        row.operator(
 +            "custom.universal_remove_object", icon="REMOVE", text="Remove Object"
 +        )
 +
 +        layout.prop(publish_prop, "publish_name", text="Super Name")
 +        layout.prop(publish_prop, "publish_version")
 +        layout.prop(publish_prop, "publish_dir")
 +        layout.prop(publish_prop, "publish_type", expand=True)
 +        asset_author_name = bpy.context.preferences.addons[
 +            "UniversalTool"
 +        ].preferences.asset_author_name.replace(" ", "-")
 +        if (
 +            os.path.isdir(publish_prop.publish_dir)
 +            and publish_prop.publish_name.strip() != ""
 +        ):
 +            result_file = os.path.join(
 +                publish_prop.publish_dir,
 +                publish_prop.publish_name.strip()
 +                + f"_v{publish_prop.publish_version}_{asset_author_name}.json",
 +            )
 +            layout.label(text=f"output: {result_file}")
 +        else:
 +            layout.label(text="output: not valid setting")
 +        layout.operator("custom.universal_publish_asset")
 +
 +
 +# =======================================
 +#  class list to register
 +# =======================================
 +classes = (
 +    UniversalAddonPreferences,
 +    # properties
 +    VariationItem,
 +    VariationProps,
 +    PublishSetting,
 +    # operators
 +    AddCube,
 +    AddObject,
 +    UNIVERSAL_OT_RemoveObject,
 +    PublishAsset,
 +    # ui
 +    UNIVERSAL_UL_VariationList,
 +    UNIVERSAL_PT_VariationPanel,
 +)
 +
 +
 +def register():
 +    for cls in classes:
 +        bpy.utils.register_class(cls)
 +    bpy.types.WindowManager.variation_list = CollectionProperty(type=VariationItem)
 +    bpy.types.WindowManager.variation_list_index = IntProperty(default=0)
 +    bpy.types.WindowManager.variation_props = PointerProperty(type=VariationProps)
 +    bpy.types.WindowManager.publish_setting_props = PointerProperty(type=PublishSetting)
 +
 +
 +def unregister():
 +    del bpy.types.WindowManager.variation_list
 +    del bpy.types.WindowManager.variation_list_index
 +    del bpy.types.WindowManager.variation_type_props
 +    del bpy.types.WindowManager.publish_setting_props
 +    for cls in classes:
 +        bpy.utils.unregister_class(cls)
 +
 +
 +if __name__ == "__main__":
 +    register()
 +
 +</code>
 +
  • graphic/python/blender.txt
  • Last modified: 2024/05/15 04:30
  • by ying