Spaces:
Running
Running
| """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 | |
| 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 | |