gri-plot 0.2.0.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ """Surface ABC and base utilities for 3D visualization.
2
+
3
+ This module defines the ImplicitShape ABC that all plottable shapes must implement.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+
11
+ if TYPE_CHECKING:
12
+ import plotly.graph_objects as go
13
+ from numpy.typing import NDArray
14
+
15
+
16
+ class ImplicitShape(ABC):
17
+ """Base class for all plottable 3D shapes and observables.
18
+
19
+ This ABC provides a unified interface for shapes that can be:
20
+ - Evaluated via an implicit function (residual_fn)
21
+ - Rendered as a Plotly Mesh3d trace
22
+ - Used in intersection calculations
23
+
24
+ Residual convention:
25
+ - residual < 0: inside the shape
26
+ - residual = 0: on the boundary
27
+ - residual > 0: outside the shape
28
+
29
+ The is_volume property determines intersection behavior:
30
+ - True: interior (residual < 0) is the solution region
31
+ - False: boundary (residual = 0) is the solution locus
32
+ """
33
+
34
+ @property
35
+ @abstractmethod
36
+ def is_volume(self) -> bool:
37
+ """Whether this shape represents a volume (True) or surface (False).
38
+
39
+ Volumes contribute to intersection everywhere inside (residual < 0).
40
+ Surfaces contribute based on distance from boundary (|residual|).
41
+ """
42
+ ...
43
+
44
+ @property
45
+ @abstractmethod
46
+ def label(self) -> str | None:
47
+ """Optional label for the shape in legends."""
48
+ ...
49
+
50
+ @abstractmethod
51
+ def residual_fn(self, xyz: NDArray[np.floating]) -> NDArray[np.floating]:
52
+ """Evaluate the implicit function at given points.
53
+
54
+ Args:
55
+ xyz: Points to evaluate, shape (..., 3).
56
+
57
+ Returns:
58
+ Residual values, shape (...). Negative inside, zero on boundary,
59
+ positive outside.
60
+ """
61
+ ...
62
+
63
+ @abstractmethod
64
+ def get_bounds_xyz(
65
+ self,
66
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
67
+ """Get axis-aligned bounding box in XYZ coordinates.
68
+
69
+ Returns:
70
+ Tuple of (min_corner, max_corner), each shape (3,).
71
+ """
72
+ ...
73
+
74
+ @abstractmethod
75
+ def to_mesh(
76
+ self,
77
+ resolution: int | None = None,
78
+ ) -> tuple[NDArray[np.floating], NDArray[np.integer]]:
79
+ """Generate mesh vertices and faces.
80
+
81
+ Args:
82
+ resolution: Number of samples per dimension. If None, uses
83
+ shape-specific default.
84
+
85
+ Returns:
86
+ Tuple of (vertices, faces) where:
87
+ - vertices: shape (N, 3) array of vertex positions
88
+ - faces: shape (M, 3) array of triangle indices
89
+ """
90
+ ...
91
+
92
+ @abstractmethod
93
+ def to_trace(
94
+ self,
95
+ resolution: int | None = None,
96
+ **kwargs,
97
+ ) -> go.Mesh3d:
98
+ """Generate a Plotly Mesh3d trace for visualization.
99
+
100
+ Args:
101
+ resolution: Mesh resolution. If None, uses shape-specific default.
102
+ **kwargs: Additional arguments for the trace.
103
+
104
+ Returns:
105
+ Plotly Mesh3d trace.
106
+ """
107
+ ...
108
+
109
+ def contains(self, xyz: NDArray[np.floating]) -> NDArray[np.bool_]:
110
+ """Test if points are inside the shape.
111
+
112
+ Args:
113
+ xyz: Points to test, shape (..., 3).
114
+
115
+ Returns:
116
+ Boolean array, shape (...). True if inside (residual < 0).
117
+ """
118
+ return self.residual_fn(xyz) < 0
119
+
120
+
121
+ def merge_bounds(
122
+ *bounds: tuple[NDArray[np.floating], NDArray[np.floating]],
123
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
124
+ """Merge multiple bounding boxes into one.
125
+
126
+ Args:
127
+ *bounds: Tuples of (min_corner, max_corner), each shape (3,).
128
+
129
+ Returns:
130
+ Tuple of (min_corner, max_corner) encompassing all inputs.
131
+
132
+ Raises:
133
+ ValueError: If no bounds are provided.
134
+ """
135
+ if not bounds:
136
+ raise ValueError("At least one bounds tuple required")
137
+
138
+ all_mins = np.array([b[0] for b in bounds])
139
+ all_maxs = np.array([b[1] for b in bounds])
140
+
141
+ return all_mins.min(axis=0), all_maxs.max(axis=0)
142
+
143
+
144
+ def expand_bounds(
145
+ bounds: tuple[NDArray[np.floating], NDArray[np.floating]],
146
+ factor: float = 1.1,
147
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
148
+ """Expand bounds by a factor around their center.
149
+
150
+ Args:
151
+ bounds: Tuple of (min_corner, max_corner), each shape (3,).
152
+ factor: Expansion factor (1.0 = no change, 1.1 = 10% expansion).
153
+
154
+ Returns:
155
+ Expanded bounds tuple.
156
+ """
157
+ min_corner, max_corner = np.asarray(bounds[0]), np.asarray(bounds[1])
158
+ center = (min_corner + max_corner) / 2
159
+ half_extent = (max_corner - min_corner) / 2
160
+
161
+ return center - half_extent * factor, center + half_extent * factor
162
+
163
+
164
+ from .intersection import IntersectionField # noqa: E402
165
+
166
+ __all__ = [
167
+ "ImplicitShape",
168
+ "IntersectionField",
169
+ "expand_bounds",
170
+ "merge_bounds",
171
+ ]
@@ -0,0 +1,39 @@
1
+ """Intensity functions for mesh coloring.
2
+
3
+ This module provides factory functions that create intensity calculators
4
+ for coloring 3D mesh vertices. Each function returns a callable that takes
5
+ a vertex array and returns intensity values.
6
+
7
+ Example::
8
+
9
+ from gri_plot.surfaces.gradients import distance_from_axis, distance_from_line
10
+ from gri_plot.surfaces.mesh import vertices_to_mesh3d
11
+
12
+ # Color by distance from Z=0 (axis 2)
13
+ trace = vertices_to_mesh3d(
14
+ verts, faces,
15
+ intensity_fn=distance_from_axis(2, center=0.0),
16
+ colorbar_title="|Z| (m)",
17
+ )
18
+
19
+ # Color by distance from baseline between two collectors
20
+ trace = vertices_to_mesh3d(
21
+ verts, faces,
22
+ intensity_fn=distance_from_line(c1, c2),
23
+ colorbar_title="Baseline distance (m)",
24
+ )
25
+ """
26
+
27
+ from .axis import axis_value, distance_from_axis
28
+ from .line import distance_from_line, distance_from_line_segment
29
+ from .plane import distance_from_plane
30
+ from .point import distance_from_point
31
+
32
+ __all__ = [
33
+ "axis_value",
34
+ "distance_from_axis",
35
+ "distance_from_line",
36
+ "distance_from_line_segment",
37
+ "distance_from_plane",
38
+ "distance_from_point",
39
+ ]
@@ -0,0 +1,74 @@
1
+ """Axis-based intensity functions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from numpy.typing import NDArray
11
+
12
+
13
+ def axis_value(
14
+ axis: int = 2,
15
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
16
+ """Create function that returns raw axis coordinate value.
17
+
18
+ Args:
19
+ axis: Which axis index (0, 1, or 2 for X, Y, Z respectively).
20
+
21
+ Returns:
22
+ Function that maps vertices (N, 3) to coordinate values (N,).
23
+
24
+ Example::
25
+
26
+ # Color vertices by their Z coordinate
27
+ intensity_fn = axis_value(2)
28
+ intensities = intensity_fn(vertices)
29
+ """
30
+ if axis not in (0, 1, 2):
31
+ msg = f"axis must be 0, 1, or 2, got {axis}"
32
+ raise ValueError(msg)
33
+
34
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
35
+ return vertices[:, axis]
36
+
37
+ return fn
38
+
39
+
40
+ def distance_from_axis(
41
+ axis: int = 2,
42
+ center: float = 0.0,
43
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
44
+ """Create function that computes distance from an axis value.
45
+
46
+ This produces symmetric coloring around a reference value. Useful for
47
+ showing distance from a ground plane (Z=0) or similar reference.
48
+
49
+ Args:
50
+ axis: Which axis index (0, 1, or 2 for X, Y, Z respectively).
51
+ center: The reference value on that axis.
52
+
53
+ Returns:
54
+ Function that maps vertices (N, 3) to |axis_value - center| (N,).
55
+
56
+ Example::
57
+
58
+ # Color by distance from ground (Z=0)
59
+ intensity_fn = distance_from_axis(2, center=0.0)
60
+
61
+ # Color by distance from X=1000
62
+ intensity_fn = distance_from_axis(0, center=1000.0)
63
+ """
64
+ if axis not in (0, 1, 2):
65
+ msg = f"axis must be 0, 1, or 2, got {axis}"
66
+ raise ValueError(msg)
67
+
68
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
69
+ return np.abs(vertices[:, axis] - center)
70
+
71
+ return fn
72
+
73
+
74
+ __all__ = ["axis_value", "distance_from_axis"]
@@ -0,0 +1,86 @@
1
+ """Line-based intensity functions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from numpy.typing import NDArray
11
+
12
+
13
+ def distance_from_line(
14
+ point1: NDArray[np.floating],
15
+ point2: NDArray[np.floating],
16
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
17
+ """Create function that computes perpendicular distance from an infinite line.
18
+
19
+ The line extends infinitely in both directions through the two points.
20
+
21
+ Args:
22
+ point1: First point on line, shape (3,).
23
+ point2: Second point on line, shape (3,).
24
+
25
+ Returns:
26
+ Function that maps vertices (N, 3) to perpendicular distances (N,).
27
+
28
+ Example::
29
+
30
+ # Color by distance from baseline between two collectors
31
+ intensity_fn = distance_from_line(collector1_xyz, collector2_xyz)
32
+ """
33
+ p1 = np.asarray(point1, dtype=np.float64)
34
+ p2 = np.asarray(point2, dtype=np.float64)
35
+ line_vec = p2 - p1
36
+ line_len = np.linalg.norm(line_vec)
37
+ line_dir = line_vec / line_len
38
+
39
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
40
+ to_vert = vertices - p1
41
+ # Project onto line direction
42
+ proj_len = np.dot(to_vert, line_dir)
43
+ proj_point = p1 + np.outer(proj_len, line_dir)
44
+ return np.linalg.norm(vertices - proj_point, axis=1)
45
+
46
+ return fn
47
+
48
+
49
+ def distance_from_line_segment(
50
+ point1: NDArray[np.floating],
51
+ point2: NDArray[np.floating],
52
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
53
+ """Create function that computes distance from a line segment.
54
+
55
+ Unlike distance_from_line, this clamps to the segment endpoints.
56
+ Points beyond the endpoints measure distance to the nearest endpoint.
57
+
58
+ Args:
59
+ point1: Start of segment, shape (3,).
60
+ point2: End of segment, shape (3,).
61
+
62
+ Returns:
63
+ Function that maps vertices (N, 3) to distances (N,).
64
+
65
+ Example::
66
+
67
+ # Color by distance from a finite baseline segment
68
+ intensity_fn = distance_from_line_segment(start_xyz, end_xyz)
69
+ """
70
+ p1 = np.asarray(point1, dtype=np.float64)
71
+ p2 = np.asarray(point2, dtype=np.float64)
72
+ line_vec = p2 - p1
73
+ line_len_sq = np.dot(line_vec, line_vec)
74
+
75
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
76
+ to_vert = vertices - p1
77
+ # Parameter t along segment, clamped to [0, 1]
78
+ t = np.dot(to_vert, line_vec) / line_len_sq
79
+ t = np.clip(t, 0, 1)
80
+ proj_point = p1 + np.outer(t, line_vec)
81
+ return np.linalg.norm(vertices - proj_point, axis=1)
82
+
83
+ return fn
84
+
85
+
86
+ __all__ = ["distance_from_line", "distance_from_line_segment"]
@@ -0,0 +1,53 @@
1
+ """Plane-based intensity functions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from numpy.typing import NDArray
11
+
12
+
13
+ def distance_from_plane(
14
+ normal: NDArray[np.floating],
15
+ point_on_plane: NDArray[np.floating],
16
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
17
+ """Create function that computes absolute distance from a plane.
18
+
19
+ The plane is defined by a normal vector and a point on the plane.
20
+
21
+ Args:
22
+ normal: Plane normal vector (will be normalized), shape (3,).
23
+ point_on_plane: Any point on the plane, shape (3,).
24
+
25
+ Returns:
26
+ Function that maps vertices (N, 3) to absolute distances (N,).
27
+
28
+ Example::
29
+
30
+ # Color by height above ground (horizontal plane at Z=0)
31
+ intensity_fn = distance_from_plane(
32
+ normal=np.array([0, 0, 1]),
33
+ point_on_plane=np.array([0, 0, 0]),
34
+ )
35
+
36
+ # Color by distance from a tilted reference plane
37
+ intensity_fn = distance_from_plane(
38
+ normal=np.array([1, 1, 1]), # Will be normalized
39
+ point_on_plane=center_xyz,
40
+ )
41
+ """
42
+ normal = np.asarray(normal, dtype=np.float64)
43
+ normal = normal / np.linalg.norm(normal)
44
+ point_on_plane = np.asarray(point_on_plane, dtype=np.float64)
45
+ d = -np.dot(normal, point_on_plane)
46
+
47
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
48
+ return np.abs(np.dot(vertices, normal) + d)
49
+
50
+ return fn
51
+
52
+
53
+ __all__ = ["distance_from_plane"]
@@ -0,0 +1,40 @@
1
+ """Point-based intensity functions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from numpy.typing import NDArray
11
+
12
+
13
+ def distance_from_point(
14
+ point: NDArray[np.floating],
15
+ ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]:
16
+ """Create function that computes Euclidean distance from a point.
17
+
18
+ Args:
19
+ point: Reference point, shape (3,).
20
+
21
+ Returns:
22
+ Function that maps vertices (N, 3) to distances (N,).
23
+
24
+ Example::
25
+
26
+ # Color by distance from origin
27
+ intensity_fn = distance_from_point(np.array([0, 0, 0]))
28
+
29
+ # Color by distance from sensor location
30
+ intensity_fn = distance_from_point(sensor_xyz)
31
+ """
32
+ point = np.asarray(point, dtype=np.float64)
33
+
34
+ def fn(vertices: NDArray[np.floating]) -> NDArray[np.floating]:
35
+ return np.linalg.norm(vertices - point, axis=1)
36
+
37
+ return fn
38
+
39
+
40
+ __all__ = ["distance_from_point"]
@@ -0,0 +1,217 @@
1
+ """Intersection field for visualizing where multiple implicit shapes meet.
2
+
3
+ This module provides the IntersectionField class for highlighting regions
4
+ where multiple implicit shapes intersect. The intersection is computed using
5
+ Euclidean distance in residual space, giving a smooth tube-like volume around
6
+ the intersection curve/region.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import numpy as np
12
+
13
+ # Import directly to avoid circular import
14
+ from gri_plot.surfaces import merge_bounds
15
+
16
+ if TYPE_CHECKING:
17
+ from gri_plot.surfaces import ImplicitShape
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+
22
+ from numpy.typing import NDArray
23
+
24
+
25
+ class IntersectionField:
26
+ """Combined field showing where multiple implicit shapes intersect.
27
+
28
+ Computes the Euclidean distance to the intersection of multiple shapes.
29
+ For surfaces, this is sqrt(sum of squared residuals), giving a smooth
30
+ tube-like volume around the intersection curve. A radius parameter
31
+ controls the tube thickness.
32
+
33
+ Shapes are treated differently based on their is_volume property:
34
+ - Surfaces (is_volume=False): Use |residual| as distance to surface
35
+ - Volumes (is_volume=True): Use max(0, residual) as distance outside
36
+
37
+ Attributes:
38
+ shapes: The implicit shapes being intersected.
39
+ radius: Tube radius - distance from intersection where residual = 0.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ shapes: Sequence[ImplicitShape],
45
+ radius: float | None = None,
46
+ # Legacy parameters for backward compatibility
47
+ threshold: float | None = None,
48
+ steepness: float | None = None,
49
+ ) -> None:
50
+ """Initialize the intersection field.
51
+
52
+ Args:
53
+ shapes: Sequence of ImplicitShape objects to intersect.
54
+ Must have at least 2 shapes.
55
+ radius: Tube radius around the intersection. Points within this
56
+ distance of the intersection curve have negative residual.
57
+ If None, auto-computed as 5% of bounds diagonal.
58
+ threshold: Deprecated, use radius instead.
59
+ steepness: Deprecated, ignored.
60
+
61
+ Raises:
62
+ ValueError: If fewer than 2 shapes provided.
63
+ """
64
+ if len(shapes) < 2: # noqa: PLR2004 - intersection requires >= 2 shapes
65
+ raise ValueError("IntersectionField requires at least 2 shapes")
66
+
67
+ self._shapes = list(shapes)
68
+
69
+ # Compute characteristic scale from bounds
70
+ bounds = self._compute_bounds()
71
+ extent = bounds[1] - bounds[0]
72
+ self._scale = float(np.linalg.norm(extent))
73
+ if self._scale == 0:
74
+ self._scale = 1.0
75
+
76
+ # Handle radius - either explicit or auto-computed
77
+ # The radius is in "residual space" - the same units as residual_fn outputs.
78
+ # For normalized residuals (like TDOA), typical values are 0.01-0.5.
79
+ # For spatial residuals (like distance to sphere), values are in meters.
80
+ if radius is not None:
81
+ self._radius = float(radius)
82
+ else:
83
+ # Default: 0.1 in residual space
84
+ # This works well for normalized residuals (TDOA, etc.)
85
+ # For spatial residuals, user should specify radius explicitly
86
+ self._radius = 0.1
87
+
88
+ # Keep for backward compatibility (though not used in new approach)
89
+ self._threshold = threshold if threshold is not None else 1.5
90
+ self._steepness = steepness if steepness is not None else 9.0
91
+
92
+ self._label: str | None = None
93
+
94
+ @property
95
+ def shapes(self) -> list[ImplicitShape]:
96
+ """Get the list of shapes."""
97
+ return self._shapes
98
+
99
+ # Backward compatibility alias
100
+ @property
101
+ def surfaces(self) -> list[ImplicitShape]:
102
+ """Get the list of shapes (deprecated, use shapes instead)."""
103
+ return self._shapes
104
+
105
+ @property
106
+ def radius(self) -> float:
107
+ """Get the tube radius."""
108
+ return self._radius
109
+
110
+ @property
111
+ def threshold(self) -> float:
112
+ """Get the threshold value (deprecated)."""
113
+ return self._threshold
114
+
115
+ @property
116
+ def steepness(self) -> float:
117
+ """Get the steepness parameter (deprecated)."""
118
+ return self._steepness
119
+
120
+ @property
121
+ def label(self) -> str | None:
122
+ """Get the optional label."""
123
+ return self._label
124
+
125
+ @label.setter
126
+ def label(self, value: str | None) -> None:
127
+ """Set the label."""
128
+ self._label = value
129
+
130
+ def distance_to_intersection(
131
+ self,
132
+ xyz: NDArray[np.floating],
133
+ ) -> NDArray[np.floating]:
134
+ """Compute Euclidean distance to the intersection.
135
+
136
+ For surfaces, computes sqrt(sum of squared residuals), which gives
137
+ the distance to the intersection curve/region in residual space.
138
+
139
+ For volumes, uses max(0, residual) so points inside contribute 0.
140
+
141
+ Args:
142
+ xyz: Points to evaluate, shape (..., 3).
143
+
144
+ Returns:
145
+ Distance values, shape (...). Zero exactly on the intersection,
146
+ positive elsewhere.
147
+ """
148
+ xyz = np.asarray(xyz)
149
+ sum_sq = np.zeros(xyz.shape[:-1], dtype=np.float64)
150
+
151
+ for shape in self._shapes:
152
+ residual = shape.residual_fn(xyz)
153
+
154
+ if shape.is_volume:
155
+ # Volume: only count distance if outside (residual > 0)
156
+ distance = np.maximum(0.0, residual)
157
+ else:
158
+ # Surface: distance is |residual|
159
+ distance = np.abs(residual)
160
+
161
+ sum_sq += distance * distance
162
+
163
+ return np.sqrt(sum_sq)
164
+
165
+ def residual_fn(self, xyz: NDArray[np.floating]) -> NDArray[np.floating]:
166
+ """Compute the intersection field residual.
167
+
168
+ Returns distance_to_intersection - radius, so:
169
+ - Negative inside the tube (within radius of intersection)
170
+ - Zero on the tube surface
171
+ - Positive outside the tube
172
+
173
+ Args:
174
+ xyz: Points to evaluate, shape (..., 3).
175
+
176
+ Returns:
177
+ Residual values, shape (...).
178
+ """
179
+ return self.distance_to_intersection(xyz) - self._radius
180
+
181
+ # Keep for backward compatibility
182
+ def combined_closeness(self, xyz: NDArray[np.floating]) -> NDArray[np.floating]:
183
+ """Compute closeness to intersection (deprecated).
184
+
185
+ This method is kept for backward compatibility but now uses the
186
+ Euclidean distance approach internally.
187
+
188
+ Args:
189
+ xyz: Points to evaluate, shape (..., 3).
190
+
191
+ Returns:
192
+ Closeness values based on distance to intersection.
193
+ """
194
+ distance = self.distance_to_intersection(xyz)
195
+ # Convert distance to closeness using exponential falloff
196
+ # Normalize by radius so closeness ~ 1 at the intersection
197
+ return np.exp(-distance / self._radius)
198
+
199
+ def _compute_bounds(
200
+ self,
201
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
202
+ """Compute combined bounding box from all shapes."""
203
+ all_bounds = [s.get_bounds_xyz() for s in self._shapes]
204
+ return merge_bounds(*all_bounds)
205
+
206
+ def get_bounds_xyz(
207
+ self,
208
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
209
+ """Get combined bounding box from all shapes.
210
+
211
+ Returns:
212
+ Tuple of (min_corner, max_corner) encompassing all shapes.
213
+ """
214
+ return self._compute_bounds()
215
+
216
+
217
+ __all__ = ["IntersectionField"]