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