Skip to content

Instantly share code, notes, and snippets.

@zippy731
Created February 26, 2025 19:37
Show Gist options
  • Save zippy731/d1444d6b7160111751fa76e316ffc651 to your computer and use it in GitHub Desktop.
Save zippy731/d1444d6b7160111751fa76e316ffc651 to your computer and use it in GitHub Desktop.
import bpy
import math
import mathutils
from mathutils import Vector
from collections import deque, defaultdict
def print_header(text):
"""Print a nicely formatted header for sections."""
print(f"\n{'=' * 50}")
print(f" {text}")
print(f"{'=' * 50}")
def print_subheader(text):
"""Print a nicely formatted subheader."""
print(f"\n{'-' * 40}")
print(f" {text}")
print(f"{'-' * 40}")
def get_face_center(mesh, face):
"""Calculate the center of a face."""
verts = [mesh.vertices[v] for v in face.vertices]
center = sum((v.co for v in verts), Vector()) / len(verts)
return center
def pick_origin_face(mesh):
"""
Select the face with:
1) lowest Z-center
2) if tie, largest area
3) if tie, most edges (verts)
4) if tie, lowest face index
"""
print_subheader("SELECTING ORIGIN FACE")
best_idx = None
best_data = None
for f in mesh.polygons:
center = get_face_center(mesh, f)
z_center = center.z
area = f.area
num_verts = len(f.vertices)
compare_tuple = (z_center, -area, -num_verts, f.index)
if best_idx is None or compare_tuple < best_data:
best_idx = f.index
best_data = compare_tuple
print(f" New best face: {f.index} with z={z_center:.4f}, area={area:.4f}, verts={num_verts}")
print(f" Selected origin face: {best_idx}")
return best_idx
def build_mesh_connectivity(mesh):
"""
Build the mesh connectivity information including:
- face adjacency (which faces are connected to each face)
- edge to faces mapping (which faces share each edge)
- each face's edges
"""
print_subheader("BUILDING MESH CONNECTIVITY")
# Map edges to faces
edge_to_faces = defaultdict(list)
for f in mesh.polygons:
for edge_key in f.edge_keys:
# Sort edge keys for consistency
sorted_edge = tuple(sorted(edge_key))
edge_to_faces[sorted_edge].append(f.index)
# Build face adjacency
face_adjacency = defaultdict(set)
for edge_key, faces in edge_to_faces.items():
if len(faces) == 1:
# This is an outer edge (boundary)
continue
for i in range(len(faces)):
for j in range(i+1, len(faces)):
face_adjacency[faces[i]].add(faces[j])
face_adjacency[faces[j]].add(faces[i])
# Build face to edges mapping
face_to_edges = {}
for f in mesh.polygons:
face_to_edges[f.index] = [tuple(sorted(ek)) for ek in f.edge_keys]
print(f" Total faces: {len(mesh.polygons)}")
print(f" Total unique edges: {len(edge_to_faces)}")
print(f" Found {sum(1 for faces in edge_to_faces.values() if len(faces) == 1)} outer edges")
# Print a few sample face adjacencies
sample_count = min(5, len(face_adjacency))
print(f" Sample adjacencies (showing {sample_count}):")
for i, (face_idx, neighbors) in enumerate(list(face_adjacency.items())[:sample_count]):
print(f" Face {face_idx}: connected to {neighbors}")
return face_adjacency, edge_to_faces, face_to_edges
def find_shared_edge(mesh, face1, face2, face_to_edges):
"""Find the shared edge between two faces."""
edges1 = set(face_to_edges[face1])
edges2 = set(face_to_edges[face2])
shared = edges1.intersection(edges2)
if shared:
return next(iter(shared)) # Return the first shared edge
return None
def initialize_paths(mesh, origin_face_idx, face_adjacency, face_to_edges):
"""
Initialize one path for each edge of the origin face.
Each path starts with just the origin face.
"""
print_subheader("INITIALIZING PATHS")
# Get all edges of the origin face
origin_edges = face_to_edges[origin_face_idx]
# Initialize paths
paths = []
for i, edge_key in enumerate(origin_edges):
path = {
"id": i,
"origin_edge": edge_key,
"faces": {
origin_face_idx: {
"level": 0,
"parent": None,
"connecting_edge": None,
"children": []
}
},
"edges": set(), # Edges used in this path
"face_indices": {origin_face_idx}, # Initialize face_indices set, starting with origin_face_idx
"highest_level": 0,
"faces_at_highest_level": {origin_face_idx},
"max_level": 0,
"initial_unfold_angle": 0.0,
"direction_reason_initial_angle": "Origin Face",
"levels": { # <--- Ensure "levels" key is initialized correctly as a dictionary
0: {origin_face_idx} # <--- Level 0 should contain origin_face_idx as a set
},
"origin_face_idx": origin_face_idx, # <--- ADD THIS LINE INSIDE DICT INITIALIZATION
}
paths.append(path)
print(f" Initialized path {i} for origin edge {edge_key}")
return paths
def is_edge_adjacent_to_path_edges(edge_key, face_idx, path, face_to_edges):
"""
Check if an edge shares any vertices with edges already in the path
that are connected to this face.
"""
# Get all vertices of the candidate edge
v1, v2 = edge_key
edge_verts = {v1, v2}
# Check all edges currently in the path that belong to this face
for path_edge in path["edges"]:
# If the edge is part of this face
if path_edge in face_to_edges[face_idx]:
# Get vertices of path edge
pv1, pv2 = path_edge
path_verts = {pv1, pv2}
# If they share any vertices, they're adjacent
if edge_verts.intersection(path_verts):
return True
return False
def select_best_edge_for_face(face_idx, available_edges, path, face_to_edges, mesh):
"""
Select the best edge to traverse from a face based on the criteria:
1. Prefer edges not adjacent to current path edges
2. If face is a quad, prefer edge opposite to entry edge
3. For pentagon/hexagon, skip nearest edge and choose next one
4. If no clear choice, use lowest index edge
"""
# Get all edges of this face that are still available
face_edges = face_to_edges[face_idx]
candidate_edges = [e for e in face_edges if e in available_edges]
if not candidate_edges:
return None
# Check if this is a quad face (4 edges)
if len(face_edges) == 4:
# Find the entry edge (the edge connecting to parent)
parent_face = path["faces"][face_idx]["parent"]
if parent_face is not None:
entry_edge = path["faces"][face_idx]["connecting_edge"]
# Find the opposite edge
for e in candidate_edges:
# A simple way to check for "opposite" - no shared vertices
e_verts = set(e)
entry_verts = set(entry_edge)
if not e_verts.intersection(entry_verts):
return e
# For all faces, first try to find edges not adjacent to current path edges
non_adjacent_edges = []
for e in candidate_edges:
if not is_edge_adjacent_to_path_edges(e, face_idx, path, face_to_edges):
non_adjacent_edges.append(e)
if non_adjacent_edges:
# Sort by edge index and return the lowest one
sorted_edges = sorted(non_adjacent_edges, key=lambda e: (e[0], e[1]))
return sorted_edges[0]
# If no non-adjacent edges, just pick the lowest index edge
sorted_edges = sorted(candidate_edges, key=lambda e: (e[0], e[1]))
return sorted_edges[0]
def select_face_with_fewest_edges_in_path(faces, path, face_to_edges):
"""
From a list of faces, select the one that has the fewest edges
already in the path. Break ties using lowest face index.
"""
face_scores = []
for face_idx in faces:
# Count how many edges of this face are already in the path
face_edges = face_to_edges[face_idx]
used_edges = sum(1 for e in face_edges if e in path["edges"])
face_scores.append((used_edges, face_idx))
# Sort by used edges, then by face index
face_scores.sort()
return face_scores[0][1] if face_scores else None
def grow_path_one_step(path, available_faces, available_edges, face_adjacency, edge_to_faces, face_to_edges, mesh, copy_obj, split_edges, outer_edges): # Added copy_obj
"""Grows a single path by one face, round-robin style, enhanced logging and direction-corrected angle storage."""
current_level = path["max_level"]
faces_at_current_level = path["levels"][current_level]
faces_at_next_level = set()
path_grown = False
print(f" Path {path['id']} turn:")
print(f" Path {path['id']}: Growing from level {current_level}")
print(f" Faces at this level: {faces_at_current_level}")
for current_face_idx in faces_at_current_level:
print(f" Selected face {current_face_idx} for traversal")
current_face_edges = face_to_edges[current_face_idx]
available_face_edges = list(available_edges.intersection(current_face_edges)) # Edges of current face that are still available
# --- INSERT DEBUG PRINTS HERE ---
print(f" [EDGE-AVAIL-DEBUG] Before intersection - Available edges (count): {len(available_edges)}, Edges: {available_edges}")
available_face_edges = list(available_edges.intersection(current_face_edges)) # Edges of current face that are still available
print(f" [EDGE-AVAIL-DEBUG] After intersection - Traversable edges: {available_face_edges}")
print(f" Available edges on face {current_face_idx} for traversal: {available_face_edges}") # (Existing print)
# --- END INSERT DEBUG PRINTS ---
if not available_face_edges:
print(f" No available edges on face {current_face_idx} to traverse.")
continue # No edges to traverse for this face
# Find 'best' edge to traverse (outer, then non-split, then any available)
best_edge = None
best_edge_type = None # 'outer', 'non-split', or 'any'
outer_face_edges = [e for e in available_face_edges if e in outer_edges]
if outer_face_edges:
best_edge = outer_face_edges[0] # Just pick the first outer edge if available
best_edge_type = 'outer'
else:
non_split_face_edges = [e for e in available_face_edges if e not in split_edges]
if non_split_face_edges:
best_edge = non_split_face_edges[0] # Pick first non-split if available
best_edge_type = 'non-split'
elif available_face_edges:
best_edge = available_face_edges[0] # Just pick any available edge as fallback
best_edge_type = 'any'
if best_edge:
print(f" Selected edge {best_edge} for traversal")
# Determine next face index
new_face_idx = edge_to_faces[best_edge][1] if edge_to_faces[best_edge][0] == current_face_idx else edge_to_faces[best_edge][0]
print(f" [DEBUG-ORIGIN-FACE-KEY] Before check - Path ID: {path['id']}, Keys in path: {path.keys()}") # <--- ADD DEBUG PRINT
print(f" [DEBUG-ORIGIN-FACE-KEY] Origin Face Index from Path: {path.get('origin_face_idx', 'KeyError or Missing')}") # <--- ADD DEBUG PRINT
# Check if we are not trаversing back to origin face
if new_face_idx == path["origin_face_idx"]:
print(f" Avoided traversing back to origin face {new_face_idx} via edge {best_edge}")
available_edges.discard(best_edge) # Make edge unavailable for this path, try another edge if available
continue # Skip to next available edge or face
if new_face_idx in path["face_indices"]:
print(f" Avoided revisiting face {new_face_idx} via edge {best_edge}")
available_edges.discard(best_edge)
continue
# Update path data with new face and edge
if (current_level + 1) not in path["levels"]: # <--- ADD THIS CHECK
path["levels"][current_level + 1] = set() # <--- AND THIS INITIALIZATION
path["levels"][current_level + 1].add(new_face_idx)
path["max_level"] += 1
path["face_indices"].add(new_face_idx)
path["faces"][new_face_idx] = {
"parent": current_face_idx,
"connecting_edge": best_edge,
"children": [],
"level": current_level + 1
}
path["faces"][current_face_idx]["children"].append(new_face_idx)
# **NEW: Calculate INITIAL unfold angle AND direction correction in grow_path_one_step**
initial_angle_radians = compute_angle_between_faces(mesh, new_face_idx, current_face_idx) # Child vs Parent
# Get necessary data for direction determination (using parent and child faces)
hinge = best_edge # Connecting edge is the hinge
vA_co, vB_co = edge_verts_positions(copy_obj, hinge) # Need copy_obj here to get positions - added copy_obj to function args
axis_vec = (vB_co - vA_co).normalized()
direction_factor, direction_reason = determine_rotation_direction_factor( # Determine direction
mesh, new_face_idx, current_face_idx, initial_angle_radians, axis_vec, vA_co, vB_co
)
corrected_initial_angle_radians = initial_angle_radians * direction_factor # Apply correction
path["faces"][new_face_idx]["initial_unfold_angle"] = corrected_initial_angle_radians # Store *corrected* initial angle
path["faces"][new_face_idx]["direction_reason_initial_angle"] = direction_reason # Store direction reason for logging
print(f" Traversing to face {new_face_idx}")
print(f" [INIT-ANGLE-DEBUG] Face {new_face_idx}: Initial Angle (degrees): {math.degrees(initial_angle_radians):.4f}, Direction Factor: {direction_factor}, Reason: {direction_reason}, Corrected Angle (degrees): {math.degrees(corrected_initial_angle_radians):.4f}")
faces_at_next_level.add(new_face_idx)
path["edges"].add(best_edge)
available_faces.remove(new_face_idx)
available_edges.difference_update(current_face_edges) # Remove edges of the face we moved *from*
path_grown = True
else:
print(f" No suitable edge found to traverse from face {current_face_idx} this round.")
return path_grown, faces_at_next_level, available_faces, available_edges, split_edges, outer_edges
def collect_subtree_vertices(mesh, path, face_idx):
"""Return vertex indices for face_idx + all descendants."""
stack = [face_idx]
all_verts = set()
print(f" [CSV-DEBUG] Starting vertex collection for face {face_idx} subtree.") # Start marker
while stack:
f = stack.pop()
face_vertex_indices = list(mesh.polygons[f].vertices)
print(f" [CSV-DEBUG] Face {f} (polygon index): Vertices (vertex indices) = {face_vertex_indices}, Children = {path['faces'][f]['children']}") # Detailed face vertex info + CHILDREN DEBUG
for v in face_vertex_indices:
all_verts.add(v)
for c in path["faces"][f]["children"]:
stack.append(c)
collected_vert_indices = list(all_verts)
print(f" [CSV-DEBUG] Total collected verts for face {face_idx} subtree: {collected_vert_indices}") # End marker
return collected_vert_indices
def compute_angle_between_faces(mesh, child_face, parent_face):
"""Compute angle between two faces with enhanced debugging."""
child_normal = mesh.polygons[child_face].normal.copy()
parent_normal = mesh.polygons[parent_face].normal.copy()
print(f" [ANGLE-DEBUG] Face {child_face} vs Parent {parent_face}:") # Added debug header
print(f" [ANGLE-DEBUG] Child Normal: {child_normal}")
print(f" [ANGLE-DEBUG] Parent Normal: {parent_normal}")
dot_val = max(min(child_normal.dot(parent_normal), 1.0), -1.0)
angle = math.acos(dot_val)
angle_deg = math.degrees(angle) # Angle in degrees for easier understanding
print(f" [ANGLE-DEBUG] Dot Product: {dot_val:.4f}")
print(f" [ANGLE-DEBUG] Angle (radians): {angle:.4f}")
print(f" [ANGLE-DEBUG] Angle (degrees): {angle_deg:.4f}") # Added degree output
return angle
def edge_verts_positions(obj, edge_key): # Changed to accept 'obj'
"""Get 3D positions of an edge's vertices using obj.data.vertices."""
vA_idx, vB_idx = edge_key
return (obj.data.vertices[vA_idx].co.copy(), obj.data.vertices[vB_idx].co.copy()) # Use obj.data.vertices
def rotate_face_center(face_center, hinge_vA_co, rot_mat):
"""Rotate a face center point around hinge vertex A using the given rotation matrix."""
local_center = face_center - hinge_vA_co
rotated_center = rot_mat @ local_center
final_center = rotated_center + hinge_vA_co
return final_center
def determine_rotation_direction_factor(mesh, face_idx, parent_idx, use_angle, axis_vec, vA_co, vB_co):
"""
Determine the rotation direction factor (1.0 or -1.0) to unfold outwards,
based on face center heuristic.
"""
parent_face = mesh.polygons[parent_idx]
child_face = mesh.polygons[face_idx]
parent_center = get_face_center(mesh, parent_face)
child_center_initial = get_face_center(mesh, child_face)
child_normal_initial = child_face.normal.copy()
parent_to_child_initial_vec = child_center_initial - parent_center
rot_mat_positive = mathutils.Matrix.Rotation(use_angle, 4, axis_vec)
child_center_rotated_positive = rotate_face_center(child_center_initial, vA_co, rot_mat_positive)
parent_to_child_rotated_positive_vec = child_center_rotated_positive - parent_center
dot_product_positive = child_normal_initial.dot(parent_to_child_rotated_positive_vec)
rot_mat_negative = mathutils.Matrix.Rotation(-use_angle, 4, axis_vec)
child_center_rotated_negative = rotate_face_center(child_center_initial, vA_co, rot_mat_negative)
parent_to_child_rotated_negative_vec = child_center_rotated_negative - parent_center
dot_product_negative = child_normal_initial.dot(parent_to_child_rotated_negative_vec)
print(f" [DIR-DEBUG] Face {face_idx}: Parent Center: {parent_center}, Child Center (Initial): {child_center_initial}")
print(f" [DIR-DEBUG] Face {face_idx}: Child Normal (Initial): {child_normal_initial}")
print(f" [DIR-DEBUG] Face {face_idx}: Dot Product (Positive Angle): {dot_product_positive:.4f}, Dot Product (Negative Angle): {dot_product_negative:.4f}")
if dot_product_positive > dot_product_negative:
direction_factor = 1.0
reason = "Positive dot product better"
else:
direction_factor = -1.0
reason = "Negative dot product better/equal"
print(f" [DIR-DEBUG] Face {face_idx}: Chosen Direction Factor: {direction_factor}, Reason: {reason}")
return direction_factor, reason # Return both factor and reason for logging
def rotate_subtree(obj, mesh, path, face_idx, fraction, shape_key):
"""Rotate face_idx and descendants using pre-calculated, direction-corrected angles."""
face_data = path["faces"][face_idx]
parent_idx = face_data["parent"]
print(f" [RS-DEBUG] Starting rotation for face {face_idx}, fraction {fraction}")
subtree_verts = collect_subtree_vertices(mesh, path, face_idx)
print(f" [RS-DEBUG] Face {face_idx}, Verts to rotate: {subtree_verts}")
if parent_idx is not None and face_data["connecting_edge"]: # Rotate only if has parent and hinge
hinge = face_data["connecting_edge"]
vA_co, vB_co = edge_verts_positions(obj, hinge)
axis_vec = (vB_co - vA_co).normalized()
print(f" [AXIS-DEBUG] Face {face_idx}, Hinge Edge: {hinge}:")
print(f" [AXIS-DEBUG] Vertex A Co: {vA_co}")
print(f" [AXIS-DEBUG] Vertex B Co: {vB_co}")
print(f" [AXIS-DEBUG] Axis Vector: {axis_vec}")
# **Retrieve pre-calculated, direction-corrected initial_unfold_angle from path data**
corrected_initial_unfold_angle = face_data["initial_unfold_angle"] # Get corrected angle
use_angle = corrected_initial_unfold_angle * fraction # Apply fraction
print(f" [ANGLE-DEBUG] Face {face_idx}, Corrected Initial Angle (degrees): {math.degrees(corrected_initial_unfold_angle):.4f}, Fraction: {fraction}, Use Angle (degrees): {math.degrees(use_angle):.4f}")
rot_mat = mathutils.Matrix.Rotation(use_angle, 4, axis_vec) # Use pre-corrected angle
for v_idx in subtree_verts:
orig_co = obj.data.vertices[v_idx].co
local_co = orig_co - vA_co
rotated_co = rot_mat @ local_co
final_co = rotated_co + vA_co
shape_key.data[v_idx].co = final_co
# **Vertex Position Logging:** (keep vertex logging as is)
print(f" [VERT-LOG] ShapeKey: {shape_key.name}, Face: {face_idx}, Vert: {v_idx}")
print(f" [VERT-LOG] Original Coords: {orig_co}")
print(f" [VERT-LOG] Rotated Coords: {final_co}")
print(f" [VERT-LOG] ShapeKey Data Coords (after assignment): {shape_key.data[v_idx].co}")
else: # Origin face or no hinge
print(f" [RS-DEBUG] Origin face {face_idx} or no hinge, no rotation.")
print(f" [RS-DEBUG] Finished rotation for face {face_idx}, fraction {fraction}")
# Recurse to children
for child_idx in face_data["children"]:
rotate_subtree(obj, mesh, path, child_idx, fraction, shape_key)
def create_shape_keys_for_path(obj, mesh, path, origin_face_idx):
"""Create shape keys for unfolding animation with incremental deformation and simplified Relative To setting."""
print_subheader(f"CREATING SHAPE KEYS FOR PATH {path['id']}")
# **REMOVE Basis creation - assume Basis exists (created in unfold_papercraft)**
# Ensure basis exists
# if not obj.data.shape_keys:
# obj.shape_key_add(name="Basis", from_mix=False)
# print(" Created Basis shape key")
# Skip paths with no additional faces (keep this)
if len(path["faces"]) <= 1:
print(" Path has no additional faces, skipping shape keys")
return
fractions = [0.25, 0.50, 0.75, 1.00]
previous_shape_key = None
previous_shape_key = None # Reset at start - already in latest version
for frac in fractions:
sk_name = f"Path_{path['id']}_{int(frac*100)}%"
if previous_shape_key:
previous_shape_key.value = 1.0
print(f" Ensured {previous_shape_key.name} factor is 1.0 before creating {sk_name}")
shape_key = obj.shape_key_add(name=sk_name, from_mix=True)
print(f" Creating shape key: {sk_name} (from mix=True)")
if previous_shape_key:
shape_key.relative_key = previous_shape_key
print(f" Set {sk_name} Relative To: {previous_shape_key.name}")
# ELSE - for 25% key, rely on default "Relative To: Basis" (implicitly)
rotate_subtree(obj, mesh, path, origin_face_idx, frac, shape_key)
print(f" Completed rotation for {sk_name}")
previous_shape_key = shape_key
def print_path_summary(path):
"""Print a summary of a path's structure."""
print(f" Path {path['id']} summary:")
print(f" Origin edge: {path['origin_edge']}")
print(f" Total faces: {len(path['faces'])}")
print(f" Total edges: {len(path['edges'])}")
print(f" Max hierarchy level: {path['highest_level']}")
# Count faces at each level
level_counts = {}
for face_idx, face_data in path["faces"].items():
level = face_data["level"]
level_counts[level] = level_counts.get(level, 0) + 1
print(f" Faces by level: {level_counts}")
# Print hierarchy tree
if len(path["faces"]) > 1:
print(f" Hierarchy tree:")
def print_tree(face_idx, depth=0):
face_data = path["faces"][face_idx]
indent = " " + " " * depth
edge_info = ""
if face_data["connecting_edge"]:
edge_info = f" via edge {face_data['connecting_edge']}"
print(f"{indent}Face {face_idx} (level {face_data['level']}){edge_info}")
for child in face_data["children"]:
print_tree(child, depth + 1)
# Find origin face (level 0)
for face_idx, face_data in path["faces"].items():
if face_data["level"] == 0:
print_tree(face_idx)
break
def split_edges_and_duplicate_verts(mesh, all_fold_edges):
"""
Split non-fold edges using bmesh.ops.split_edges.
(Corrected for Blender 4.1 - use_duplicate=True removed, NO manual duplication)
"""
print_subheader("SPLITTING EDGES AND DUPLICATING VERTS")
initial_vertex_count = len(mesh.vertices)
# Collect all fold edges from all paths (already done in main script)
# all_fold_edges = set()
# for path in paths:
# all_fold_edges.update(path["edges"])
print(f"Total fold edges: {len(all_fold_edges)}")
# Check if any edges have been processed
if len(all_fold_edges) == 0:
print("WARNING: No fold edges found! All edges will be split and verts duplicated.")
print("Check the path building logic.")
# Instead of selection and ops.edge_split, use BMesh for direct edge & vert splitting
import bmesh
# Make sure we're in object mode
if bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Create BMesh from the mesh
bm = bmesh.new()
bm.from_mesh(mesh)
bm.edges.ensure_lookup_table()
edges_to_split = []
edge_keys_to_split = []
for edge in bm.edges:
v0, v1 = edge.verts[0].index, edge.verts[1].index
edge_key = tuple(sorted((v0, v1)))
if edge_key not in all_fold_edges:
edges_to_split.append(edge)
edge_keys_to_split.append(edge_key)
split_count = len(edges_to_split)
print(f"Found {split_count} BMesh edges to split and duplicate verts:")
# for ek in edge_keys_to_split: # No longer printing all edge keys to avoid clutter
# print(f" - Edge {ek}")
# Only split and duplicate if we have edges to split and some fold edges exist
if split_count > 0: # Removed the check for `len(all_fold_edges) > 0` to allow splitting all if no fold edges found.
# 1. Split the edges using BMesh's edge splitting
split_result = bmesh.ops.split_edges(bm, edges=edges_to_split) # use_duplicate=True REMOVED - THIS IS CORRECT!
split_edges_new = split_result['edges']
final_vertex_count = len(mesh.vertices)
vertices_added = final_vertex_count - initial_vertex_count
print(f"Split {split_count} edges (using bmesh.ops.split_edges), added {vertices_added} vertices.") # Adjusted print message
else:
print("No edges to split or duplicate.")
# Update the mesh data from BMesh
bm.to_mesh(mesh)
bm.free()
mesh.update()
print(f"Mesh verts after duplication: {len(mesh.vertices)}") # Debug: Check vertex count
def reset_shape_key_values(obj):
"""Reset all shape key values to 0 for the given object."""
print_subheader("RESETTING SHAPE KEY VALUES")
if obj.data.shape_keys:
for key_block in obj.data.shape_keys.key_blocks:
key_block.value = 0.0
print(f" Resetting shape key: {key_block.name} to value 0.0")
else:
print(" No shape keys found to reset.")
def unfold_papercraft():
"""Main function for the papercraft unfolding script."""
print_header("STARTING UNFOLD PAPERCRAFT SCRIPT")
# Always start in Object Mode to ensure consistent behavior
if bpy.context.object and bpy.context.object.mode != 'OBJECT':
print("Switching to Object Mode before starting")
bpy.ops.object.mode_set(mode='OBJECT')
# 1) Get active object
obj = bpy.context.active_object
if not obj or obj.type != 'MESH':
print("Please select a valid mesh object.")
return
original_name = obj.name
print(f"Processing object: {original_name}")
# 2) Duplicate the object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.duplicate()
copy_obj = bpy.context.active_object
copy_obj.name = f"{original_name}-flat"
mesh = copy_obj.data
mesh.update()
print(f"Created duplicate object: {copy_obj.name}")
print(f"Mesh stats: {len(mesh.vertices)} vertices, {len(mesh.edges)} edges, {len(mesh.polygons)} faces")
# 3) Pick the origin face
origin_face_idx = pick_origin_face(mesh)
origin_face = mesh.polygons[origin_face_idx]
print(f"Origin face has {len(origin_face.vertices)} vertices")
# 4) Build mesh connectivity
face_adjacency, edge_to_faces, face_to_edges = build_mesh_connectivity(mesh)
# 5) Initialize paths - one for each edge of the origin face
paths = initialize_paths(mesh, origin_face_idx, face_adjacency, face_to_edges)
print_subheader("DEBUG - PATHS AFTER INITIALIZATION") # <--- ADD HEADER
for path_check in paths: # <--- LOOP THROUGH INITIALIZED PATHS
print(f" Path ID: {path_check['id']}, Keys: {path_check.keys()}, Origin Face Index: {path_check.get('origin_face_idx', 'Missing')}") # <--- DEBUG PRINT INSIDE LOOP
# 6) Set up available pools
available_faces = set(f.index for f in mesh.polygons)
available_faces.remove(origin_face_idx) # Origin face is already claimed
available_edges = set()
for edge_key in edge_to_faces.keys():
available_edges.add(edge_key)
split_edges = set()
outer_edges = set()
# 7) Perform turn-based growth
print_subheader("GROWING PATHS")
all_paths_done = False
max_rounds = len(mesh.polygons) * 2 # Safety limit
round_num = 0
while not all_paths_done and round_num < max_rounds:
round_num += 1
print(f"\nRound {round_num}:")
paths_grown = 0
for i, path in enumerate(paths):
print(f"\n Path {i} turn:")
result = grow_path_one_step(path, available_faces, available_edges,
face_adjacency, edge_to_faces, face_to_edges,
mesh, copy_obj, split_edges, outer_edges)
if result:
paths_grown += 1
print(f"\nRound {round_num} results: {paths_grown} paths grown")
print(f" Remaining available faces: {len(available_faces)}")
print(f" Remaining available edges: {len(available_edges)}")
print(f" Current split edges: {len(split_edges)}")
print(f" Current outer edges: {len(outer_edges)}")
if paths_grown == 0:
all_paths_done = True
print_subheader("GROWTH COMPLETED")
print(f"Completed after {round_num} rounds")
print(f"Final available faces: {len(available_faces)}")
print(f"Final available edges: {len(available_edges)}")
print(f"Final split edges: {len(split_edges)}")
print(f"Final outer edges: {len(outer_edges)}")
# 8) Print final path summaries
print_subheader("PATH SUMMARIES")
for path in paths:
print_path_summary(path)
# 9) COLLECTING FOLD EDGES
print_subheader("COLLECTING FOLD EDGES") # Added header
all_fold_edges = set()
for path in paths:
all_fold_edges.update(path["edges"])
print(f"Total fold edges across all paths: {len(all_fold_edges)}")
print(f"Fold edges: {all_fold_edges}")
# --- all_fold_edges is now defined ---
# 10) Split non-fold edges and DUPLICATE VERTS
split_edges_and_duplicate_verts(mesh, all_fold_edges) # Call the edge splitting function
# 10.5) **NEW: Create Basis Shape Key ONCE, before path loop**
print_subheader("ENSURING BASIS SHAPE KEY EXISTS")
if not copy_obj.data.shape_keys: # Check on copy_obj
copy_obj.shape_key_add(name="Basis", from_mix=False) # Create Basis on copy_obj
print(" Created Basis shape key (once, before paths)")
# 11) Create shape keys for each path
print_subheader("CREATING SHAPE KEYS")
for path in paths:
create_shape_keys_for_path(copy_obj, mesh, path, origin_face_idx)
# 11.5) **NEW: Reset Shape Key Values AFTER each path**
reset_shape_key_values(copy_obj) # Reset shape keys after each path
# 12) (Remove the final reset in unfold_papercraft, as it's now done per path)
# reset_shape_key_values(copy_obj) # REMOVE this line - reset is now done per path
# Print final mesh stats - Moved after shape key creation and splitting for accurate info
mesh.update()
print(f"Final mesh stats: {len(mesh.vertices)} vertices, {len(mesh.edges)} edges, {len(mesh.polygons)} faces")
print_header(f"UNFOLDING COMPLETED: {copy_obj.name}")
return copy_obj
class OBJECT_OT_unfold_papercraft_multi(bpy.types.Operator):
"""Unfold the selected mesh into a papercraft pattern with multiple paths."""
bl_idname = "object.unfold_papercraft_multi"
bl_label = "Unfold Papercraft (Multi-Paths)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
unfold_papercraft()
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_unfold_papercraft_multi)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_unfold_papercraft_multi)
# For debugging in the Text Editor:
#if __name__ == "__main__":
# unfold_papercraft()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment