nova_rerun_bridge.hull_visualizer
1from typing import Any 2 3import numpy as np 4import trimesh 5from scipy.spatial import ConvexHull 6 7 8class HullVisualizer: 9 @staticmethod 10 def compute_hull_mesh( 11 polygons: list[np.ndarray], 12 ) -> tuple[list[list[float]], list[list[int]], list[list[float]]]: 13 """Convert polygons to mesh with optimized hull generation.""" 14 vertices = np.vstack(polygons) 15 16 # Custom qhull options for better quality 17 qhull_opts = trimesh.convex.QhullOptions( 18 Qt=True, # Triangulated output 19 QJ=True, # Joggled input for precision 20 Qc=True, # Keep coplanar points 21 Qx=True, # Exact pre-merges 22 QbB=True, # Scale to unit cube 23 Pp=True, # Remove precision warnings 24 ) 25 26 mesh = trimesh.convex.convex_hull(vertices, qhull_options=qhull_opts, repair=True) 27 28 return (mesh.vertices.tolist(), mesh.faces.tolist(), mesh.vertex_normals.tolist()) 29 30 @staticmethod 31 def plane_from_triangle(p0, p1, p2, normal_epsilon=1e-6): 32 # Compute normal 33 v1 = p1 - p0 34 v2 = p2 - p0 35 n = np.cross(v1, v2) 36 norm = np.linalg.norm(n) 37 if norm < normal_epsilon: 38 return None, None 39 n = n / norm 40 # Plane: n·x = d 41 d = np.dot(n, p0) 42 return n, d 43 44 @staticmethod 45 def group_coplanar_triangles(points, hull, angle_epsilon=1e-6, dist_epsilon=1e-6): 46 # Group triangles by their plane (normal and distance) 47 plane_map = {} 48 for simplex in hull.simplices: 49 p0, p1, p2 = points[simplex] 50 n, d = HullVisualizer.plane_from_triangle(p0, p1, p2) 51 if n is None: 52 continue 53 54 # Ensure a canonical representation of the plane normal 55 for i_comp in range(3): 56 if abs(n[i_comp]) > angle_epsilon: 57 if n[i_comp] < 0: 58 n = -n 59 d = -d 60 break 61 62 # Round normal and distance for stable hashing 63 n_rounded = tuple(np.round(n, 6)) 64 d_rounded = round(d, 3) 65 66 key = (n_rounded, d_rounded) 67 if key not in plane_map: 68 plane_map[key] = [] 69 plane_map[key].append(simplex) 70 71 return plane_map 72 73 @staticmethod 74 def merge_coplanar_triangles_to_polygon(points, simplices): 75 # Extract polygon outline from coplanar triangles 76 edges = {} 77 for tri in simplices: 78 for i in range(3): 79 a = tri[i] 80 b = tri[(i + 1) % 3] 81 e = (min(a, b), max(a, b)) 82 edges[e] = edges.get(e, 0) + 1 83 84 # Keep only outer edges (appear once) 85 boundary_edges = [e for e, count in edges.items() if count == 1] 86 if not boundary_edges: 87 return [] 88 89 # Build adjacency 90 adj = {} 91 for a, b in boundary_edges: 92 adj.setdefault(a, []).append(b) 93 adj.setdefault(b, []).append(a) 94 95 # Walk along the boundary edges to form a closed loop 96 start = boundary_edges[0][0] 97 loop = [start] 98 current = start 99 prev = None 100 while True: 101 neighbors = adj[current] 102 next_vertex = None 103 for n in neighbors: 104 if n != prev: 105 next_vertex = n 106 break 107 if next_vertex is None: 108 break 109 loop.append(next_vertex) 110 prev, current = current, next_vertex 111 if next_vertex == start: 112 break 113 114 polygon_points = points[loop] 115 return polygon_points 116 117 @staticmethod 118 def compute_hull_outlines_from_geometries(child_geometries: list[Any]) -> list[np.ndarray]: 119 """Compute polygon outlines from geometry child objects. 120 121 Args: 122 child_geometries: List of geometry objects containing convex hulls 123 124 Returns: 125 List of closed polygons as Nx3 numpy arrays 126 """ 127 all_points = [] 128 for child in child_geometries: 129 if child.convex_hull is not None: 130 for v in child.convex_hull.vertices: 131 all_points.append([v.x, v.y, v.z]) 132 133 if len(all_points) < 4: 134 return [] 135 136 return HullVisualizer._compute_hull_from_points(np.array(all_points)) 137 138 @staticmethod 139 def compute_hull_outlines_from_points(points: list[list[float]]) -> list[np.ndarray]: 140 """Compute polygon outlines directly from point coordinates. 141 142 Args: 143 points: List of [x,y,z] coordinates 144 145 Returns: 146 List of closed polygons as Nx3 numpy arrays 147 """ 148 if len(points) < 4: 149 return [] 150 151 return HullVisualizer._compute_hull_from_points(np.array(points)) 152 153 @staticmethod 154 def _compute_hull_from_points(points: np.ndarray) -> list[np.ndarray]: 155 """Internal helper to compute hull from numpy points array.""" 156 try: 157 hull = ConvexHull(points) 158 plane_map = HullVisualizer.group_coplanar_triangles(points, hull) 159 160 polygons = [] 161 for simplices in plane_map.values(): 162 polygon_points = HullVisualizer.merge_coplanar_triangles_to_polygon( 163 points, simplices 164 ) 165 if len(polygon_points) > 2: 166 closed_loop = np.vstack([polygon_points, polygon_points[0]]) 167 polygons.append(closed_loop) 168 return polygons 169 170 except Exception: 171 return []
class
HullVisualizer:
9class HullVisualizer: 10 @staticmethod 11 def compute_hull_mesh( 12 polygons: list[np.ndarray], 13 ) -> tuple[list[list[float]], list[list[int]], list[list[float]]]: 14 """Convert polygons to mesh with optimized hull generation.""" 15 vertices = np.vstack(polygons) 16 17 # Custom qhull options for better quality 18 qhull_opts = trimesh.convex.QhullOptions( 19 Qt=True, # Triangulated output 20 QJ=True, # Joggled input for precision 21 Qc=True, # Keep coplanar points 22 Qx=True, # Exact pre-merges 23 QbB=True, # Scale to unit cube 24 Pp=True, # Remove precision warnings 25 ) 26 27 mesh = trimesh.convex.convex_hull(vertices, qhull_options=qhull_opts, repair=True) 28 29 return (mesh.vertices.tolist(), mesh.faces.tolist(), mesh.vertex_normals.tolist()) 30 31 @staticmethod 32 def plane_from_triangle(p0, p1, p2, normal_epsilon=1e-6): 33 # Compute normal 34 v1 = p1 - p0 35 v2 = p2 - p0 36 n = np.cross(v1, v2) 37 norm = np.linalg.norm(n) 38 if norm < normal_epsilon: 39 return None, None 40 n = n / norm 41 # Plane: n·x = d 42 d = np.dot(n, p0) 43 return n, d 44 45 @staticmethod 46 def group_coplanar_triangles(points, hull, angle_epsilon=1e-6, dist_epsilon=1e-6): 47 # Group triangles by their plane (normal and distance) 48 plane_map = {} 49 for simplex in hull.simplices: 50 p0, p1, p2 = points[simplex] 51 n, d = HullVisualizer.plane_from_triangle(p0, p1, p2) 52 if n is None: 53 continue 54 55 # Ensure a canonical representation of the plane normal 56 for i_comp in range(3): 57 if abs(n[i_comp]) > angle_epsilon: 58 if n[i_comp] < 0: 59 n = -n 60 d = -d 61 break 62 63 # Round normal and distance for stable hashing 64 n_rounded = tuple(np.round(n, 6)) 65 d_rounded = round(d, 3) 66 67 key = (n_rounded, d_rounded) 68 if key not in plane_map: 69 plane_map[key] = [] 70 plane_map[key].append(simplex) 71 72 return plane_map 73 74 @staticmethod 75 def merge_coplanar_triangles_to_polygon(points, simplices): 76 # Extract polygon outline from coplanar triangles 77 edges = {} 78 for tri in simplices: 79 for i in range(3): 80 a = tri[i] 81 b = tri[(i + 1) % 3] 82 e = (min(a, b), max(a, b)) 83 edges[e] = edges.get(e, 0) + 1 84 85 # Keep only outer edges (appear once) 86 boundary_edges = [e for e, count in edges.items() if count == 1] 87 if not boundary_edges: 88 return [] 89 90 # Build adjacency 91 adj = {} 92 for a, b in boundary_edges: 93 adj.setdefault(a, []).append(b) 94 adj.setdefault(b, []).append(a) 95 96 # Walk along the boundary edges to form a closed loop 97 start = boundary_edges[0][0] 98 loop = [start] 99 current = start 100 prev = None 101 while True: 102 neighbors = adj[current] 103 next_vertex = None 104 for n in neighbors: 105 if n != prev: 106 next_vertex = n 107 break 108 if next_vertex is None: 109 break 110 loop.append(next_vertex) 111 prev, current = current, next_vertex 112 if next_vertex == start: 113 break 114 115 polygon_points = points[loop] 116 return polygon_points 117 118 @staticmethod 119 def compute_hull_outlines_from_geometries(child_geometries: list[Any]) -> list[np.ndarray]: 120 """Compute polygon outlines from geometry child objects. 121 122 Args: 123 child_geometries: List of geometry objects containing convex hulls 124 125 Returns: 126 List of closed polygons as Nx3 numpy arrays 127 """ 128 all_points = [] 129 for child in child_geometries: 130 if child.convex_hull is not None: 131 for v in child.convex_hull.vertices: 132 all_points.append([v.x, v.y, v.z]) 133 134 if len(all_points) < 4: 135 return [] 136 137 return HullVisualizer._compute_hull_from_points(np.array(all_points)) 138 139 @staticmethod 140 def compute_hull_outlines_from_points(points: list[list[float]]) -> list[np.ndarray]: 141 """Compute polygon outlines directly from point coordinates. 142 143 Args: 144 points: List of [x,y,z] coordinates 145 146 Returns: 147 List of closed polygons as Nx3 numpy arrays 148 """ 149 if len(points) < 4: 150 return [] 151 152 return HullVisualizer._compute_hull_from_points(np.array(points)) 153 154 @staticmethod 155 def _compute_hull_from_points(points: np.ndarray) -> list[np.ndarray]: 156 """Internal helper to compute hull from numpy points array.""" 157 try: 158 hull = ConvexHull(points) 159 plane_map = HullVisualizer.group_coplanar_triangles(points, hull) 160 161 polygons = [] 162 for simplices in plane_map.values(): 163 polygon_points = HullVisualizer.merge_coplanar_triangles_to_polygon( 164 points, simplices 165 ) 166 if len(polygon_points) > 2: 167 closed_loop = np.vstack([polygon_points, polygon_points[0]]) 168 polygons.append(closed_loop) 169 return polygons 170 171 except Exception: 172 return []
@staticmethod
def
compute_hull_mesh( polygons: list[numpy.ndarray]) -> tuple[list[list[float]], list[list[int]], list[list[float]]]:
10 @staticmethod 11 def compute_hull_mesh( 12 polygons: list[np.ndarray], 13 ) -> tuple[list[list[float]], list[list[int]], list[list[float]]]: 14 """Convert polygons to mesh with optimized hull generation.""" 15 vertices = np.vstack(polygons) 16 17 # Custom qhull options for better quality 18 qhull_opts = trimesh.convex.QhullOptions( 19 Qt=True, # Triangulated output 20 QJ=True, # Joggled input for precision 21 Qc=True, # Keep coplanar points 22 Qx=True, # Exact pre-merges 23 QbB=True, # Scale to unit cube 24 Pp=True, # Remove precision warnings 25 ) 26 27 mesh = trimesh.convex.convex_hull(vertices, qhull_options=qhull_opts, repair=True) 28 29 return (mesh.vertices.tolist(), mesh.faces.tolist(), mesh.vertex_normals.tolist())
Convert polygons to mesh with optimized hull generation.
@staticmethod
def
plane_from_triangle(p0, p1, p2, normal_epsilon=1e-06):
31 @staticmethod 32 def plane_from_triangle(p0, p1, p2, normal_epsilon=1e-6): 33 # Compute normal 34 v1 = p1 - p0 35 v2 = p2 - p0 36 n = np.cross(v1, v2) 37 norm = np.linalg.norm(n) 38 if norm < normal_epsilon: 39 return None, None 40 n = n / norm 41 # Plane: n·x = d 42 d = np.dot(n, p0) 43 return n, d
@staticmethod
def
group_coplanar_triangles(points, hull, angle_epsilon=1e-06, dist_epsilon=1e-06):
45 @staticmethod 46 def group_coplanar_triangles(points, hull, angle_epsilon=1e-6, dist_epsilon=1e-6): 47 # Group triangles by their plane (normal and distance) 48 plane_map = {} 49 for simplex in hull.simplices: 50 p0, p1, p2 = points[simplex] 51 n, d = HullVisualizer.plane_from_triangle(p0, p1, p2) 52 if n is None: 53 continue 54 55 # Ensure a canonical representation of the plane normal 56 for i_comp in range(3): 57 if abs(n[i_comp]) > angle_epsilon: 58 if n[i_comp] < 0: 59 n = -n 60 d = -d 61 break 62 63 # Round normal and distance for stable hashing 64 n_rounded = tuple(np.round(n, 6)) 65 d_rounded = round(d, 3) 66 67 key = (n_rounded, d_rounded) 68 if key not in plane_map: 69 plane_map[key] = [] 70 plane_map[key].append(simplex) 71 72 return plane_map
@staticmethod
def
merge_coplanar_triangles_to_polygon(points, simplices):
74 @staticmethod 75 def merge_coplanar_triangles_to_polygon(points, simplices): 76 # Extract polygon outline from coplanar triangles 77 edges = {} 78 for tri in simplices: 79 for i in range(3): 80 a = tri[i] 81 b = tri[(i + 1) % 3] 82 e = (min(a, b), max(a, b)) 83 edges[e] = edges.get(e, 0) + 1 84 85 # Keep only outer edges (appear once) 86 boundary_edges = [e for e, count in edges.items() if count == 1] 87 if not boundary_edges: 88 return [] 89 90 # Build adjacency 91 adj = {} 92 for a, b in boundary_edges: 93 adj.setdefault(a, []).append(b) 94 adj.setdefault(b, []).append(a) 95 96 # Walk along the boundary edges to form a closed loop 97 start = boundary_edges[0][0] 98 loop = [start] 99 current = start 100 prev = None 101 while True: 102 neighbors = adj[current] 103 next_vertex = None 104 for n in neighbors: 105 if n != prev: 106 next_vertex = n 107 break 108 if next_vertex is None: 109 break 110 loop.append(next_vertex) 111 prev, current = current, next_vertex 112 if next_vertex == start: 113 break 114 115 polygon_points = points[loop] 116 return polygon_points
@staticmethod
def
compute_hull_outlines_from_geometries(child_geometries: list[typing.Any]) -> list[numpy.ndarray]:
118 @staticmethod 119 def compute_hull_outlines_from_geometries(child_geometries: list[Any]) -> list[np.ndarray]: 120 """Compute polygon outlines from geometry child objects. 121 122 Args: 123 child_geometries: List of geometry objects containing convex hulls 124 125 Returns: 126 List of closed polygons as Nx3 numpy arrays 127 """ 128 all_points = [] 129 for child in child_geometries: 130 if child.convex_hull is not None: 131 for v in child.convex_hull.vertices: 132 all_points.append([v.x, v.y, v.z]) 133 134 if len(all_points) < 4: 135 return [] 136 137 return HullVisualizer._compute_hull_from_points(np.array(all_points))
Compute polygon outlines from geometry child objects.
Args: child_geometries: List of geometry objects containing convex hulls
Returns: List of closed polygons as Nx3 numpy arrays
@staticmethod
def
compute_hull_outlines_from_points(points: list[list[float]]) -> list[numpy.ndarray]:
139 @staticmethod 140 def compute_hull_outlines_from_points(points: list[list[float]]) -> list[np.ndarray]: 141 """Compute polygon outlines directly from point coordinates. 142 143 Args: 144 points: List of [x,y,z] coordinates 145 146 Returns: 147 List of closed polygons as Nx3 numpy arrays 148 """ 149 if len(points) < 4: 150 return [] 151 152 return HullVisualizer._compute_hull_from_points(np.array(points))
Compute polygon outlines directly from point coordinates.
Args: points: List of [x,y,z] coordinates
Returns: List of closed polygons as Nx3 numpy arrays