Spaces:
Running
Running
File size: 4,621 Bytes
bc52096 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """Origami fold simulator — analytical rotation with cumulative transforms.
BFS from face 0 through the face adjacency graph. Each face accumulates
a rotation transform (R, t) such that: folded_pos = R @ flat_pos + t.
When crossing a fold edge, the fold rotation is composed with the parent
face's transform. Non-fold edges inherit the parent's transform directly.
"""
from dataclasses import dataclass
import numpy as np
from scipy.spatial.transform import Rotation
from .fold_parser import parse_fold
@dataclass
class SimResult:
"""Result of a fold simulation."""
positions: np.ndarray # (N, 3) final vertex positions
converged: bool
steps_taken: int
max_strain: float
total_energy: float
def simulate(
fold_data: dict,
crease_percent: float = 1.0,
max_steps: int = 500,
params: dict | None = None,
) -> SimResult:
"""Simulate a FOLD crease pattern and return final 3D positions."""
parsed = parse_fold(fold_data)
flat_pos = parsed["vertices"].copy()
edges = parsed["edges"]
assignments = parsed["assignments"]
fold_angles = parsed["fold_angles"]
faces = parsed["faces"]
positions = flat_pos.copy()
if len(faces) == 0:
return SimResult(
positions=positions, converged=True,
steps_taken=0, max_strain=0.0, total_energy=0.0,
)
face_adj = _build_face_adjacency(faces)
crease_map: dict[tuple[int, int], float] = {}
for i, (v1, v2) in enumerate(edges):
key = (min(int(v1), int(v2)), max(int(v1), int(v2)))
if assignments[i] in ("M", "V"):
crease_map[key] = fold_angles[i] * crease_percent
n_faces = len(faces)
face_R = [None] * n_faces
face_t = [None] * n_faces
face_R[0] = np.eye(3)
face_t[0] = np.zeros(3)
visited = [False] * n_faces
visited[0] = True
placed: set[int] = set()
for vi in faces[0]:
placed.add(int(vi))
queue = [0]
while queue:
fi = queue.pop(0)
face = faces[fi]
for j in range(len(face)):
v1, v2 = int(face[j]), int(face[(j + 1) % len(face)])
edge_key = (min(v1, v2), max(v1, v2))
for fj in face_adj.get(edge_key, []):
if visited[fj]:
continue
visited[fj] = True
queue.append(fj)
angle = crease_map.get(edge_key, 0.0)
if abs(angle) > 1e-10:
p1 = positions[v1].copy()
axis = positions[v2] - p1
axis_len = np.linalg.norm(axis)
if axis_len > 1e-12:
axis_unit = axis / axis_len
fold_rot = Rotation.from_rotvec(
angle * axis_unit,
).as_matrix()
else:
fold_rot = np.eye(3)
face_R[fj] = fold_rot @ face_R[fi]
face_t[fj] = fold_rot @ (face_t[fi] - p1) + p1
else:
face_R[fj] = face_R[fi].copy()
face_t[fj] = face_t[fi].copy()
for vi in faces[fj]:
vi_int = int(vi)
if vi_int not in placed:
positions[vi_int] = face_R[fj] @ flat_pos[vi_int] + face_t[fj]
placed.add(vi_int)
max_strain = _compute_strain(positions, parsed)
return SimResult(
positions=positions,
converged=True,
steps_taken=1,
max_strain=max_strain,
total_energy=0.0,
)
def _build_face_adjacency(
faces: np.ndarray,
) -> dict[tuple[int, int], list[int]]:
"""Map each edge (sorted vertex pair) to list of face indices."""
adj: dict[tuple[int, int], list[int]] = {}
for fi, face in enumerate(faces):
n = len(face)
for j in range(n):
v1, v2 = int(face[j]), int(face[(j + 1) % n])
key = (min(v1, v2), max(v1, v2))
if key not in adj:
adj[key] = []
adj[key].append(fi)
return adj
def _compute_strain(positions: np.ndarray, parsed: dict) -> float:
"""Compute max axial strain across all edges."""
edges = parsed["edges"]
vertices_flat = parsed["vertices"]
max_strain = 0.0
for v1, v2 in edges:
rest = np.linalg.norm(vertices_flat[v2] - vertices_flat[v1])
curr = np.linalg.norm(positions[v2] - positions[v1])
if rest > 1e-12:
strain = abs(curr - rest) / rest
max_strain = max(max_strain, strain)
return max_strain
|