diff --git a/ballance_blender_plugin/BMFILE_import.py b/ballance_blender_plugin/BMFILE_import.py index 69b66f6..a5e8da2 100644 --- a/ballance_blender_plugin/BMFILE_import.py +++ b/ballance_blender_plugin/BMFILE_import.py @@ -332,9 +332,9 @@ def import_bm(context, bmx_filepath, prefs_fncg, prefs_externalTexture, prefs_te # write custom property if len(object_groupList) != 0: - UTILS_virtools_prop.set_virtools_group_data(object_target, tuple(object_groupList)) + UTILS_virtools_prop.fill_virtools_group_data(object_target, tuple(object_groupList)) else: - UTILS_virtools_prop.set_virtools_group_data(object_target, None) + UTILS_virtools_prop.fill_virtools_group_data(object_target, None) # update view layer after all objects has been imported blender_viewLayer.update() diff --git a/ballance_blender_plugin/MODS_3dsmax_align.py b/ballance_blender_plugin/MODS_3dsmax_align.py index adf351d..30ab3c9 100644 --- a/ballance_blender_plugin/MODS_3dsmax_align.py +++ b/ballance_blender_plugin/MODS_3dsmax_align.py @@ -2,7 +2,7 @@ import bpy, mathutils from . import UTILS_functions class BALLANCE_OT_super_align(bpy.types.Operator): - """Align object with 3ds Max way""" + """Align object with 3ds Max style""" bl_idname = "ballance.super_align" bl_label = "3ds Max Align" bl_options = {'UNDO'} @@ -12,7 +12,7 @@ class BALLANCE_OT_super_align(bpy.types.Operator): align_z: bpy.props.BoolProperty(name="Z position") current_references: bpy.props.EnumProperty( - name="Reference", + name="Reference (Active Object)", items=(('MIN', "Min", ""), ('CENTER', "Center (bound box)", ""), ('POINT', "Center (axis)", ""), @@ -21,7 +21,7 @@ class BALLANCE_OT_super_align(bpy.types.Operator): ) target_references: bpy.props.EnumProperty( - name="Target", + name="Target (Other Objects)", items=(('MIN', "Min", ""), ('CENTER', "Center (bound box)", ""), ('POINT', "Center (axis)", ""), diff --git a/ballance_blender_plugin/NAMES_rename_system.py b/ballance_blender_plugin/NAMES_rename_system.py index 1949754..e9b95b3 100644 --- a/ballance_blender_plugin/NAMES_rename_system.py +++ b/ballance_blender_plugin/NAMES_rename_system.py @@ -436,7 +436,7 @@ def _set_for_group(obj, name_info): # apply to custom property - UTILS_virtools_prop.set_virtools_group_data(obj, tuple(gps)) + UTILS_virtools_prop.fill_virtools_group_data(obj, tuple(gps)) # ========================================== # assemble funcs diff --git a/ballance_blender_plugin/OBJS_group_opers.py b/ballance_blender_plugin/OBJS_group_opers.py new file mode 100644 index 0000000..dfd7967 --- /dev/null +++ b/ballance_blender_plugin/OBJS_group_opers.py @@ -0,0 +1,221 @@ +import bpy +from . import UTILS_constants, UTILS_functions, UTILS_virtools_prop + +class common_group_name_props(bpy.types.Operator): + use_custom_name: bpy.props.BoolProperty( + name="Use Custom Name", + description="Whether use user defined group name.", + default=False, + ) + + group_name: bpy.props.EnumProperty( + name="Group Name", + description="Pick vanilla Ballance group name.", + items=tuple((x, x, "") for x in UTILS_constants.propsVtGroups_availableGroups), + ) + + custom_group_name: bpy.props.StringProperty( + name="Custom Group Name", + description="Input your custom group name.", + default="", + ) + + def get_group_name_string(self): + return str(self.custom_group_name if self.use_custom_name else self.group_name) + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + +class BALLANCE_OT_select_virtools_group(common_group_name_props): + """Select objects by Virtools Group.""" + bl_idname = "ballance.select_virtools_group" + bl_label = "Select by Virtools Group" + bl_options = {'UNDO'} + + merge_selection: bpy.props.BoolProperty( + name="Merge Selection", + description="Merge selection, rather than re-select them.", + default=False, + ) + + ignore_hide: bpy.props.BoolProperty( + name="Ignore Hide Property", + description="Select objects without considering visibility.", + default=False, + ) + + def execute(self, context): + # iterate object + for obj in bpy.context.scene.objects: + # ignore hidden objects + if (not self.ignore_hide) and obj.hide_get() == True: + continue + + # check group + if UTILS_virtools_prop.check_virtools_group_data(obj, self.get_group_name_string()): + # select object + obj.select_set(True) + else: + # if not in merge mode, deselect them + if not self.merge_selection: + obj.select_set(False) + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + + row = layout.row() + row.prop(self, 'ignore_hide') + row.prop(self, 'merge_selection') + + layout.separator() + layout.prop(self, 'use_custom_name') + if (self.use_custom_name): + layout.prop(self, 'custom_group_name') + else: + layout.prop(self, 'group_name') + +class BALLANCE_OT_filter_virtools_group(common_group_name_props): + """Filter objects by Virtools Group.""" + bl_idname = "ballance.filter_virtools_group" + bl_label = "Filter by Virtools Group" + bl_options = {'UNDO'} + + reverse_selection: bpy.props.BoolProperty( + name="Reverse", + description="Reverse operation. Remove matched objects.", + default=False, + ) + + ignore_hide: bpy.props.BoolProperty( + name="Ignore Hide Property", + description="Select objects without considering visibility.", + default=False, + ) + + def execute(self, context): + # make a copy for all objects, to ensure it is not viotile + # becuase we need deselect some objects in for statement + selected = bpy.context.selected_objects[:] + # iterate object + for obj in selected: + # ignore hidden objects + if (not self.ignore_hide) and obj.hide_get() == True: + continue + + # check group and decide select + is_selected = UTILS_virtools_prop.check_virtools_group_data(obj, self.get_group_name_string()) + if self.reverse_selection: + is_selected = not is_selected + + # select object + obj.select_set(is_selected) + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + + row = layout.row() + row.prop(self, 'ignore_hide') + row.prop(self, 'reverse_selection') + + layout.separator() + layout.prop(self, 'use_custom_name') + if (self.use_custom_name): + layout.prop(self, 'custom_group_name') + else: + layout.prop(self, 'group_name') + + + +class BALLANCE_OT_ctx_set_group(common_group_name_props): + """Grouping selected objects""" + bl_idname = "ballance.ctx_set_group" + bl_label = "Grouping Objects" + bl_options = {'UNDO'} + + @classmethod + def poll(self, context): + return len(bpy.context.selected_objects) != 0 + + def execute(self, context): + has_duplicated = False + + # iterate object + for obj in bpy.context.selected_objects: + # try setting + if not UTILS_virtools_prop.add_virtools_group_data(obj, self.get_group_name_string()): + has_duplicated = True + + # throw a warning if some objects have duplicated group + if has_duplicated: + UTILS_functions.show_message_box(("Some objects have duplicated group name.", "These objects have been omitted.", ), "Duplicated Group", 'ERROR') + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, 'use_custom_name') + if (self.use_custom_name): + layout.prop(self, 'custom_group_name') + else: + layout.prop(self, 'group_name') + +class BALLANCE_OT_ctx_unset_group(common_group_name_props): + """Ungrouping selected objects""" + bl_idname = "ballance.ctx_unset_group" + bl_label = "Ungrouping Objects" + bl_options = {'UNDO'} + + @classmethod + def poll(self, context): + return len(bpy.context.selected_objects) != 0 + + def execute(self, context): + lack_group = False + + # iterate object + for obj in bpy.context.selected_objects: + # try unsetting + if not UTILS_virtools_prop.remove_virtools_group_data(obj, self.get_group_name_string()): + lack_group = True + + # throw a warning if some objects have duplicated group + if lack_group: + UTILS_functions.show_message_box(("Some objects lack specified group name.", "These objects have been omitted.", ), "Lack Group", 'ERROR') + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.prop(self, 'use_custom_name') + if (self.use_custom_name): + layout.prop(self, 'custom_group_name') + else: + layout.prop(self, 'group_name') + +class BALLANCE_OT_ctx_clear_group(bpy.types.Operator): + """Clear Virtools Groups for selected objects""" + bl_idname = "ballance.ctx_clear_group" + bl_label = "Clear Grouping" + bl_options = {'UNDO'} + + @classmethod + def poll(self, context): + return len(bpy.context.selected_objects) != 0 + + def execute(self, context): + # iterate object + for obj in bpy.context.selected_objects: + UTILS_virtools_prop.clear_virtools_group_data(obj) + + + return {'FINISHED'} + diff --git a/ballance_blender_plugin/PROPS_virtools_group.py b/ballance_blender_plugin/PROPS_virtools_group.py index 64279e2..b22fc59 100644 --- a/ballance_blender_plugin/PROPS_virtools_group.py +++ b/ballance_blender_plugin/PROPS_virtools_group.py @@ -7,31 +7,50 @@ class BALLANCE_OT_add_virtools_group(bpy.types.Operator): bl_label = "Add Virtools Group" bl_options = {'UNDO'} + use_custom_name: bpy.props.BoolProperty( + name="Use Custom Name", + description="Whether use user defined group name.", + default=False, + ) + group_name: bpy.props.EnumProperty( name="Group Name", - description="Group name. For custom group name, please pick `CustomCKGroup` and change it later.", + description="Pick vanilla Ballance group name.", items=tuple((x, x, "") for x in UTILS_constants.propsVtGroups_availableGroups), ) + custom_group_name: bpy.props.StringProperty( + name="Custom Group Name", + description="Input your custom group name.", + default="", + ) + @classmethod def poll(self, context): return context.object is not None def execute(self, context): + # get name first + gotten_group_name = str(self.custom_group_name if self.use_custom_name else self.group_name) + + # try adding obj = context.object - gp = UTILS_virtools_prop.get_virtools_group(obj) - item = gp.add() - item.name = "" - item.group_name = str(self.group_name) + if not UTILS_virtools_prop.add_virtools_group_data(obj, gotten_group_name): + UTILS_functions.show_message_box(("Group name is duplicated!", ), "Duplicated Name", 'ERROR') return {'FINISHED'} - + def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self) - + def draw(self, context): - self.layout.prop(self, 'group_name') + self.layout.prop(self, 'use_custom_name') + if (self.use_custom_name): + self.layout.prop(self, 'custom_group_name') + else: + self.layout.prop(self, 'group_name') + class BALLANCE_OT_rm_virtools_group(bpy.types.Operator): """Remove a Virtools Group for Active Object.""" @@ -44,24 +63,14 @@ class BALLANCE_OT_rm_virtools_group(bpy.types.Operator): if context.object is None: return False - try: - obj = context.object - gp = UTILS_virtools_prop.get_virtools_group(obj) - active_gp = UTILS_virtools_prop.get_active_virtools_group(obj) - data = gp[active_gp] - except: - return False - else: - return True - - def execute(self, context): obj = context.object gp = UTILS_virtools_prop.get_virtools_group(obj) active_gp = UTILS_virtools_prop.get_active_virtools_group(obj) - idx = int(active_gp) - - active_gp -= 1 - gp.remove(idx) + return int(active_gp) >= 0 and int(active_gp) < len(gp) + + def execute(self, context): + obj = context.object + UTILS_virtools_prop.remove_virtools_group_data_by_index(obj, int(UTILS_virtools_prop.get_active_virtools_group(obj))) return {'FINISHED'} class BALLANCE_UL_virtools_group(bpy.types.UIList): diff --git a/ballance_blender_plugin/UTILS_constants.py b/ballance_blender_plugin/UTILS_constants.py index de409b4..f7a736c 100644 --- a/ballance_blender_plugin/UTILS_constants.py +++ b/ballance_blender_plugin/UTILS_constants.py @@ -345,9 +345,7 @@ propsVtGroups_availableGroups = ( "Phys_Floors", "Phys_FloorRails", - "Phys_FloorStopper", - - "CustomCKGroup" + "Phys_FloorStopper" ) diff --git a/ballance_blender_plugin/UTILS_virtools_prop.py b/ballance_blender_plugin/UTILS_virtools_prop.py index 3406a98..21d31e6 100644 --- a/ballance_blender_plugin/UTILS_virtools_prop.py +++ b/ballance_blender_plugin/UTILS_virtools_prop.py @@ -98,19 +98,82 @@ def get_active_virtools_group(obj): def get_virtools_group(obj): return obj.virtools_group +def check_virtools_group_data(obj, probe): + for item in get_virtools_group(obj): + if probe == str(item.group_name): + return True + + return False + +def add_virtools_group_data(obj, new_data): + # check exist + if check_virtools_group_data(obj, new_data): + # existed, give up + return False + + # "add" do not need operate active_virtools_group + data = get_virtools_group(obj) + it = data.add() + it.name = "" + it.group_name = new_data + + return True + +def remove_virtools_group_data(obj, rm_data): + gp = get_virtools_group(obj) + active_gp = get_active_virtools_group(obj) + + for idx, item in enumerate(gp): + if rm_data == str(item.group_name): + # decrease active group if removed item is ahead of active group + if idx <= active_gp: + active_gp -= 1 + # remove + gp.remove(idx) + # indicate success + return True + + return False + +def remove_virtools_group_data_by_index(obj, rm_idx): + gp = get_virtools_group(obj) + active_gp = get_active_virtools_group(obj) + + # report error + if rm_idx >= len(gp): + return False + + # remove + if rm_idx <= active_gp: + active_gp -= 1 + gp.remove(rm_idx) + return True + +def clear_virtools_group_data(obj): + gp = get_virtools_group(obj) + active_gp = get_active_virtools_group(obj) + + gp.clear() + active_gp = 0 + +def fill_virtools_group_data(obj, data_list): + # clear first + clear_virtools_group_data(obj) + + # if no data to add, return + if data_list is None: + return + + # add one by one after check duplication + data = get_virtools_group(obj) + for item in set(data_list): + it = data.add() + it.name = "" + it.group_name = item + def get_virtools_group_data(obj): return tuple(str(item.group_name) for item in get_virtools_group(obj)) -def set_virtools_group_data(obj, new_data): - data = get_virtools_group(obj) - data.clear() - - if new_data is not None: - for item in new_data: - it = data.add() - it.name = "" - it.group_name = item - def register_props(): bpy.types.Object.virtools_group = bpy.props.CollectionProperty(type=BALLANCE_PG_virtools_group) bpy.types.Object.active_virtools_group = bpy.props.IntProperty() diff --git a/ballance_blender_plugin/__init__.py b/ballance_blender_plugin/__init__.py index 8be5efa..9cd7ce1 100644 --- a/ballance_blender_plugin/__init__.py +++ b/ballance_blender_plugin/__init__.py @@ -50,6 +50,8 @@ if "bpy" in locals(): importlib.reload(OBJS_add_floors) if "OBJS_add_rails" in locals(): importlib.reload(OBJS_add_rails) + if "OBJS_group_opers" in locals(): + importlib.reload(OBJS_group_opers) if "NAMES_rename_system" in locals(): importlib.reload(NAMES_rename_system) @@ -62,7 +64,7 @@ if "bpy" in locals(): from . import UTILS_constants, UTILS_functions, UTILS_preferences, UTILS_virtools_prop from . import BMFILE_export, BMFILE_import from . import MODS_3dsmax_align, MODS_flatten_uv, MODS_rail_uv -from . import OBJS_add_components, OBJS_add_floors, OBJS_add_rails +from . import OBJS_add_components, OBJS_add_floors, OBJS_add_rails, OBJS_group_opers from . import NAMES_rename_system from . import PROPS_virtools_group, PROPS_virtools_material @@ -149,8 +151,14 @@ classes = ( PROPS_virtools_group.BALLANCE_UL_virtools_group, PROPS_virtools_group.BALLANCE_PT_virtools_group, PROPS_virtools_material.BALLANCE_OT_apply_virtools_material, - PROPS_virtools_material.BALLANCE_PT_virtools_material + PROPS_virtools_material.BALLANCE_PT_virtools_material, + OBJS_group_opers.BALLANCE_OT_select_virtools_group, + OBJS_group_opers.BALLANCE_OT_filter_virtools_group, + OBJS_group_opers.BALLANCE_OT_ctx_set_group, + OBJS_group_opers.BALLANCE_OT_ctx_unset_group, + OBJS_group_opers.BALLANCE_OT_ctx_clear_group, + ) def menu_func_bm_import(self, context): @@ -172,6 +180,21 @@ def menu_func_ballance_add(self, context): def menu_func_ballance_rename(self, context): layout = self.layout layout.menu(BALLANCE_MT_OutlinerMenu.bl_idname) +def menu_func_ballance_select(self, context): + layout = self.layout + layout.separator() + layout.label(text="Ballance") + layout.operator(OBJS_group_opers.BALLANCE_OT_select_virtools_group.bl_idname, icon='SELECT_SET') + layout.operator(OBJS_group_opers.BALLANCE_OT_filter_virtools_group.bl_idname, icon='FILTER') +def menu_func_ballance_grouping(self, context): + layout = self.layout + layout.separator() + col = layout.column() + col.operator_context = 'INVOKE_DEFAULT' + col.label(text="Ballance") + col.operator(OBJS_group_opers.BALLANCE_OT_ctx_set_group.bl_idname, icon='ADD', text="Group into...") + col.operator(OBJS_group_opers.BALLANCE_OT_ctx_unset_group.bl_idname, icon='REMOVE', text="Ungroup from...") + col.operator(OBJS_group_opers.BALLANCE_OT_ctx_clear_group.bl_idname, icon='TRASH', text="Clear Grouping") def register(): @@ -196,6 +219,9 @@ def register(): bpy.types.VIEW3D_MT_add.append(menu_func_ballance_add) bpy.types.OUTLINER_HT_header.append(menu_func_ballance_rename) + bpy.types.VIEW3D_MT_select_object.append(menu_func_ballance_select) + bpy.types.VIEW3D_MT_object_context_menu.append(menu_func_ballance_grouping) + def unregister(): bpy.types.TOPBAR_MT_file_import.remove(menu_func_bm_import) bpy.types.TOPBAR_MT_file_export.remove(menu_func_bm_export) @@ -204,6 +230,9 @@ def unregister(): bpy.types.VIEW3D_MT_add.remove(menu_func_ballance_add) bpy.types.OUTLINER_HT_header.remove(menu_func_ballance_rename) + bpy.types.VIEW3D_MT_select_object.remove(menu_func_ballance_select) + bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func_ballance_grouping) + UTILS_virtools_prop.unregister_props() del bpy.types.Scene.BallanceBlenderPluginProperty