From eff1d9efe2f1bb026b3e9e84f024cd1ceeab6703 Mon Sep 17 00:00:00 2001 From: Onan Chew Date: Sat, 31 Aug 2024 15:29:12 -0400 Subject: [PATCH] start of advanced mode almost done, just need to find out why doubles aren't merging even when perfectly together when using merge to unselected --- .vscode/settings.json | 4 +- functions/remove_doubles_safely.py | 187 +++++++++++++++++++++++++---- ui/tools.py | 2 +- 3 files changed, 170 insertions(+), 23 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e685f8d..2021d12 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,9 @@ "python.analysis.extraPaths": [ "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\scripts\\addons", "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.3\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons - "D:\\blender stuff\\blendercodestuff\\4.3" + "D:\\blender stuff\\blendercodestuff\\4.3", + "/Users/frankche/Documents/blendercoding/4.1/", + "/Users/frankche/Library/Application Support/Blender/4.3/extensions/user_default/" ], "python.analysis.diagnosticSeverityOverrides": { "reportInvalidTypeForm": "none" diff --git a/functions/remove_doubles_safely.py b/functions/remove_doubles_safely.py index 9c2078c..d00f71a 100644 --- a/functions/remove_doubles_safely.py +++ b/functions/remove_doubles_safely.py @@ -2,15 +2,18 @@ from ast import Dict from itertools import count import bpy import re -from typing import List, Tuple, Optional, TypedDict +from typing import List, Tuple, Optional, TypedDict, Any from bpy.types import Material, Operator, Context, Object from ..core.register import register_wrap from ..core.common import get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes from ..functions.translations import t class meshEntry(TypedDict): - mesh: bpy.types.Object + mesh: Object shapekeys: list[str] + vertices: int + cur_vertex_pass: int + mesh_shapekeys: dict[str, Object] @register_wrap class RemoveDoublesSafely(Operator): @@ -20,6 +23,7 @@ class RemoveDoublesSafely(Operator): bl_options = {'REGISTER', 'UNDO'} objects_to_do: list[meshEntry] = [] merge_distance: bpy.props.FloatProperty(default=0.0001) + advanced: bpy.props.BoolProperty(default=False) @classmethod def poll(cls, context: Context) -> bool: @@ -35,20 +39,49 @@ class RemoveDoublesSafely(Operator): bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') objects: List[Object] = get_all_meshes(context) + self.objects_to_do = [] for mesh in objects: if mesh.data.name not in [stored_object["mesh"].data.name for stored_object in self.objects_to_do]: - mesh_shapekeys = {"mesh":mesh,"shapekeys":[]} + print("setting up data for object" + mesh.name) + mesh_shapekeys = {"mesh":mesh,"shapekeys":[],"vertices":0,"cur_vertex_pass":0,"mesh_shapekeys":{}} mesh_data: bpy.types.Mesh = mesh.data shape: bpy.types.ShapeKey = None + mesh_shapekeys["vertices"] = len(mesh_data.vertices) + bpy.ops.object.mode_set(mode='EDIT') + mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices)) + bpy.ops.object.mode_set(mode='OBJECT') + if mesh_data.shape_keys: for shape in mesh_data.shape_keys.key_blocks: mesh_shapekeys["shapekeys"].append(shape.name) + if self.advanced: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + context.view_layer.objects.active = mesh + mesh.select_set(True) + mesh.active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shape.name) + bpy.ops.object.duplicate() + newobj = context.view_layer.objects.active + bpy.ops.object.shape_key_move(type='TOP') + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.shape_key_remove(all=True, apply_mix=False) + newobj.name = shape.name+"_object_is_"+mesh.name + mesh_shapekeys["mesh_shapekeys"][shape.name] = newobj + + context.view_layer.objects.active = mesh + bpy.ops.object.select_all(action='DESELECT') + print("queued data for "+mesh.name+" is: ") + print(mesh_shapekeys) self.objects_to_do.append(mesh_shapekeys) return {'FINISHED'} def invoke(self, context: Context, event: bpy.types.Event) -> set: + print("starting modal execution of merge doubles safely.") self.execute(context) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} @@ -56,7 +89,6 @@ class RemoveDoublesSafely(Operator): def modify_mesh(self, context: Context, mesh: meshEntry): mesh["mesh"].select_set(True) context.view_layer.objects.active = mesh["mesh"] - context.view_layer.objects.active = mesh["mesh"] mesh_data: bpy.types.Mesh = mesh["mesh"].data bpy.ops.object.mode_set(mode='EDIT') @@ -69,19 +101,120 @@ class RemoveDoublesSafely(Operator): bpy.ops.object.mode_set(mode='OBJECT') mesh["mesh"].select_set(False) + print("finished shapekey basic.") + + def modify_mesh_advanced(self, context: Context, mesh_entry: meshEntry): + + final_merged_vertex_group: list[int] = [] + + + for shapekey_name in mesh_entry["shapekeys"]: + mesh = mesh_entry["mesh_shapekeys"][shapekey_name] + + + + #make a copy to do double merge testing on for the current vertex + context.view_layer.objects.active = mesh + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + context.view_layer.objects.active = mesh + mesh_data: bpy.types.Mesh = mesh.data + vertices_original: dict[int,Any] = {} + original_count: int = len(mesh_data.vertices) + mesh.select_set(True) + bpy.ops.object.duplicate() + mesh = context.view_layer.objects.active + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + + mesh.select_set(True) + context.view_layer.objects.active = mesh + mesh_data: bpy.types.Mesh = mesh.data + bpy.ops.object.mode_set(mode='EDIT') + + + + bpy.ops.object.mode_set(mode='OBJECT') + for index, merged_point in enumerate(mesh_data.vertices): + vertices_original[index] = merged_point.co.xyz + + + #if point.co.xyz != original_mesh_data.shape_keys.key_blocks[0].points[index].co.xyz: + # mesh_data.vertices[index].select = True + # break + print("vertex indices and their positions.") + print(vertices_original) + print("vertex positions end.") + bpy.ops.object.mode_set(mode='EDIT') + mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices)) + bpy.ops.object.mode_set(mode='OBJECT') + + select_target_vertex = [False]*len(mesh_data.vertices) + try: + select_target_vertex[mesh_entry["cur_vertex_pass"]] = True + except: + bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless. + return True + print("vertex select list:") + print(select_target_vertex) + + + bpy.ops.object.mode_set(mode='EDIT') + mesh_data.vertices.foreach_set("select",select_target_vertex) + bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=True, use_sharp_edge_from_normals=False) + bpy.ops.object.mode_set(mode='OBJECT') + #this doesn't keep in mind vertices that are exactly in the same place in the shapekey positional wise that also appear right after one another in the array + #Based on my internal thoughts and theories, this will cause problems. I don't know the solution to this, so it's fine for now. + # besides, the chance of this happening should be very very slim, and will require user input to fix. - @989onan + # { + # "1":"0,0,0" + # "2":"0,0,0" + # "3":"1,0,0" + # } + + merged_vertices: list[int] = [] + + for i in range(0,original_count): + if mesh_data.vertices[i+len(merged_vertices)].co.xyz != vertices_original[i]: + merged_vertices.append(i) + + #iterate through a copy of final vertex groups to prevent crash. If a vertex was merged before, but didn't merge in this vertex, + # then the vertex shouldn't be merged because it moves away from the vertex we are double merging now (ex: bottom of mouth moving away from top when opening on a shapekey) - @989onan + for merged_point in final_merged_vertex_group[:]: + if merged_point not in merged_vertices: + final_merged_vertex_group.remove(merged_point) + else: + final_merged_vertex_group.append(merged_point) + + bpy.ops.object.mode_set(mode='EDIT') + mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices)) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless. + context.view_layer.objects.active = mesh_entry["mesh"] + bpy.ops.object.mode_set(mode='EDIT') + original_mesh_data: bpy.types.Mesh = mesh_entry["mesh"].data + select_target_group = [False]*len(mesh_data.vertices) + for vertex_index in final_merged_vertex_group: + select_target_group[vertex_index] = True + original_mesh_data.vertices.foreach_set("select",select_target_group) + bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=False, use_sharp_edge_from_normals=False) + mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices)) + bpy.ops.object.mode_set(mode='OBJECT') + print("finished shapekey advanced.") + return not (len(final_merged_vertex_group) > 0) def modal(self, context: Context, event: bpy.types.Event) -> set: if len(self.objects_to_do) > 0: - mesh = self.objects_to_do[0] + bpy.ops.object.select_all(action='DESELECT') + mesh: meshEntry = self.objects_to_do[0] mesh_data: bpy.types.Mesh = mesh["mesh"].data - if len(mesh['shapekeys']) > 0: + if (len(mesh['shapekeys']) > 0) and (not self.advanced): shapekeyname: str = mesh['shapekeys'].pop(0) target_shapekey: int = mesh_data.shape_keys.key_blocks.find(shapekeyname) mesh["mesh"].active_shape_key_index = target_shapekey print("doing shapekey \""+shapekeyname+"\" on mesh \""+mesh['mesh'].name+"\".") self.modify_mesh(context, mesh) - elif not (mesh_data.shape_keys): print("doing mesh with no shapekeys named \""+mesh['mesh'].name+"\".") mesh["mesh"].select_set(True) @@ -95,29 +228,41 @@ class RemoveDoublesSafely(Operator): bpy.ops.object.mode_set(mode='OBJECT') mesh["mesh"].select_set(False) self.objects_to_do.pop(0) + elif (not (mesh["cur_vertex_pass"] > mesh["vertices"])) and self.advanced: + + print("doing a merge by single vertex index at index "+str(mesh["cur_vertex_pass"])) + + if self.modify_mesh_advanced(context, mesh): + mesh["cur_vertex_pass"] = mesh["cur_vertex_pass"]+1 else: - mesh["mesh"].select_set(True) - context.view_layer.objects.active = mesh["mesh"] - bpy.ops.object.mode_set(mode='EDIT') - - bpy.ops.mesh.select_all(action="INVERT") - bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False) - - bpy.ops.object.mode_set(mode='OBJECT') - mesh["mesh"].select_set(False) - - self.objects_to_do.pop(0) - if len(self.objects_to_do) > 0: - mesh = self.objects_to_do[0] + print("finishing double merge object.") + if not self.advanced: mesh["mesh"].select_set(True) context.view_layer.objects.active = mesh["mesh"] bpy.ops.object.mode_set(mode='EDIT') - mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices)) + + bpy.ops.mesh.select_all(action="INVERT") + bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False) + bpy.ops.object.mode_set(mode='OBJECT') mesh["mesh"].select_set(False) + else: + mesh["mesh"].select_set(True) + context.view_layer.objects.active = mesh["mesh"] + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for obj in mesh["mesh_shapekeys"].values(): + obj.select_set(True) + bpy.ops.object.delete() #delete all objects that were shapekey types. + + self.objects_to_do.pop(0) + + + else: self.report({'INFO'}, t("Optimization.remove_doubles_completed")) + print("finishing modal execution of merge doubles safely.") return {'FINISHED'} return {'RUNNING_MODAL'} diff --git a/ui/tools.py b/ui/tools.py index 7d8dc73..62dde6c 100644 --- a/ui/tools.py +++ b/ui/tools.py @@ -33,7 +33,7 @@ class AvatarToolkitToolsPanel(bpy.types.Panel): row.operator(CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade_legs.label"), icon='BONE_DATA') layout.separator() row = layout.row(align=True) - layout.label(text=t("Tools.separate_by.label"), icon='MESH') + layout.label(text=t("Tools.separate_by.label"), icon='MESH_DATA') row.operator(SeparateByMaterials.bl_idname, text=t("Tools.separate_by_materials.label"), icon='MATERIAL') row.operator(SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH') row = layout.row(align=True)