Spaces:
Running
Running
| import numpy as np | |
| from shapely.geometry import LineString, Point, Polygon | |
| from shapely.ops import unary_union | |
| from typing import Optional | |
| from .graph import CreaseGraph, VERTEX_TOL | |
| UNIT_SQUARE_CORNERS = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] | |
| _UNIT_SQUARE = Polygon(UNIT_SQUARE_CORNERS) | |
| class PaperState: | |
| """ | |
| Represents the evolving crease pattern on a unit square [0,1]x[0,1]. | |
| Uses CreaseGraph for the underlying data structure. | |
| """ | |
| def __init__(self): | |
| self.graph = CreaseGraph() | |
| self.fold_history: list[dict] = [] | |
| def anchor_points(self) -> list[tuple[float, float]]: | |
| points: dict[tuple[float, float], None] = {} | |
| for corner in UNIT_SQUARE_CORNERS: | |
| points[corner] = None | |
| for vid, (x, y) in self.graph.vertices.items(): | |
| points[(float(x), float(y))] = None | |
| return list(points.keys()) | |
| def _is_anchor(self, pt: tuple[float, float]) -> bool: | |
| px, py = pt | |
| for ax, ay in self.anchor_points(): | |
| if abs(ax - px) < VERTEX_TOL and abs(ay - py) < VERTEX_TOL: | |
| return True | |
| return False | |
| def add_crease(self, p1: list, p2: list, assignment: str) -> dict: | |
| errors: list[str] = [] | |
| if assignment not in ('M', 'V'): | |
| return { | |
| 'valid': False, | |
| 'anchored': False, | |
| 'new_vertices': [], | |
| 'errors': ['invalid_assignment'], | |
| } | |
| p1 = (float(p1[0]), float(p1[1])) | |
| p2 = (float(p2[0]), float(p2[1])) | |
| anchored = self._is_anchor(p1) and self._is_anchor(p2) | |
| seg_len = np.hypot(p2[0] - p1[0], p2[1] - p1[1]) | |
| if seg_len < VERTEX_TOL: | |
| errors.append('zero_length') | |
| return {'valid': False, 'anchored': anchored, 'new_vertices': [], 'errors': errors} | |
| new_line = LineString([p1, p2]) | |
| if not _UNIT_SQUARE.contains(new_line) and not _UNIT_SQUARE.boundary.contains(new_line): | |
| clipped = new_line.intersection(_UNIT_SQUARE) | |
| if clipped.is_empty: | |
| errors.append('outside_bounds') | |
| return {'valid': False, 'anchored': anchored, 'new_vertices': [], 'errors': errors} | |
| intersection_points: list[tuple[float, float]] = [] | |
| for eid, (ev1, ev2, _) in list(self.graph.edges.items()): | |
| ex1, ey1 = self.graph.vertices[ev1] | |
| ex2, ey2 = self.graph.vertices[ev2] | |
| existing_line = LineString([(ex1, ey1), (ex2, ey2)]) | |
| inter = new_line.intersection(existing_line) | |
| if inter.is_empty: | |
| continue | |
| if inter.geom_type == 'Point': | |
| ix, iy = inter.x, inter.y | |
| ep1 = (ex1, ey1) | |
| ep2 = (ex2, ey2) | |
| if ( | |
| abs(ix - ep1[0]) < VERTEX_TOL and abs(iy - ep1[1]) < VERTEX_TOL | |
| or abs(ix - ep2[0]) < VERTEX_TOL and abs(iy - ep2[1]) < VERTEX_TOL | |
| ): | |
| continue | |
| intersection_points.append((ix, iy)) | |
| # MultiPoint or LineString intersections (collinear) are skipped | |
| new_vertex_coords: list[tuple[float, float]] = [] | |
| for ix, iy in intersection_points: | |
| before = set(self.graph.vertices.keys()) | |
| vid = self.graph.add_vertex(ix, iy) | |
| if vid not in before: | |
| new_vertex_coords.append((ix, iy)) | |
| for eid in list(self.graph.edges.keys()): | |
| if eid not in self.graph.edges: | |
| continue | |
| ev1, ev2, _ = self.graph.edges[eid] | |
| ex1, ey1 = self.graph.vertices[ev1] | |
| ex2, ey2 = self.graph.vertices[ev2] | |
| seg = LineString([(ex1, ey1), (ex2, ey2)]) | |
| pt = Point(ix, iy) | |
| if seg.distance(pt) < VERTEX_TOL: | |
| if ev1 != vid and ev2 != vid: | |
| self.graph.split_edge(eid, vid) | |
| v1_id = self.graph.add_vertex(p1[0], p1[1]) | |
| v2_id = self.graph.add_vertex(p2[0], p2[1]) | |
| waypoints = [p1] + sorted( | |
| intersection_points, | |
| key=lambda pt: np.hypot(pt[0] - p1[0], pt[1] - p1[1]), | |
| ) + [p2] | |
| waypoint_ids = [] | |
| for wp in waypoints: | |
| wid = self.graph.add_vertex(wp[0], wp[1]) | |
| waypoint_ids.append(wid) | |
| for i in range(len(waypoint_ids) - 1): | |
| wa = waypoint_ids[i] | |
| wb = waypoint_ids[i + 1] | |
| if wa != wb: | |
| self.graph.add_edge(wa, wb, assignment) | |
| record = { | |
| 'p1': p1, | |
| 'p2': p2, | |
| 'assignment': assignment, | |
| 'anchored': anchored, | |
| 'new_vertices': new_vertex_coords, | |
| } | |
| self.fold_history.append(record) | |
| return { | |
| 'valid': True, | |
| 'anchored': anchored, | |
| 'new_vertices': new_vertex_coords, | |
| 'errors': errors, | |
| } | |
| def crease_edges(self) -> list[dict]: | |
| result = [] | |
| for eid in self.graph.crease_edges(): | |
| v1, v2, assignment = self.graph.edges[eid] | |
| x1, y1 = self.graph.vertices[v1] | |
| x2, y2 = self.graph.vertices[v2] | |
| result.append({'v1': (x1, y1), 'v2': (x2, y2), 'assignment': assignment}) | |
| return result | |