Created
February 26, 2025 19:37
-
-
Save zippy731/d1444d6b7160111751fa76e316ffc651 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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