Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
graphic:python:blender [2023/11/15 02:17] – [Blender preference] ying | graphic:python:blender [2024/04/09 10:46] (current) – [My related dev notes] ying | ||
---|---|---|---|
Line 38: | Line 38: | ||
if each in bpy.data.objects.keys(): | if each in bpy.data.objects.keys(): | ||
bpy.data.objects.remove(bpy.data.objects[each], | bpy.data.objects.remove(bpy.data.objects[each], | ||
+ | </ | ||
+ | |||
+ | ===== cmd related operation ===== | ||
+ | |||
+ | * send blender file and python script to blender process with parameter <code python> | ||
+ | # blender use empty scene to load task script with parameter after --, as (blender file, blender preview path, resolution) | ||
+ | app_path = bpy.app.binary_path | ||
+ | task_path = os.path.join( os.path.dirname(__file__), | ||
+ | info=subprocess.Popen(f'" | ||
+ | out, err = info.communicate() | ||
+ | </ | ||
+ | * task script reading parameter inside blender <code python> | ||
+ | # task script | ||
+ | import sys | ||
+ | import bpy | ||
+ | argv = sys.argv | ||
+ | argv = argv[argv.index(" | ||
+ | print(argv) | ||
</ | </ | ||
Line 375: | Line 393: | ||
ref: | ref: | ||
* https:// | * https:// | ||
+ | * https:// | ||
* https:// | * https:// | ||
+ | * https:// | ||
- | * Class name convention | + | |
- | * Header : _HT_ | + | |
- | * Menu : _MT_ | + | * {SEPARATOR} |
- | * Operator : _OT_ | + | |
- | * Panel : _PT_ | + | * Menu : _MT_ |
- | * UIList : _UL_ | + | * Operator : _OT_ |
- | * like [A-Z][A-Z0-9_]*_MT_[A-Za-z0-9_]+ | + | * Panel : _PT_ |
- | * if not follow above: you get warning " | + | * UIList : _UL_ |
+ | * mixed_case | ||
+ | * _CheckSelection (this more like standard python PEP class naming) | ||
+ | * _Check_Selection | ||
+ | * _check_selection | ||
+ | | ||
+ | * if not follow above: you get warning " | ||
+ | * **operator ID name convention** (feel like a username for your operator inside blender ) | ||
+ | * category_name.operator_task_like_name | ||
+ | * custom.addon_name_task_name (snake_case like PEP function) | ||
+ | * For headers, menus and panels, the bl_idname is expected to match the class name | ||
+ | * class: ADDON_NAME_OT_check_selection | ||
+ | * bl_idname: addon_name.check_selection | ||
+ | * **other same as PEP** | ||
+ | * obj = " | ||
+ | * obj_max_count = 2 | ||
+ | * def function_name(arg_name) | ||
+ | * class ClassName() | ||
+ | Example of class name, balancing between Python PEP | ||
+ | * example addon, <code python> | ||
+ | class BUILDMAKER_OT_CreateAsset(bpy.types.Operator): | ||
+ | bl_idname = " | ||
+ | | ||
+ | class CreateAsset(bpy.types.Operator): | ||
+ | bl_idname = " | ||
- | + | class BUILDMAKER_OT_createAsset(bpy.types.Operator): | |
+ | # 2.8x onwards, property | ||
+ | class MyOperator(bpy.types.Operator): | ||
+ | value: IntProperty() | ||
+ | |||
+ | class ToolProperties(bpy.types.PropertyGroup): | ||
+ | commandLineMode: | ||
+ | name = " | ||
+ | description = "The option specifies if the addon is used in the command line mode", | ||
+ | default = False | ||
+ | ) | ||
+ | | ||
+ | class ToolProps(bpy.types.PropertyGroup): | ||
+ | email : StringProperty( | ||
+ | name=" | ||
+ | description=" | ||
+ | default="" | ||
+ | ) | ||
+ | class TOOL_PROPS(bpy.types.PropertyGroup): | ||
+ | lon: FloatProperty() | ||
+ | lat: FloatProperty() | ||
+ | class BUILDMAKER_PG_color(PropertyGroup): | ||
+ | color: FloatVectorProperty(subtype=' | ||
+ | </ | ||
===== common blender api commands ===== | ===== common blender api commands ===== | ||
Line 508: | 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 566: | Line 634: | ||
... | ... | ||
layout.template_list(type_name, | layout.template_list(type_name, | ||
+ | </ | ||
+ | * 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 == ' | ||
+ | break | ||
+ | # Calculate the width of the panel | ||
+ | panel_width = 280 # default value | ||
+ | for region in area.regions: | ||
+ | if region.type == ' | ||
+ | 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, | ||
+ | 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, | ||
+ | if not first_icon_line_done: | ||
+ | layout.label(text=chunk, | ||
+ | first_icon_line_done = True | ||
+ | else: | ||
+ | layout.label(text=rest_line_indent+chunk) | ||
+ | |||
+ | class MyPanel(bpy.types.Panel): | ||
+ | bl_idname = " | ||
+ | bl_label = "My Panel" | ||
+ | bl_space_type = " | ||
+ | bl_region_type = " | ||
+ | | ||
+ | def draw(self, context): | ||
+ | layout = self.layout | ||
+ | create_long_label(layout, | ||
+ | | ||
+ | |||
+ | # Register the panel | ||
+ | bpy.utils.register_class(MyPanel) | ||
</ | </ | ||
===== Blender preference ===== | ===== Blender preference ===== | ||
Line 751: | Line 879: | ||
* (Darkfall studio - tut, | * (Darkfall studio - tut, | ||
* (Luca Chirichella - blender tut) https:// | * (Luca Chirichella - blender tut) https:// | ||
+ | |||
+ | ====== My Blender addon template ====== | ||
+ | |||
+ | <code python> | ||
+ | import os | ||
+ | |||
+ | import bpy | ||
+ | from bpy.types import AddonPreferences, | ||
+ | from bpy.props import ( | ||
+ | StringProperty, | ||
+ | IntProperty, | ||
+ | CollectionProperty, | ||
+ | EnumProperty, | ||
+ | PointerProperty, | ||
+ | ) | ||
+ | |||
+ | bl_info = { | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # preference | ||
+ | # ======================================= | ||
+ | class UniversalAddonPreferences(AddonPreferences): | ||
+ | bl_idname = " | ||
+ | |||
+ | asset_author_name: | ||
+ | name=" | ||
+ | default="", | ||
+ | description=" | ||
+ | ) | ||
+ | |||
+ | def draw(self, context): | ||
+ | layout = self.layout | ||
+ | layout.prop(self, | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # properties | ||
+ | # ======================================= | ||
+ | class VariationItem(PropertyGroup): | ||
+ | name: StringProperty(name=" | ||
+ | variation_type: | ||
+ | |||
+ | |||
+ | class VariationProps(PropertyGroup): | ||
+ | variation_type_enum_items = [ | ||
+ | (" | ||
+ | (" | ||
+ | (" | ||
+ | (" | ||
+ | (" | ||
+ | (" | ||
+ | ] | ||
+ | variation_type: | ||
+ | items=variation_type_enum_items, | ||
+ | name=" | ||
+ | description=" | ||
+ | default=" | ||
+ | ) | ||
+ | |||
+ | |||
+ | class PublishSetting(PropertyGroup): | ||
+ | publish_type_enum_items = [ | ||
+ | (" | ||
+ | (" | ||
+ | ] | ||
+ | publish_name: | ||
+ | name=" | ||
+ | ) | ||
+ | publish_version: | ||
+ | name=" | ||
+ | ) | ||
+ | publish_dir: | ||
+ | name=" | ||
+ | description=(" | ||
+ | subtype=" | ||
+ | ) | ||
+ | publish_type: | ||
+ | items=publish_type_enum_items, | ||
+ | name=" | ||
+ | description=" | ||
+ | default=" | ||
+ | ) | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # operators | ||
+ | # ======================================= | ||
+ | class AddCube(bpy.types.Operator): | ||
+ | bl_idname = " | ||
+ | bl_label = " | ||
+ | bl_options = {" | ||
+ | |||
+ | def execute(self, | ||
+ | bpy.ops.mesh.primitive_cube_add() | ||
+ | return {" | ||
+ | |||
+ | |||
+ | # example of AddObject operator class without UNIVERSAL_OT_AddObject naming pattern | ||
+ | class AddObject(Operator): | ||
+ | bl_idname = " | ||
+ | bl_label = "Add Object" | ||
+ | |||
+ | def execute(self, | ||
+ | obj = bpy.context.active_object | ||
+ | wm = context.window_manager | ||
+ | if obj: | ||
+ | item = wm.variation_list.add() | ||
+ | item.name = "," | ||
+ | item.variation_type = wm.variation_props.variation_type | ||
+ | return {" | ||
+ | |||
+ | |||
+ | class UNIVERSAL_OT_RemoveObject(Operator): | ||
+ | bl_idname = " | ||
+ | bl_label = " | ||
+ | bl_options = {" | ||
+ | |||
+ | @classmethod | ||
+ | def poll(cls, context): | ||
+ | wm = context.window_manager | ||
+ | return len(wm.variation_list) > 0 | ||
+ | |||
+ | def execute(self, | ||
+ | wm = context.window_manager | ||
+ | idx = wm.variation_list_index | ||
+ | wm.variation_list.remove(idx) | ||
+ | return {" | ||
+ | |||
+ | |||
+ | class PublishAsset(Operator): | ||
+ | bl_idname = " | ||
+ | bl_label = " | ||
+ | |||
+ | @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, | ||
+ | wm = context.window_manager | ||
+ | publish_prop = wm.publish_setting_props | ||
+ | print(publish_prop.publish_dir) | ||
+ | if publish_prop.publish_type != " | ||
+ | self.report({" | ||
+ | return {" | ||
+ | self.report({" | ||
+ | return {" | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # UI List | ||
+ | # ======================================= | ||
+ | class UNIVERSAL_UL_VariationList(UIList): | ||
+ | def draw_item( | ||
+ | self, context, layout, data, item, icon, active_data, | ||
+ | ): | ||
+ | custom_icon = " | ||
+ | if self.layout_type in {" | ||
+ | split = layout.split(factor=0.6) | ||
+ | split.label(text=item.name, | ||
+ | split.label(text=item.variation_type) | ||
+ | elif self.layout_type in {" | ||
+ | layout.alignment = " | ||
+ | layout.label(text=item.name, | ||
+ | layout.label(text=item.variation_type) | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # panels | ||
+ | # ======================================= | ||
+ | # UNIVERSAL_PT_VariationPanel: | ||
+ | # VariationPanel: | ||
+ | # Warning: ' | ||
+ | class UNIVERSAL_PT_VariationPanel(bpy.types.Panel): | ||
+ | bl_space_type = " | ||
+ | bl_region_type = " | ||
+ | bl_context = "" | ||
+ | bl_category = " | ||
+ | bl_label = " | ||
+ | |||
+ | def draw(self, context): | ||
+ | wm = context.window_manager | ||
+ | layout = self.layout | ||
+ | publish_prop = wm.publish_setting_props | ||
+ | |||
+ | layout.operator(" | ||
+ | layout.label(text=" | ||
+ | |||
+ | row = layout.row() | ||
+ | row.template_list( | ||
+ | listtype_name=" | ||
+ | list_id="", | ||
+ | dataptr=wm, | ||
+ | propname=" | ||
+ | active_dataptr=wm, | ||
+ | active_propname=" | ||
+ | ) | ||
+ | |||
+ | layout.prop(wm.variation_props, | ||
+ | row = layout.row() | ||
+ | row.operator(" | ||
+ | row.operator( | ||
+ | " | ||
+ | ) | ||
+ | |||
+ | layout.prop(publish_prop, | ||
+ | layout.prop(publish_prop, | ||
+ | layout.prop(publish_prop, | ||
+ | layout.prop(publish_prop, | ||
+ | asset_author_name = bpy.context.preferences.addons[ | ||
+ | " | ||
+ | ].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" | ||
+ | ) | ||
+ | layout.label(text=f" | ||
+ | else: | ||
+ | layout.label(text=" | ||
+ | layout.operator(" | ||
+ | |||
+ | |||
+ | # ======================================= | ||
+ | # 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__ == " | ||
+ | register() | ||
+ | |||
+ | </ | ||
+ |