summaryrefslogtreecommitdiffstats
path: root/BakeVertexData.py
diff options
context:
space:
mode:
Diffstat (limited to 'BakeVertexData.py')
-rw-r--r--BakeVertexData.py1084
1 files changed, 0 insertions, 1084 deletions
diff --git a/BakeVertexData.py b/BakeVertexData.py
deleted file mode 100644
index 3eecf05..0000000
--- a/BakeVertexData.py
+++ /dev/null
@@ -1,1084 +0,0 @@
-bl_info = {
- "name": "Bake Vertex to Target Vector",
- "blender": (4, 2, 0),
- "category": "Mesh",
- "version": (2, 3, 0),
- "author": "yum_food",
- "description": "Bake vertex vectors with automatic center and scale calculation in Edit Mode, with submesh deduplication"
-}
-
-import bpy
-import mathutils
-import bmesh
-import math
-from bpy.props import BoolProperty, FloatProperty, IntProperty, PointerProperty
-from bpy.types import Panel, Operator, PropertyGroup
-from collections import deque, defaultdict
-
-
-class BakeVertexSettings(PropertyGroup):
- correction_angle_x: FloatProperty(
- name="X",
- description="Correction angle for the X-axis",
- default=math.pi,
- subtype='ANGLE'
- )
- correction_angle_y: FloatProperty(
- name="Y",
- description="Correction angle for the Y-axis",
- default=0.0,
- subtype='ANGLE'
- )
- correction_angle_z: FloatProperty(
- name="Z",
- description="Correction angle for the Z-axis",
- default=0.0,
- subtype='ANGLE'
- )
-
-
-class MeshUtils:
- """Utility functions for mesh operations"""
-
- @staticmethod
- def with_mode(mode):
- """Decorator to handle mode switching"""
- def decorator(func):
- def wrapper(self, context, *args, **kwargs):
- original_mode = context.mode
- if mode == 'OBJECT' and context.mode != 'OBJECT':
- bpy.ops.object.mode_set(mode='OBJECT')
- elif mode == 'EDIT' and context.mode != 'EDIT_MESH':
- bpy.ops.object.mode_set(mode='EDIT')
-
- try:
- result = func(self, context, *args, **kwargs)
- finally:
- if original_mode == 'EDIT_MESH' and context.mode != 'EDIT_MESH':
- bpy.ops.object.mode_set(mode='EDIT')
- elif original_mode == 'OBJECT' and context.mode != 'OBJECT':
- bpy.ops.object.mode_set(mode='OBJECT')
-
- return result
- return wrapper
- return decorator
-
- @staticmethod
- def with_multi_object_support(process_func_name='process_object'):
- """Decorator to add multi-object support to operators
-
- The decorated execute method should return a tuple of (success, stats_dict)
- where stats_dict contains the statistics to aggregate across objects.
- """
- def decorator(func):
- def wrapper(self, context, *args, **kwargs):
- # Store the original active object
- original_active = context.active_object
-
- # Get all selected mesh objects
- selected_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
- if not selected_objects:
- self.report({'WARNING'}, "No mesh objects selected")
- return {'CANCELLED'}
-
- # Initialize aggregated stats
- total_stats = {}
- processed_count = 0
-
- # Process each selected object
- for obj in selected_objects:
- # Make this object active temporarily
- context.view_layer.objects.active = obj
-
- # Call the actual processing method
- if hasattr(self, process_func_name):
- success, stats = getattr(self, process_func_name)(context, obj)
- else:
- # Fallback to calling the decorated function
- result = func(self, context, obj, *args, **kwargs)
- if isinstance(result, tuple) and len(result) == 2:
- success, stats = result
- else:
- success, stats = (result == {'FINISHED'}), {}
-
- if success:
- processed_count += 1
- # Aggregate statistics
- for key, value in stats.items():
- if isinstance(value, (int, float)):
- total_stats[key] = total_stats.get(key, 0) + value
- else:
- # For non-numeric values, just store the last one
- total_stats[key] = value
-
- # Restore original active object
- context.view_layer.objects.active = original_active
-
- # Report results
- total_stats['object_count'] = processed_count
- if hasattr(self, 'format_report'):
- message = self.format_report(total_stats)
- else:
- # Default reporting
- if processed_count == 0:
- self.report({'WARNING'}, "No objects processed")
- return {'CANCELLED'}
- message = f"Processed {processed_count} object(s)"
-
- if message:
- self.report({'INFO'}, message)
-
- return {'FINISHED'}
- return wrapper
- return decorator
-
- @staticmethod
- def get_selected_vertices(mesh):
- """Get indices of selected vertices"""
- return {v.index for v in mesh.vertices if v.select}
-
- @staticmethod
- def build_adjacency(mesh, vertex_indices):
- """Build adjacency list for given vertices"""
- adjacency = {idx: set() for idx in vertex_indices}
- for edge in mesh.edges:
- v0, v1 = edge.vertices
- if v0 in adjacency and v1 in adjacency:
- adjacency[v0].add(v1)
- adjacency[v1].add(v0)
- return adjacency
-
- @staticmethod
- def flood_fill(start_nodes, adjacency_func):
- """Generic flood fill algorithm"""
- visited = set()
- result = set()
- queue = deque(start_nodes)
-
- while queue:
- current = queue.popleft()
- if current in visited:
- continue
- visited.add(current)
- result.add(current)
-
- for neighbor in adjacency_func(current):
- if neighbor not in visited:
- queue.append(neighbor)
-
- return result
-
- @staticmethod
- def find_islands(nodes, adjacency_func):
- """Find connected components"""
- islands = []
- visited = set()
-
- for node in nodes:
- if node in visited:
- continue
-
- island = MeshUtils.flood_fill([node], lambda n: adjacency_func.get(n, []))
- visited.update(island)
- islands.append(island)
-
- return islands
-
- @staticmethod
- def select_edges_and_faces(mesh):
- """Select edges and faces based on selected vertices"""
- for edge in mesh.edges:
- v0, v1 = edge.vertices
- if mesh.vertices[v0].select and mesh.vertices[v1].select:
- edge.select = True
-
- for face in mesh.polygons:
- if all(mesh.vertices[v].select for v in face.vertices):
- face.select = True
-
-
-class BaseSubmeshOperator(Operator):
- """Base class for submesh operations"""
- bl_options = {'REGISTER', 'UNDO'}
-
- @classmethod
- def poll(cls, context):
- obj = context.active_object
- return obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH'
-
- def get_selected_submeshes(self, mesh):
- """Get selected vertices grouped by submesh"""
- selected = MeshUtils.get_selected_vertices(mesh)
- if not selected:
- return []
- adjacency = MeshUtils.build_adjacency(mesh, selected)
- return MeshUtils.find_islands(selected, adjacency)
-
-
-class MESH_OT_select_all_linked(BaseSubmeshOperator):
- bl_idname = "mesh.select_all_linked"
- bl_label = "Select All Linked Submeshes"
- bl_description = "Select all vertices in any submesh that has at least one vertex selected"
-
- def process_object(self, context, obj):
- """Process a single object and return (success, stats)"""
- mesh = obj.data
- initially_selected = MeshUtils.get_selected_vertices(mesh)
-
- if not initially_selected:
- return False, {}
-
- all_vertices = set(range(len(mesh.vertices)))
- adjacency = MeshUtils.build_adjacency(mesh, all_vertices)
- islands = MeshUtils.find_islands(all_vertices, adjacency)
-
- expanded_count = 0
- affected_islands = 0
-
- for island in islands:
- if island & initially_selected:
- new_selections = island - initially_selected
- if new_selections:
- expanded_count += len(new_selections)
- affected_islands += 1
- for idx in island:
- mesh.vertices[idx].select = True
-
- MeshUtils.select_edges_and_faces(mesh)
-
- return True, {
- 'affected_islands': affected_islands,
- 'expanded_count': expanded_count
- }
-
- def format_report(self, stats):
- """Format the aggregated statistics into a report message"""
- if stats['object_count'] == 0:
- return "No objects with selected vertices found"
- return f"Expanded selection in {stats['affected_islands']} submeshes ({stats['expanded_count']} new vertices) across {stats['object_count']} object(s)"
-
- @MeshUtils.with_mode('OBJECT')
- @MeshUtils.with_multi_object_support('process_object')
- def execute(self, context):
- # The decorator handles everything
- pass
-
-
-class MESH_OT_select_linked_across_boundaries(BaseSubmeshOperator):
- 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"
-
- 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'
- )
-
- def build_combined_adjacency(self, mesh):
- """Build adjacency including both edges and position-based connections"""
- edge_adjacency = MeshUtils.build_adjacency(mesh, set(range(len(mesh.vertices))))
-
- # Build position adjacency
- scale = min(1.0 / self.epsilon, 1e7) if self.epsilon > 0 else 1e7
- position_map = defaultdict(list)
-
- for v in mesh.vertices:
- key = tuple(int(v.co[i] * scale) for i in range(3))
- position_map[key].append(v.index)
-
- position_adjacency = {}
- for vertices_at_pos in position_map.values():
- if len(vertices_at_pos) > 1:
- for i, v1 in enumerate(vertices_at_pos):
- if v1 not in position_adjacency:
- position_adjacency[v1] = set()
- position_adjacency[v1].update(vertices_at_pos[i+1:])
- for v2 in vertices_at_pos[i+1:]:
- if v2 not in position_adjacency:
- position_adjacency[v2] = set()
- position_adjacency[v2].add(v1)
-
- # Combine adjacencies
- def combined_adjacency(vertex):
- neighbors = set()
- if vertex in edge_adjacency:
- neighbors.update(edge_adjacency[vertex])
- if vertex in position_adjacency:
- neighbors.update(position_adjacency[vertex])
- return neighbors
-
- return combined_adjacency
-
- def process_object(self, context, obj):
- """Process a single object and return (success, stats)"""
- mesh = obj.data
- initially_selected = MeshUtils.get_selected_vertices(mesh)
-
- if not initially_selected:
- return False, {}
-
- combined_adjacency = self.build_combined_adjacency(mesh)
- visited = MeshUtils.flood_fill(initially_selected, combined_adjacency)
-
- for idx in visited:
- mesh.vertices[idx].select = True
-
- MeshUtils.select_edges_and_faces(mesh)
- expanded_count = len(visited) - len(initially_selected)
-
- return True, {
- 'selected': len(visited),
- 'expanded': expanded_count
- }
-
- def format_report(self, stats):
- """Format the aggregated statistics into a report message"""
- if stats['object_count'] == 0:
- return "No objects with selected vertices found"
- return f"Selected {stats['selected']} vertices ({stats['expanded']} new) across {stats['object_count']} object(s)"
-
- @MeshUtils.with_mode('OBJECT')
- @MeshUtils.with_multi_object_support('process_object')
- def execute(self, context):
- # The decorator handles everything
- pass
-
- 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(BaseSubmeshOperator):
- bl_idname = "mesh.deduplicate_submeshes"
- bl_label = "Deduplicate Submeshes"
- bl_description = "Remove duplicate submeshes from selection that have vertices at the same locations"
-
- 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
- )
-
- def get_island_signature(self, mesh, island_indices):
- """Create a hash for an island based on vertex positions"""
- 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
- rounded = tuple(round(co[i], decimal_places) for i in range(3))
- positions.append(rounded)
-
- positions.sort()
- return tuple(positions)
-
- def process_object(self, context, obj):
- """Process a single object and return (success, stats)"""
- mesh = obj.data
- islands = self.get_selected_submeshes(mesh)
-
- if len(islands) <= 1:
- return False, {}
-
- # Group islands by signature
- island_groups = defaultdict(list)
- for island in islands:
- signature = self.get_island_signature(mesh, island)
- island_groups[signature].append(island)
-
- # Find duplicates to remove
- vertices_to_delete = set()
- duplicate_count = 0
-
- for group in island_groups.values():
- if len(group) > 1:
- for island in group[1:]:
- vertices_to_delete.update(island)
- duplicate_count += 1
-
- if not vertices_to_delete:
- return False, {}
-
- # 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')
-
- for idx in vertices_to_delete:
- mesh.vertices[idx].select = True
-
- bpy.ops.object.mode_set(mode='EDIT')
- bpy.ops.mesh.delete(type='VERT')
- bpy.ops.object.mode_set(mode='OBJECT')
-
- return True, {
- 'duplicates': duplicate_count,
- 'vertices_deleted': len(vertices_to_delete)
- }
-
- def format_report(self, stats):
- """Format the aggregated statistics into a report message"""
- if stats['object_count'] == 0:
- return "No duplicate submeshes found"
- return f"Removed {stats['duplicates']} duplicate submeshes ({stats['vertices_deleted']} vertices) across {stats['object_count']} object(s)"
-
- @MeshUtils.with_mode('OBJECT')
- @MeshUtils.with_multi_object_support('process_object')
- def execute(self, context):
- # The decorator handles everything
- pass
-
- 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_pack_uv_islands_by_submesh_z(BaseSubmeshOperator):
- bl_idname = "mesh.pack_uv_islands_by_submesh_z"
- bl_label = "Pack UV Islands by Submesh Z"
- bl_description = "Pack UV islands vertically sorted by submesh Z position"
-
- padding: FloatProperty(
- name="Island Padding",
- description="Padding between UV islands",
- default=0.02,
- min=0.0,
- max=0.1,
- precision=3
- )
-
- max_islands_per_row: IntProperty(
- name="Max Islands Per Row",
- description="Maximum number of islands per row",
- default=100,
- min=1,
- max=1000
- )
-
- lock_overlapping: BoolProperty(
- name="Lock Overlapping Islands",
- description="Treat overlapping UV islands as a single island",
- default=False
- )
-
- skip_overlap_check: BoolProperty(
- name="Skip Overlap Check",
- description="Skip overlap detection for better performance",
- default=False
- )
-
- @classmethod
- def poll(cls, context):
- obj = context.active_object
- return (obj and obj.type == 'MESH' and
- context.mode == 'EDIT_MESH' and
- obj.data.uv_layers.active)
-
- def get_uv_islands(self, bm, uv_layer):
- """Find all UV islands in the mesh"""
- uv_vert_map = {}
- uv_adjacency = defaultdict(set)
-
- for face in bm.faces:
- if not face.select:
- continue
-
- face_uvs = []
- for loop in face.loops:
- uv = loop[uv_layer].uv
- uv_key = (round(uv.x, 6), round(uv.y, 6))
- face_uvs.append(uv_key)
-
- if uv_key not in uv_vert_map:
- uv_vert_map[uv_key] = set()
- uv_vert_map[uv_key].add(loop.vert.index)
-
- # Create UV edges
- for i in range(len(face_uvs)):
- j = (i + 1) % len(face_uvs)
- uv_adjacency[face_uvs[i]].add(face_uvs[j])
- uv_adjacency[face_uvs[j]].add(face_uvs[i])
-
- # Find connected components
- islands = []
- for start_uv in uv_vert_map:
- if any(start_uv in island['uvs'] for island in islands):
- continue
-
- island_uvs = MeshUtils.flood_fill([start_uv], lambda uv: uv_adjacency.get(uv, []))
- island_vert_indices = set()
- for uv in island_uvs:
- island_vert_indices.update(uv_vert_map[uv])
-
- islands.append({
- 'uvs': island_uvs,
- 'vert_indices': island_vert_indices,
- 'loops': []
- })
-
- return islands
-
- def execute(self, context):
- obj = context.active_object
- mesh = obj.data
-
- bm = bmesh.from_edit_mesh(mesh)
- bm_uv_layer = bm.loops.layers.uv.active
-
- # Get UV islands
- uv_islands = self.get_uv_islands(bm, bm_uv_layer)
- if not uv_islands:
- self.report({'WARNING'}, "No UV islands found")
- return {'CANCELLED'}
-
- # Get submeshes and their Z values
- bpy.ops.object.mode_set(mode='OBJECT')
- submeshes = self.get_selected_submeshes(mesh)
-
- submesh_z_values = []
- for submesh in submeshes:
- avg_z = sum(mesh.vertices[idx].co.z for idx in submesh) / len(submesh)
- submesh_z_values.append(avg_z)
-
- # Build vertex to submesh mapping
- vertex_to_submesh = {}
- for i, submesh in enumerate(submeshes):
- for v in submesh:
- vertex_to_submesh[v] = i
-
- bpy.ops.object.mode_set(mode='EDIT')
- bm = bmesh.from_edit_mesh(mesh)
- bm_uv_layer = bm.loops.layers.uv.active
-
- # Build UV to loops mapping
- uv_to_loops = defaultdict(list)
- for face in bm.faces:
- if face.select:
- for loop in face.loops:
- uv = loop[bm_uv_layer].uv
- uv_key = (round(uv.x, 6), round(uv.y, 6))
- uv_to_loops[uv_key].append(loop)
-
- # Assign loops and create island data
- island_data = []
- for island in uv_islands:
- island['loops'] = []
- for uv_key in island['uvs']:
- island['loops'].extend(uv_to_loops.get(uv_key, []))
-
- if not island['loops']:
- continue
-
- # Find submesh
- submesh_idx = None
- for v_idx in island['vert_indices']:
- if v_idx in vertex_to_submesh:
- submesh_idx = vertex_to_submesh[v_idx]
- break
-
- if submesh_idx is not None:
- # Get bounds
- min_u = min_v = float('inf')
- max_u = max_v = float('-inf')
-
- for loop in island['loops']:
- uv = loop[bm_uv_layer].uv
- min_u = min(min_u, uv.x)
- max_u = max(max_u, uv.x)
- min_v = min(min_v, uv.y)
- max_v = max(max_v, uv.y)
-
- width = max_u - min_u
- height = max_v - min_v
-
- if width > 0 and height > 0:
- island_data.append({
- 'island': island,
- 'submesh_z': submesh_z_values[submesh_idx],
- 'bounds': (min_u, min_v, max_u, max_v),
- 'width': width,
- 'height': height
- })
-
- # Sort by submesh Z
- island_data.sort(key=lambda x: x['submesh_z'], reverse=True)
-
- # Calculate scale
- total_area = sum((d['width'] + self.padding) * (d['height'] + self.padding)
- for d in island_data)
- target_size = min(0.95, math.sqrt(total_area) * 1.2)
- scale_factor = 0.95 / target_size if target_size > 1.0 else 1.0
-
- # Pack islands
- current_v = 0.95
- current_row = []
-
- for data in island_data:
- width = data['width'] * scale_factor
- height = data['height'] * scale_factor
-
- # Start new row if needed
- if current_row and (sum(d['width'] * scale_factor + self.padding for d in current_row) + width > target_size
- or len(current_row) >= self.max_islands_per_row):
- # Place current row
- current_u = 0.025
- row_height = max(d['height'] * scale_factor for d in current_row)
-
- for row_data in current_row:
- offset_u = current_u - row_data['bounds'][0] * scale_factor
- offset_v = current_v - row_data['bounds'][3] * scale_factor
-
- for loop in row_data['island']['loops']:
- uv = loop[bm_uv_layer].uv
- uv.x = uv.x * scale_factor + offset_u
- uv.y = uv.y * scale_factor + offset_v
-
- current_u += row_data['width'] * scale_factor + self.padding
-
- current_v -= row_height + self.padding
- current_row = []
-
- current_row.append(data)
-
- # Place final row
- if current_row:
- current_u = 0.025
- for row_data in current_row:
- offset_u = current_u - row_data['bounds'][0] * scale_factor
- offset_v = current_v - row_data['bounds'][3] * scale_factor
-
- for loop in row_data['island']['loops']:
- uv = loop[bm_uv_layer].uv
- uv.x = uv.x * scale_factor + offset_u
- uv.y = uv.y * scale_factor + offset_v
-
- current_u += row_data['width'] * scale_factor + self.padding
-
- bmesh.update_edit_mesh(mesh)
- self.report({'INFO'}, f"Packed {len(island_data)} UV islands from {len(submeshes)} submeshes")
- return {'FINISHED'}
-
- def draw(self, context):
- layout = self.layout
- layout.prop(self, "padding")
- layout.prop(self, "max_islands_per_row")
- layout.prop(self, "lock_overlapping")
- layout.prop(self, "skip_overlap_check")
-
-
-class MESH_OT_merge_by_distance_per_submesh(BaseSubmeshOperator):
- bl_idname = "mesh.merge_by_distance_per_submesh"
- bl_label = "Merge by Distance (Per Submesh)"
- bl_description = "Merge vertices by distance within each submesh separately"
-
- 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'
- )
-
- def process_object(self, context, obj):
- """Process a single object and return (success, stats)"""
- mesh = obj.data
- bm = bmesh.from_edit_mesh(mesh)
-
- selected_verts = [v for v in bm.verts if v.select]
- if not selected_verts:
- return False, {}
-
- # Build islands using BMesh
- selected_set = set(selected_verts)
- island_verts = []
- visited = set()
-
- for start_v in selected_verts:
- if start_v in visited:
- continue
-
- # Find island
- island = []
- stack = [start_v]
-
- while stack:
- current = stack.pop()
- if current in visited:
- continue
- visited.add(current)
- island.append(current)
-
- for edge in current.link_edges:
- other = edge.other_vert(current)
- if other in selected_set and other not in visited:
- stack.append(other)
-
- island_verts.append(island)
-
- # Merge within each island
- merge_targets = {}
- merge_dist_sq = self.merge_distance ** 2
- merged_count = 0
-
- for verts in island_verts:
- if len(verts) < 2:
- continue
-
- processed = set()
- for i, v1 in enumerate(verts):
- if v1 in merge_targets or v1 in processed:
- continue
- processed.add(v1)
-
- for v2 in verts[i+1:]:
- if v2 in merge_targets or v2 in processed:
- continue
-
- if (v1.co - v2.co).length_squared <= merge_dist_sq:
- merge_targets[v2] = v1
- processed.add(v2)
- merged_count += 1
-
- # Perform merges
- if merge_targets:
- targetmap = {v: merge_targets[v] for v in merge_targets if v.is_valid}
- if targetmap:
- bmesh.ops.weld_verts(bm, targetmap=targetmap)
-
- bmesh.update_edit_mesh(mesh)
-
- return merged_count > 0, {'merged': merged_count}
-
- def format_report(self, stats):
- """Format the aggregated statistics into a report message"""
- if stats.get('merged', 0) > 0:
- return f"Merged {stats['merged']} vertices across {stats['object_count']} object(s)"
- else:
- return "No vertices close enough to merge"
-
- @MeshUtils.with_multi_object_support('process_object')
- def execute(self, context):
- # The decorator handles everything
- pass
-
- def draw(self, context):
- layout = self.layout
- layout.prop(self, "merge_distance")
-
-
-class MESH_OT_bake_origin_and_orientation_combined(BaseSubmeshOperator):
- bl_idname = "mesh.bake_submesh_origin_and_orientation"
- bl_label = "Bake Submesh Data"
- bl_description = "Bake vertex vectors and orientation quaternions"
-
- contiguous_mode: BoolProperty(
- name="Contiguous Groups",
- description="Process each contiguous group separately",
- default=True
- )
-
- normal_epsilon: FloatProperty(
- name="Normal Epsilon",
- description="Minimum angle difference between normals",
- default=0.1,
- min=0.01,
- max=1.0,
- precision=3
- )
-
- use_cache: BoolProperty(
- name="Cache Identical Submeshes",
- description="Cache calculations for identical submeshes to avoid recomputing basis and scale",
- default=True
- )
-
- def calculate_island_center(self, mesh, island_indices):
- """Calculate center for an island"""
- if not island_indices:
- return None
-
- center = mathutils.Vector((0.0, 0.0, 0.0))
- for idx in island_indices:
- center += mesh.vertices[idx].co
- center /= len(island_indices)
-
- return center
-
- def calculate_island_scale(self, mesh, island_indices, center, basis_inv):
- """Calculate scale using L-infinity norm in rotated basis"""
- if not island_indices:
- return 1.0
-
- max_coord = 0.0
- for idx in island_indices:
- # Transform to local rotated basis
- offset = mesh.vertices[idx].co - center
- local_pos = basis_inv @ offset
-
- # L-infinity norm: max of absolute values
- max_coord = max(max_coord, abs(local_pos.x), abs(local_pos.y), abs(local_pos.z))
-
- scale = 1.0 / max_coord if max_coord > 0 else 1.0
- return scale
-
- def build_basis_from_faces(self, mesh, face_indices, epsilon):
- """Build orthonormal basis from face normals"""
- if not face_indices:
- return mathutils.Matrix.Identity(3)
-
- # Get largest face normal
- faces = sorted(face_indices, key=lambda i: mesh.polygons[i].area, reverse=True)
- x_axis = mesh.polygons[faces[0]].normal.normalized()
-
- # Find second normal
- epsilon_cos = math.cos(epsilon)
- y_axis = None
-
- for face_idx in faces[1:]:
- normal = mesh.polygons[face_idx].normal.normalized()
- if abs(normal.dot(x_axis)) < epsilon_cos:
- y_axis = normal - normal.dot(x_axis) * x_axis
- y_axis.normalize()
- break
-
- if not y_axis:
- if abs(x_axis.z) < 0.9:
- y_axis = mathutils.Vector((-x_axis.y, x_axis.x, 0))
- else:
- y_axis = mathutils.Vector((0, -x_axis.z, x_axis.y))
- y_axis.normalize()
-
- z_axis = x_axis.cross(y_axis)
-
- matrix = mathutils.Matrix((x_axis, y_axis, z_axis)).transposed()
- if matrix.determinant() < 0:
- matrix[2] = -matrix[2]
-
- return matrix
-
- def create_submesh_signature(self, mesh, island_indices, center):
- """Create signature for caching - based on relative positions only"""
- tolerance = 0.0001
- relative_positions = []
-
- for idx in island_indices:
- relative_pos = mesh.vertices[idx].co - center
- # Round to tolerance to handle floating point precision
- rounded = tuple(round(relative_pos[i] / tolerance) * tolerance for i in range(3))
- relative_positions.append(rounded)
-
- relative_positions.sort()
- return (len(island_indices), tuple(relative_positions))
-
- def process_object(self, context, obj):
- """Process a single object and return (success, stats)"""
- mesh = obj.data
- selected = MeshUtils.get_selected_vertices(mesh)
-
- if not selected:
- return False, {}
-
- # Create/get data layers
- if not mesh.vertex_colors:
- mesh.vertex_colors.new(name="BakedVectors")
- color_layer = mesh.vertex_colors.active
-
- uv_layer0 = mesh.uv_layers.get("BakedOriginAngle0") or mesh.uv_layers.new(name="BakedOriginAngle0")
- uv_layer1 = mesh.uv_layers.get("BakedOriginAngle1") or mesh.uv_layers.new(name="BakedOriginAngle1")
-
- # Get correction quaternion
- settings = context.scene.bake_vertex_settings
- correction = mathutils.Euler(
- (settings.correction_angle_x, settings.correction_angle_y, settings.correction_angle_z), 'XYZ'
- ).to_quaternion()
-
- # Build vertex to loops mapping
- vertex_loops = defaultdict(list)
- selected_faces = []
-
- for poly in mesh.polygons:
- face_verts = set(poly.vertices)
- if face_verts & selected:
- if face_verts <= selected:
- selected_faces.append(poly.index)
-
- for loop_idx in poly.loop_indices:
- vert_idx = mesh.loops[loop_idx].vertex_index
- if vert_idx in selected:
- vertex_loops[vert_idx].append(loop_idx)
-
- # Get islands
- islands = self.get_selected_submeshes(mesh) if self.contiguous_mode else [selected]
-
- # Process each island
- world_matrix = obj.matrix_world
- world_inv = world_matrix.inverted()
-
- # Cache for storing computed basis, scale, and quaternion for identical submeshes
- # Key: geometry signature, Value: (scale, basis, quaternion, basis_inverse)
- submesh_cache = {} if self.use_cache else None
-
- for island in islands:
- center = self.calculate_island_center(mesh, island)
- if center is None:
- continue
-
- # Check cache first (if enabled)
- cache_hit = False
- if self.use_cache:
- signature = self.create_submesh_signature(mesh, island, center)
- if signature in submesh_cache:
- scale, basis, quat, basis_inv = submesh_cache[signature]
- cache_hit = True
-
- # Only calculate if not in cache
- if not cache_hit:
- # Calculate basis (compute-intensive)
- island_faces = [f for f in selected_faces
- if all(v in island for v in mesh.polygons[f].vertices)]
- basis = self.build_basis_from_faces(mesh, island_faces, self.normal_epsilon)
- basis_inv = basis.inverted()
-
- # Calculate scale using L-infinity norm in rotated basis (compute-intensive)
- scale = self.calculate_island_scale(mesh, island, center, basis_inv)
-
- # Calculate quaternion
- quat = basis.to_quaternion()
- quat.normalize()
- if quat.w < 0:
- quat.negate()
- quat = correction @ quat
-
- # Store in cache if enabled
- if self.use_cache:
- submesh_cache[signature] = (scale, basis, quat, basis_inv)
-
- # Transform vertices
- center_world = world_matrix @ center
-
- for vert_idx in island:
- vert_world = world_matrix @ mesh.vertices[vert_idx].co
- offset = world_inv.to_3x3() @ (vert_world - center_world)
- local_pos = basis_inv @ offset
-
- # Scale and convert to color
- color = mathutils.Vector((
- (local_pos.x * scale + 1.0) * 0.5,
- (local_pos.y * scale + 1.0) * 0.5,
- (local_pos.z * scale + 1.0) * 0.5,
- scale
- ))
-
- # Apply to loops
- for loop_idx in vertex_loops.get(vert_idx, []):
- color_layer.data[loop_idx].color = color
- uv_layer0.data[loop_idx].uv = (quat.x, quat.y)
- uv_layer1.data[loop_idx].uv = (quat.z, quat.w)
-
- mesh.update()
-
- return True, {
- 'islands': len(islands),
- 'vertices': len(selected)
- }
-
- def format_report(self, stats):
- """Format the aggregated statistics into a report message"""
- if stats['object_count'] == 0:
- return "No objects with selected vertices found"
- return f"Baked {stats['islands']} island(s) with {stats['vertices']} vertices across {stats['object_count']} object(s)"
-
- @MeshUtils.with_mode('OBJECT')
- @MeshUtils.with_multi_object_support('process_object')
- def execute(self, context):
- # The decorator handles everything
- pass
-
- def draw(self, context):
- layout = self.layout
- layout.prop(self, "contiguous_mode")
- layout.prop(self, "use_cache")
- layout.prop(self, "normal_epsilon")
-
- settings = context.scene.bake_vertex_settings
- box = layout.box()
- box.label(text="Bake Rotation Correction (Degrees)")
- row = box.row(align=True)
- row.prop(settings, "correction_angle_x")
- row.prop(settings, "correction_angle_y")
- row.prop(settings, "correction_angle_z")
-
-
-class MESH_PT_bake_vertex_panel(Panel):
- bl_label = "Bake Submesh Data"
- bl_idname = "MESH_PT_bake_submesh_data"
- bl_space_type = 'VIEW_3D'
- bl_region_type = 'UI'
- bl_category = "Tool"
-
- def draw(self, context):
- layout = self.layout
- obj = context.active_object
-
- if obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH':
- col = layout.column()
- col.operator("mesh.bake_submesh_origin_and_orientation", icon='EXPORT')
- col.operator("mesh.select_all_linked", 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_per_submesh", icon='AUTOMERGE_ON')
- col.operator("mesh.pack_uv_islands_by_submesh_z", icon='UV')
- else:
- layout.label(text="Enter Edit Mode to use tools", icon='INFO')
-
-
-classes = [
- BakeVertexSettings,
- MESH_OT_select_all_linked,
- MESH_OT_select_linked_across_boundaries,
- MESH_OT_deduplicate_submeshes,
- MESH_OT_pack_uv_islands_by_submesh_z,
- MESH_OT_merge_by_distance_per_submesh,
- MESH_OT_bake_origin_and_orientation_combined,
- MESH_PT_bake_vertex_panel
-]
-
-
-def menu_func(self, context):
- self.layout.separator()
- self.layout.operator("mesh.select_all_linked", 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_per_submesh", icon='AUTOMERGE_ON')
- self.layout.operator("mesh.pack_uv_islands_by_submesh_z", icon='UV')
- self.layout.operator("mesh.bake_submesh_origin_and_orientation", icon='EXPORT')
-
-
-def register():
- for cls in classes:
- bpy.utils.register_class(cls)
- bpy.types.VIEW3D_MT_edit_mesh.append(menu_func)
- bpy.types.Scene.bake_vertex_settings = PointerProperty(type=BakeVertexSettings)
-
-
-def unregister():
- bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)
- del bpy.types.Scene.bake_vertex_settings
- for cls in reversed(classes):
- bpy.utils.unregister_class(cls)
-
-
-if __name__ == "__main__":
- register()