File size: 5,867 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""FOLD JSON parsing and validation.

Validates LLM-generated FOLD crease patterns before simulation.
FOLD spec: https://github.com/edemaine/fold
"""

from typing import Any

import numpy as np


def validate_fold(fold_data: dict[str, Any]) -> tuple[bool, str]:
    """Validate a FOLD JSON object. Returns (is_valid, error_message)."""

    # Required fields
    for key in ("vertices_coords", "edges_vertices", "edges_assignment"):
        if key not in fold_data:
            return False, f"Missing required field: {key}"

    verts = fold_data["vertices_coords"]
    edges = fold_data["edges_vertices"]
    assignments = fold_data["edges_assignment"]

    # Must have at least 3 vertices (a triangle)
    if len(verts) < 3:
        return False, f"Need at least 3 vertices, got {len(verts)}"

    # Must have at least 3 edges
    if len(edges) < 3:
        return False, f"Need at least 3 edges, got {len(edges)}"

    # Edges and assignments must match length
    if len(edges) != len(assignments):
        return False, (
            f"edges_vertices ({len(edges)}) and "
            f"edges_assignment ({len(assignments)}) must match length"
        )

    # Fold angles must match if present
    if "edges_foldAngle" in fold_data:
        angles = fold_data["edges_foldAngle"]
        if len(angles) != len(edges):
            return False, (
                f"edges_foldAngle ({len(angles)}) must match "
                f"edges_vertices ({len(edges)})"
            )

    # Validate vertex coordinates (2D or 3D)
    num_verts = len(verts)
    for i, v in enumerate(verts):
        if not isinstance(v, (list, tuple)) or len(v) < 2:
            return False, f"Vertex {i} must be [x, y] or [x, y, z], got {v}"

    # Validate edge indices
    for i, e in enumerate(edges):
        if not isinstance(e, (list, tuple)) or len(e) != 2:
            return False, f"Edge {i} must be [v1, v2], got {e}"
        v1, v2 = e
        if v1 < 0 or v1 >= num_verts or v2 < 0 or v2 >= num_verts:
            return False, f"Edge {i} references invalid vertex: {e}"
        if v1 == v2:
            return False, f"Edge {i} is degenerate (same vertex): {e}"

    # Validate assignments
    valid_assignments = {"M", "V", "B", "F", "U", "C"}
    for i, a in enumerate(assignments):
        if a not in valid_assignments:
            return False, f"Edge {i} has invalid assignment '{a}'"

    # Must have at least one fold crease (M or V)
    has_fold = any(a in ("M", "V") for a in assignments)
    if not has_fold:
        return False, "No fold creases (M or V) found"

    # Must have boundary edges
    has_boundary = any(a == "B" for a in assignments)
    if not has_boundary:
        return False, "No boundary edges (B) found"

    return True, ""


def parse_fold(fold_data: dict[str, Any]) -> dict[str, np.ndarray]:
    """Parse validated FOLD JSON into numpy arrays for simulation.

    Returns dict with:
        vertices: (N, 3) float64 — vertex positions (z=0 for 2D input)
        edges: (E, 2) int — edge vertex indices
        assignments: list[str] — edge type per edge
        fold_angles: (E,) float64 — target fold angle per edge (radians)
        faces: (F, 3) int — triangulated face vertex indices
    """
    verts = fold_data["vertices_coords"]

    # Ensure 3D (add z=0 if 2D)
    vertices = np.zeros((len(verts), 3), dtype=np.float64)
    for i, v in enumerate(verts):
        vertices[i, 0] = v[0]
        vertices[i, 1] = v[1]
        if len(v) > 2:
            vertices[i, 2] = v[2]

    edges = np.array(fold_data["edges_vertices"], dtype=np.int32)
    assignments = list(fold_data["edges_assignment"])

    # Fold angles: default based on assignment if not provided
    if "edges_foldAngle" in fold_data:
        fold_angles = np.array(fold_data["edges_foldAngle"], dtype=np.float64)
    else:
        fold_angles = np.zeros(len(edges), dtype=np.float64)
        for i, a in enumerate(assignments):
            if a == "V":
                fold_angles[i] = 180.0
            elif a == "M":
                fold_angles[i] = -180.0

    # Convert degrees to radians for simulation
    fold_angles_rad = np.radians(fold_angles)

    # Triangulate faces
    if "faces_vertices" in fold_data:
        raw_faces = fold_data["faces_vertices"]
        faces = _triangulate_faces(raw_faces)
    else:
        faces = _compute_faces(vertices, edges)

    return {
        "vertices": vertices,
        "edges": edges,
        "assignments": assignments,
        "fold_angles": fold_angles_rad,
        "faces": faces,
    }


def _triangulate_faces(raw_faces: list[list[int]]) -> np.ndarray:
    """Fan-triangulate polygon faces into triangles."""
    triangles = []
    for face in raw_faces:
        if len(face) < 3:
            continue
        for i in range(1, len(face) - 1):
            triangles.append([face[0], face[i], face[i + 1]])
    if not triangles:
        return np.zeros((0, 3), dtype=np.int32)
    return np.array(triangles, dtype=np.int32)


def _compute_faces(vertices: np.ndarray, edges: np.ndarray) -> np.ndarray:
    """Compute triangulated faces from vertices and edges using adjacency."""
    from collections import defaultdict

    adj = defaultdict(set)
    for v1, v2 in edges:
        adj[v1].add(v2)
        adj[v2].add(v1)

    triangles = set()
    for v1, v2 in edges:
        common = adj[v1] & adj[v2]
        for v3 in common:
            tri = tuple(sorted([v1, v2, v3]))
            triangles.add(tri)

    if not triangles:
        # Fallback: create faces using Delaunay on 2D projection
        from scipy.spatial import Delaunay

        pts_2d = vertices[:, :2]
        try:
            tri = Delaunay(pts_2d)
            return tri.simplices.astype(np.int32)
        except Exception:
            return np.zeros((0, 3), dtype=np.int32)

    return np.array(list(triangles), dtype=np.int32)