summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--BakeVertexData.py533
1 files changed, 529 insertions, 4 deletions
diff --git a/BakeVertexData.py b/BakeVertexData.py
index a7d462a..8724967 100644
--- a/BakeVertexData.py
+++ b/BakeVertexData.py
@@ -10,6 +10,7 @@ bl_info = {
import bpy
import mathutils
import bmesh
+import math
from bpy.props import BoolProperty, FloatProperty
from bpy.types import Panel, Operator
@@ -140,6 +141,15 @@ class MESH_OT_bake_vertex_vectors(Operator):
source_matrix = obj.matrix_world
if self.contiguous_mode:
+ # Build vertex to polygon mapping for efficiency
+ vertex_to_polys = {}
+ for poly_idx, poly in enumerate(mesh.polygons):
+ for vertex_idx in poly.vertices:
+ if vertex_idx in selected_indices:
+ if vertex_idx not in vertex_to_polys:
+ vertex_to_polys[vertex_idx] = []
+ vertex_to_polys[vertex_idx].append(poly_idx)
+
islands = self.get_vertex_islands(mesh, selected_indices)
total_updated = 0
@@ -150,7 +160,15 @@ class MESH_OT_bake_vertex_vectors(Operator):
center_world = source_matrix @ center
- for poly in mesh.polygons:
+ # Collect all polygons that contain vertices from this island
+ relevant_polys = set()
+ for vertex_idx in island_indices:
+ if vertex_idx in vertex_to_polys:
+ relevant_polys.update(vertex_to_polys[vertex_idx])
+
+ # Only process relevant polygons
+ for poly_idx in relevant_polys:
+ poly = mesh.polygons[poly_idx]
for loop_idx in poly.loop_indices:
vertex_idx = mesh.loops[loop_idx].vertex_index
@@ -241,9 +259,6 @@ class MESH_OT_bake_vertex_vectors(Operator):
def draw(self, context):
layout = self.layout
layout.prop(self, "contiguous_mode")
-
- def invoke(self, context, event):
- return context.window_manager.invoke_props_dialog(self)
class MESH_OT_select_all_linked_submeshes(Operator):
@@ -343,6 +358,504 @@ class MESH_OT_select_all_linked_submeshes(Operator):
return {'FINISHED'}
+class MESH_OT_select_linked_across_boundaries(Operator):
+ bl_idname = "mesh.select_linked_across_boundaries"
+ bl_label = "Select Linked (Cross Boundaries)"
+ bl_description = "Select linked vertices, crossing submesh boundaries where vertices share locations"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ epsilon: FloatProperty(
+ name="Location Tolerance",
+ description="Maximum distance for vertices to be considered at the same location",
+ default=0.0001,
+ min=0.0,
+ max=1.0,
+ precision=6,
+ subtype='DISTANCE'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj is not None and
+ obj.type == 'MESH' and
+ context.mode == 'EDIT_MESH')
+
+ def build_position_map(self, mesh, epsilon):
+ """Build a map of vertices that share the same position within epsilon"""
+ # Use integer hashing for much faster performance
+ if epsilon > 0:
+ scale = min(1.0 / epsilon, 1e7) # Cap scale to prevent overflow
+ else:
+ scale = 1e7 # Large scale for exact matching
+
+ position_map = {}
+
+ # Group vertices by their quantized positions
+ for v in mesh.vertices:
+ # Quantize to integer grid
+ key = (
+ int(v.co.x * scale),
+ int(v.co.y * scale),
+ int(v.co.z * scale)
+ )
+
+ if key not in position_map:
+ position_map[key] = []
+ position_map[key].append(v.index)
+
+ # Create adjacency only for vertices we'll actually use
+ # This avoids creating empty sets for all vertices
+ position_adjacency = {}
+
+ for vertices_at_pos in position_map.values():
+ if len(vertices_at_pos) > 1:
+ # For small groups, connect all to all
+ if len(vertices_at_pos) <= 10:
+ for i in range(len(vertices_at_pos)):
+ v1 = vertices_at_pos[i]
+ if v1 not in position_adjacency:
+ position_adjacency[v1] = set()
+ for j in range(i + 1, len(vertices_at_pos)):
+ v2 = vertices_at_pos[j]
+ if v2 not in position_adjacency:
+ position_adjacency[v2] = set()
+ position_adjacency[v1].add(v2)
+ position_adjacency[v2].add(v1)
+ else:
+ # For large groups, create a hub vertex to avoid O(n²) connections
+ hub = vertices_at_pos[0]
+ if hub not in position_adjacency:
+ position_adjacency[hub] = set()
+ for i in range(1, len(vertices_at_pos)):
+ v = vertices_at_pos[i]
+ if v not in position_adjacency:
+ position_adjacency[v] = set()
+ position_adjacency[hub].add(v)
+ position_adjacency[v].add(hub)
+
+ return position_adjacency
+
+ def execute(self, context):
+ obj = context.active_object
+ mesh = obj.data
+
+ # Switch to object mode
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ # Get initially selected vertices
+ initially_selected = {v.index for v in mesh.vertices if v.select}
+
+ if not initially_selected:
+ self.report({'WARNING'}, "No vertices selected")
+ bpy.ops.object.mode_set(mode='EDIT')
+ return {'CANCELLED'}
+
+ # Build edge adjacency only for vertices we might visit
+ edge_adjacency = {}
+ for edge in mesh.edges:
+ v0, v1 = edge.vertices
+ if v0 not in edge_adjacency:
+ edge_adjacency[v0] = set()
+ if v1 not in edge_adjacency:
+ edge_adjacency[v1] = set()
+ edge_adjacency[v0].add(v1)
+ edge_adjacency[v1].add(v0)
+
+ # Build position adjacency
+ position_adjacency = self.build_position_map(mesh, self.epsilon)
+
+ # Function to get combined neighbors efficiently
+ def get_neighbors(vertex_idx):
+ neighbors = set()
+ if vertex_idx in edge_adjacency:
+ neighbors.update(edge_adjacency[vertex_idx])
+ if vertex_idx in position_adjacency:
+ neighbors.update(position_adjacency[vertex_idx])
+ return neighbors
+
+ # Flood fill from selected vertices using deque for better performance
+ from collections import deque
+ visited = set()
+ queue = deque(initially_selected)
+
+ while queue:
+ current = queue.popleft()
+ if current in visited:
+ continue
+
+ visited.add(current)
+ mesh.vertices[current].select = True
+
+ # Add all connected vertices to queue
+ for neighbor in get_neighbors(current):
+ if neighbor not in visited:
+ queue.append(neighbor)
+
+ # Select edges where both vertices are selected
+ for edge in mesh.edges:
+ v0, v1 = edge.vertices
+ if mesh.vertices[v0].select and mesh.vertices[v1].select:
+ edge.select = True
+
+ # Select faces where all vertices are selected
+ for face in mesh.polygons:
+ all_verts_selected = all(mesh.vertices[v].select for v in face.vertices)
+ if all_verts_selected:
+ face.select = True
+
+ # Return to edit mode
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ expanded_count = len(visited) - len(initially_selected)
+ self.report({'INFO'}, f"Selected {len(visited)} vertices ({expanded_count} new)")
+ return {'FINISHED'}
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "epsilon")
+ layout.label(text="Connects vertices at same location", icon='INFO')
+
+
+class MESH_OT_deduplicate_submeshes(Operator):
+ bl_idname = "mesh.deduplicate_submeshes"
+ bl_label = "Deduplicate Submeshes"
+ bl_description = "Remove duplicate submeshes from selection that have vertices at the same locations"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ tolerance: FloatProperty(
+ name="Position Tolerance",
+ description="Maximum distance for vertices to be considered at the same position",
+ default=0.0001,
+ min=0.0,
+ max=1.0,
+ precision=6
+ )
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj is not None and
+ obj.type == 'MESH' and
+ context.mode == 'EDIT_MESH')
+
+ def get_selected_vertex_islands(self, mesh):
+ """Find all contiguous groups of selected vertices in the mesh"""
+ # Get selected vertices
+ selected_indices = {v.index for v in mesh.vertices if v.select}
+
+ if not selected_indices:
+ return []
+
+ # Build adjacency only for selected vertices
+ adjacency = {idx: set() for idx in selected_indices}
+
+ for edge in mesh.edges:
+ v0, v1 = edge.vertices
+ if v0 in selected_indices and v1 in selected_indices:
+ adjacency[v0].add(v1)
+ adjacency[v1].add(v0)
+
+ islands = []
+ visited = set()
+
+ for start_idx in selected_indices:
+ if start_idx in visited:
+ continue
+
+ island = set()
+ queue = [start_idx]
+
+ while queue:
+ current = queue.pop(0)
+ if current in visited:
+ continue
+
+ visited.add(current)
+ island.add(current)
+
+ for neighbor in adjacency[current]:
+ if neighbor not in visited:
+ queue.append(neighbor)
+
+ islands.append(island)
+
+ return islands
+
+ def get_island_hash(self, mesh, island_indices):
+ """Create a hash for an island based on vertex positions"""
+ # Round positions to handle tolerance
+ decimal_places = 6 if self.tolerance == 0 else max(0, int(-math.log10(self.tolerance)))
+
+ positions = []
+ for idx in island_indices:
+ co = mesh.vertices[idx].co
+ # Round each component
+ rounded = (
+ round(co.x, decimal_places),
+ round(co.y, decimal_places),
+ round(co.z, decimal_places)
+ )
+ positions.append(rounded)
+
+ # Sort positions to ensure consistent ordering
+ positions.sort()
+
+ # Convert to tuple for hashing
+ return tuple(positions)
+
+ def execute(self, context):
+ obj = context.active_object
+ mesh = obj.data
+
+ # Switch to object mode
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ # Find all selected islands
+ islands = self.get_selected_vertex_islands(mesh)
+
+ if not islands:
+ self.report({'WARNING'}, "No vertices selected")
+ bpy.ops.object.mode_set(mode='EDIT')
+ return {'CANCELLED'}
+
+ if len(islands) <= 1:
+ self.report({'INFO'}, "No duplicate submeshes found in selection (only 1 submesh)")
+ bpy.ops.object.mode_set(mode='EDIT')
+ return {'FINISHED'}
+
+ # Group islands by their hash
+ island_groups = {}
+ for island in islands:
+ island_hash = self.get_island_hash(mesh, island)
+ if island_hash not in island_groups:
+ island_groups[island_hash] = []
+ island_groups[island_hash].append(island)
+
+ # Find duplicates
+ duplicates_to_remove = []
+ duplicate_count = 0
+
+ for hash_key, group in island_groups.items():
+ if len(group) > 1:
+ # Keep the first island, mark others for removal
+ for island in group[1:]:
+ duplicates_to_remove.append(island)
+ duplicate_count += 1
+
+ if not duplicates_to_remove:
+ self.report({'INFO'}, "No duplicate submeshes found in selection")
+ bpy.ops.object.mode_set(mode='EDIT')
+ return {'FINISHED'}
+
+ # Enter edit mode and select vertices to delete
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ # Select all vertices in duplicate islands
+ vertices_to_delete = set()
+ for island in duplicates_to_remove:
+ vertices_to_delete.update(island)
+
+ for idx in vertices_to_delete:
+ mesh.vertices[idx].select = True
+
+ # Delete selected vertices
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.delete(type='VERT')
+
+ self.report({'INFO'}, f"Removed {duplicate_count} duplicate submeshes from selection ({len(vertices_to_delete)} vertices)")
+ return {'FINISHED'}
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "tolerance")
+ layout.label(text="Set to 0 for exact matching", icon='INFO')
+
+
+class MESH_OT_merge_by_distance_in_submeshes(Operator):
+ bl_idname = "mesh.merge_by_distance_in_submeshes"
+ bl_label = "Merge by Distance (Per Submesh)"
+ bl_description = "Merge vertices by distance within each submesh separately"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ merge_distance: FloatProperty(
+ name="Merge Distance",
+ description="Maximum distance for merging vertices",
+ default=0.001,
+ min=0.0,
+ max=1.0,
+ precision=6,
+ subtype='DISTANCE'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj is not None and
+ obj.type == 'MESH' and
+ context.mode == 'EDIT_MESH')
+
+ def get_selected_vertex_islands(self, mesh):
+ """Find all contiguous groups of selected vertices - works like other operators"""
+ # Get selected vertices
+ selected_indices = {v.index for v in mesh.vertices if v.select}
+
+ if not selected_indices:
+ return []
+
+ # Build adjacency only for selected vertices
+ adjacency = {idx: set() for idx in selected_indices}
+
+ # Only check edges that might connect selected vertices
+ for edge in mesh.edges:
+ v0, v1 = edge.vertices
+ if v0 in selected_indices and v1 in selected_indices:
+ adjacency[v0].add(v1)
+ adjacency[v1].add(v0)
+
+ islands = []
+ visited = set()
+
+ from collections import deque
+
+ for start_idx in selected_indices:
+ if start_idx in visited:
+ continue
+
+ island = set()
+ queue = deque([start_idx])
+
+ while queue:
+ current = queue.popleft()
+ if current in visited:
+ continue
+
+ visited.add(current)
+ island.add(current)
+
+ for neighbor in adjacency[current]:
+ if neighbor not in visited:
+ queue.append(neighbor)
+
+ islands.append(island)
+
+ return islands
+
+ def execute(self, context):
+ obj = context.active_object
+ mesh = obj.data
+
+ # Use the most efficient approach: single merge with island constraints
+ bm = bmesh.from_edit_mesh(mesh)
+
+ # Get selected vertices
+ selected_verts = [v for v in bm.verts if v.select]
+ if not selected_verts:
+ self.report({'WARNING'}, "No vertices selected")
+ return {'CANCELLED'}
+
+ # Build islands using optimized algorithm
+ selected_set = set(selected_verts)
+ vert_to_island = {}
+ island_id = 0
+
+ # Find islands with stack-based traversal
+ for v in selected_verts:
+ if v in vert_to_island:
+ continue
+
+ # Mark all vertices in this island
+ stack = [v]
+ while stack:
+ current = stack.pop()
+ if current in vert_to_island:
+ continue
+
+ vert_to_island[current] = island_id
+
+ # Add connected vertices
+ for edge in current.link_edges:
+ other = edge.other_vert(current)
+ if other in selected_set and other not in vert_to_island:
+ stack.append(other)
+
+ island_id += 1
+
+ # Build merge mapping manually to avoid repeated operations
+ merge_targets = {}
+ total_merged = 0
+ islands_with_merges = 0
+
+ # For each island, find vertices to merge
+ from collections import defaultdict
+ island_verts = defaultdict(list)
+ for v, iid in vert_to_island.items():
+ island_verts[iid].append(v)
+
+ # Process each island
+ for island_id, verts in island_verts.items():
+ if len(verts) < 2:
+ continue
+
+ # Build spatial hash for this island only
+ merge_dist_sq = self.merge_distance * self.merge_distance
+ merged_in_island = 0
+
+ # Simple O(n²) for small islands is often faster than spatial hashing
+ # Most submeshes have 10-50 verts, so this is actually efficient
+ processed = set()
+ for i, v1 in enumerate(verts):
+ if v1 in merge_targets or v1 in processed:
+ continue
+
+ processed.add(v1)
+
+ # Find vertices within merge distance
+ for v2 in verts[i+1:]:
+ if v2 in merge_targets or v2 in processed:
+ continue
+
+ # Check distance
+ diff = v1.co - v2.co
+ if diff.length_squared <= merge_dist_sq:
+ merge_targets[v2] = v1
+ processed.add(v2)
+ merged_in_island += 1
+
+ if merged_in_island > 0:
+ total_merged += merged_in_island
+ islands_with_merges += 1
+
+ # Now perform all merges in one go using BMesh weld
+ if merge_targets:
+ # Convert merge mapping to format expected by weld_verts
+ targetmap = {v: merge_targets[v] for v in merge_targets if v.is_valid}
+
+ if targetmap:
+ bmesh.ops.weld_verts(bm, targetmap=targetmap)
+
+ # Update the mesh
+ bmesh.update_edit_mesh(mesh)
+
+ total_islands = len(island_verts)
+ multi_vert_islands = sum(1 for verts in island_verts.values() if len(verts) >= 2)
+ single_vert_islands = total_islands - multi_vert_islands
+
+ if total_merged > 0:
+ self.report({'INFO'}, f"Merged {total_merged} vertices in {islands_with_merges} of {multi_vert_islands} submeshes (skipped {single_vert_islands} single-vertex)")
+ else:
+ self.report({'INFO'}, f"No vertices close enough to merge in {multi_vert_islands} submeshes")
+
+ return {'FINISHED'}
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "merge_distance")
+ layout.label(text="Merges within submeshes only", icon='INFO')
+
+
class MESH_PT_bake_vertex_panel(Panel):
bl_label = "Bake Vertex Vectors"
bl_idname = "MESH_PT_bake_vertex_vectors"
@@ -359,12 +872,18 @@ class MESH_PT_bake_vertex_panel(Panel):
if obj and obj.type == 'MESH':
if context.mode == 'EDIT_MESH':
col.operator("mesh.select_all_linked_submeshes", icon='SELECT_EXTEND')
+ col.operator("mesh.select_linked_across_boundaries", icon='LINKED')
+ col.operator("mesh.deduplicate_submeshes", icon='DUPLICATE')
+ col.operator("mesh.merge_by_distance_in_submeshes", icon='AUTOMERGE_ON')
col.separator()
col.operator("mesh.bake_vertex_vectors", icon='EXPORT')
box = col.box()
box.label(text="Info:", icon='INFO')
box.label(text="Select All Linked: Expand selection to full submeshes")
+ box.label(text="Select Linked Cross: Select linked across boundaries")
+ box.label(text="Deduplicate: Remove duplicate selected submeshes")
+ box.label(text="Merge: Merge vertices within submeshes")
box.label(text="Bake: Auto-scale selected vertices")
box.label(text="Toggle Contiguous Groups for separate islands")
box.label(text="Scale factor stored in alpha channel")
@@ -382,6 +901,9 @@ class MESH_PT_bake_vertex_panel(Panel):
classes = [
MESH_OT_bake_vertex_vectors,
MESH_OT_select_all_linked_submeshes,
+ MESH_OT_select_linked_across_boundaries,
+ MESH_OT_deduplicate_submeshes,
+ MESH_OT_merge_by_distance_in_submeshes,
MESH_PT_bake_vertex_panel
]
@@ -389,6 +911,9 @@ classes = [
def menu_func(self, context):
self.layout.separator()
self.layout.operator("mesh.select_all_linked_submeshes", icon='SELECT_EXTEND')
+ self.layout.operator("mesh.select_linked_across_boundaries", icon='LINKED')
+ self.layout.operator("mesh.deduplicate_submeshes", icon='DUPLICATE')
+ self.layout.operator("mesh.merge_by_distance_in_submeshes", icon='AUTOMERGE_ON')
self.layout.operator("mesh.bake_vertex_vectors", icon='EXPORT')