From 452eec351d2e9d299d56c2f73a649061184e3259 Mon Sep 17 00:00:00 2001 From: Jay Gupta <66860950+JayGupta797@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:31:17 -0700 Subject: [PATCH 1/4] initial commit --- manim/mobject/geometry/labeled.py | 284 ++++++++++++++++-- manim/mobject/geometry/polygram.py | 53 ++++ manim/mobject/three_d/polyhedra.py | 98 +++++- manim/utils/polylabel.py | 110 +++++++ manim/utils/qhull.py | 162 ++++++++++ .../control_data/geometry/ConvexHull.npz | Bin 0 -> 8945 bytes .../control_data/geometry/LabeledPolygram.npz | Bin 0 -> 5029 bytes tests/test_graphical_units/test_geometry.py | 31 ++ 8 files changed, 704 insertions(+), 34 deletions(-) create mode 100644 manim/utils/polylabel.py create mode 100644 manim/utils/qhull.py create mode 100644 tests/test_graphical_units/control_data/geometry/ConvexHull.npz create mode 100644 tests/test_graphical_units/control_data/geometry/LabeledPolygram.npz diff --git a/manim/mobject/geometry/labeled.py b/manim/mobject/geometry/labeled.py index 1a39ee2771..a333b2b076 100644 --- a/manim/mobject/geometry/labeled.py +++ b/manim/mobject/geometry/labeled.py @@ -2,17 +2,102 @@ from __future__ import annotations -__all__ = ["LabeledLine", "LabeledArrow"] +__all__ = ["Label", "LabeledLine", "LabeledArrow", "LabeledPolygram"] + +import numpy as np from manim.constants import * from manim.mobject.geometry.line import Arrow, Line +from manim.mobject.geometry.polygram import Polygram from manim.mobject.geometry.shape_matchers import ( BackgroundRectangle, SurroundingRectangle, ) from manim.mobject.text.tex_mobject import MathTex, Tex from manim.mobject.text.text_mobject import Text +from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.color import WHITE, ManimColor, ParsableManimColor +from manim.utils.polylabel import PolyLabel + + +class Label(VGroup): + """A Label consisting of text and frame + + Parameters + ---------- + label : str | Tex | MathTex | Text + Label that will be displayed on the line. + font_size : float | optional + Control font size for the label. This parameter is only used when `label` is of type `str`. + label_color: ParsableManimColor | optional + The color of the label's text. This parameter is only used when `label` is of type `str`. + label_frame : Bool | optional + Add a `SurroundingRectangle` frame to the label box. + frame_fill_color : ParsableManimColor | optional + Background color to fill the label box. If no value is provided, the background color of the canvas will be used. + frame_fill_opacity : float | optional + Determine the opacity of the label box by passing a value in the range [0-1], where 0 indicates complete transparency and 1 means full opacity. + + Examples + -------- + .. manim:: LabelExample + :save_last_frame: + + class LabelExample(Scene): + def construct(self): + label = Label( + label = 'Text', + font_size = 20, + label_color = WHITE, + label_frame = True + ) + label.move_to(UP) + self.add(label) + """ + + def __init__( + self, + label: str | Tex | MathTex | Text, + font_size: float = DEFAULT_FONT_SIZE, + label_color: ParsableManimColor = WHITE, + label_frame: bool = True, + frame_fill_color: ParsableManimColor = None, + frame_fill_opacity: float = 1, + **kwargs, + ) -> None: + super().__init__(**kwargs) + + label_color = ManimColor(label_color) + frame_fill_color = ManimColor(frame_fill_color) + + # Determine the type of label and instantiate the appropriate object + if isinstance(label, str): + self.rendered_label = MathTex(label, color=label_color, font_size=font_size) + elif isinstance(label, (MathTex, Tex, Text)): + self.rendered_label = label + else: + raise ValueError("Unsupported label type. Must be MathTex, Tex, or Text.") + + # Add background box + self.background_rect = BackgroundRectangle( + self.rendered_label, + buff=0.05, + color=frame_fill_color, + fill_opacity=frame_fill_opacity, + stroke_width=0.5, + ) + + # Optionally add a frame around the label + self.frame = None + if label_frame: + self.frame = SurroundingRectangle( + self.rendered_label, buff=0.05, color=label_color, stroke_width=0.5 + ) + + # Add components to the VGroup + self.add(self.background_rect, self.rendered_label) + if self.frame: + self.add(self.frame) class LabeledLine(Line): @@ -51,11 +136,9 @@ def construct(self): font_size = 20, label_color = WHITE, label_frame = True, - start=LEFT+DOWN, end=RIGHT+UP) - line.set_length(line.get_length() * 2) self.add(line) """ @@ -72,42 +155,25 @@ def __init__( *args, **kwargs, ) -> None: - label_color = ManimColor(label_color) - frame_fill_color = ManimColor(frame_fill_color) - if isinstance(label, str): - from manim import MathTex - - rendered_label = MathTex(label, color=label_color, font_size=font_size) - else: - rendered_label = label - super().__init__(*args, **kwargs) - # calculating the vector for the label position + # Create Label + self.label = Label( + label=label, + font_size=font_size, + label_color=label_color, + label_frame=label_frame, + frame_fill_color=frame_fill_color, + frame_fill_opacity=frame_fill_opacity, + ) + + # Compute Label Position line_start, line_end = self.get_start_and_end() new_vec = (line_end - line_start) * label_position label_coords = line_start + new_vec - # rendered_label.move_to(self.get_vector() * label_position) - rendered_label.move_to(label_coords) - - box = BackgroundRectangle( - rendered_label, - buff=0.05, - color=frame_fill_color, - fill_opacity=frame_fill_opacity, - stroke_width=0.5, - ) - self.add(box) - - if label_frame: - box_frame = SurroundingRectangle( - rendered_label, buff=0.05, color=label_color, stroke_width=0.5 - ) - - self.add(box_frame) - - self.add(rendered_label) + self.label.move_to(label_coords) + self.add(self.label) class LabeledArrow(LabeledLine, Arrow): @@ -153,3 +219,155 @@ def __init__( **kwargs, ) -> None: super().__init__(*args, **kwargs) + + +class LabeledPolygram(Polygram): + """Constructs a polygram containing a label box at its pole of inaccessibility. + + Parameters + ---------- + label : str | Tex | MathTex | Text + Label that will be displayed on the line. + precision : float | optional + The precision used by the PolyLabel algorithm. + font_size : float | optional + Control font size for the label. This parameter is only used when `label` is of type `str`. + label_color: ParsableManimColor | optional + The color of the label's text. This parameter is only used when `label` is of type `str`. + label_frame : Bool | optional + Add a `SurroundingRectangle` frame to the label box. + frame_fill_color : ParsableManimColor | optional + Background color to fill the label box. If no value is provided, the background color of the canvas will be used. + frame_fill_opacity : float | optional + Determine the opacity of the label box by passing a value in the range [0-1], where 0 indicates complete transparency and 1 means full opacity. + + + .. note:: + The PolyLabel Algorithm expects each vertex group to form a closed ring. + If the input is open, LabeledPolygram will attempt to close it. + This may cause the polygon to intersect itself leading to unexpected results. + + Examples + -------- + .. manim:: LabeledPolygramExample + :save_last_frame: + + class LabeledPolygramExample(Scene): + def construct(self): + # Define Rings + ring1 = [ + [-3.8, -2.4, 0], [-2.4, -2.5, 0], [-1.3, -1.6, 0], [-0.2, -1.7, 0], + [1.7, -2.5, 0], [2.9, -2.6, 0], [3.5, -1.5, 0], [4.9, -1.4, 0], + [4.5, 0.2, 0], [4.7, 1.6, 0], [3.5, 2.4, 0], [1.1, 2.5, 0], + [-0.1, 0.9, 0], [-1.2, 0.5, 0], [-1.6, 0.7, 0], [-1.4, 1.9, 0], + [-2.6, 2.6, 0], [-4.4, 1.2, 0], [-4.9, -0.8, 0], [-3.8, -2.4, 0] + ] + ring2 = [ + [0.2, -1.2, 0], [0.9, -1.2, 0], [1.4, -2.0, 0], [2.1, -1.6, 0], + [2.2, -0.5, 0], [1.4, 0.0, 0], [0.4, -0.2, 0], [0.2, -1.2, 0] + ] + ring3 = [[-2.7, 1.4, 0], [-2.3, 1.7, 0], [-2.8, 1.9, 0], [-2.7, 1.4, 0]] + + # Create Polygons (for reference) + p1 = Polygon(*ring1, fill_opacity=0.75) + p2 = Polygon(*ring2, fill_color=BLACK, fill_opacity=1) + p3 = Polygon(*ring3, fill_color=BLACK, fill_opacity=1) + + # Create Labeled Polygram + polygram = LabeledPolygram( + *[ring1, ring2, ring3], + label=Text('Pole', font='sans-serif'), + precision=0.01, + ) + + # Display Circle (for reference) + circle = Circle(radius=polygram.radius, color=WHITE).move_to(polygram.pole) + + self.add(p1, p2, p3) + self.add(polygram) + self.add(circle) + + .. manim:: LabeledCountryExample + :save_last_frame: + + import requests + import json + + class LabeledCountryExample(Scene): + def construct(self): + # Fetch JSON data and process arcs + data = requests.get('https://cdn.jsdelivr.net/npm/us-atlas@3/nation-10m.json').json() + arcs, transform = data['arcs'], data['transform'] + sarcs = [np.cumsum(arc, axis=0) * transform['scale'] + transform['translate'] for arc in arcs] + ssarcs = sorted(sarcs, key=len, reverse=True)[:1] + + # Compute Bounding Box + points = np.concatenate(ssarcs) + mins, maxs = np.min(points, axis=0), np.max(points, axis=0) + + # Build Axes + ax = Axes( + x_range=[mins[0], maxs[0], maxs[0] - mins[0]], x_length=10, + y_range=[mins[1], maxs[1], maxs[1] - mins[1]], y_length=7, + tips=False + ) + + # Adjust Coordinates + array = [[ax.c2p(*point) for point in sarc] for sarc in ssarcs] + + # Add Polygram + polygram = LabeledPolygram( + *array, + label=Text('USA', font='sans-serif'), + precision=0.01, + fill_color=BLUE, + stroke_width=0, + fill_opacity=0.75 + ) + + # Display Circle (for reference) + circle = Circle(radius=polygram.radius, color=WHITE).move_to(polygram.pole) + + self.add(ax) + self.add(polygram) + self.add(circle) + """ + + def __init__( + self, + *vertex_groups: Point3D, + label: str | Tex | MathTex | Text, + precision: float = 0.01, + font_size: float = DEFAULT_FONT_SIZE, + label_color: ParsableManimColor = WHITE, + label_frame: bool = True, + frame_fill_color: ParsableManimColor = None, + frame_fill_opacity: float = 1, + **kwargs, + ) -> None: + # Initialize the Polygram with the vertex groups + super().__init__(*vertex_groups, **kwargs) + + # Create Label + self.label = Label( + label=label, + font_size=font_size, + label_color=label_color, + label_frame=label_frame, + frame_fill_color=frame_fill_color, + frame_fill_opacity=frame_fill_opacity, + ) + + # Close Vertex Groups + rings = [ + group if np.array_equal(group[0], group[-1]) else group + [group[0]] + for group in vertex_groups + ] + + # Compute the Pole of Inaccessibility + cell = PolyLabel(rings, precision=precision) + self.pole, self.radius = np.pad(cell.c, (0, 1), "constant"), cell.d + + # Position the label at the pole + self.label.move_to(self.pole) + self.add(self.label) diff --git a/manim/mobject/geometry/polygram.py b/manim/mobject/geometry/polygram.py index 78f54cf87a..df849af89c 100644 --- a/manim/mobject/geometry/polygram.py +++ b/manim/mobject/geometry/polygram.py @@ -13,6 +13,7 @@ "Square", "RoundedRectangle", "Cutout", + "ConvexHull", ] @@ -27,6 +28,7 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.utils.color import BLUE, WHITE, ParsableManimColor from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs +from manim.utils.qhull import QuickHull from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices if TYPE_CHECKING: @@ -764,3 +766,54 @@ def __init__(self, main_shape: VMobject, *mobjects: VMobject, **kwargs) -> None: sub_direction = "CCW" if main_shape.get_direction() == "CW" else "CW" for mobject in mobjects: self.append_points(mobject.force_direction(sub_direction).points) + + +class ConvexHull(Polygram): + """Constructs a convex hull for a set of points in no particular order. + + Parameters + ---------- + points + The points to consider. + tolerance + The tolerance used by quickhull. + kwargs + Forwarded to the parent constructor. + + Examples + -------- + .. manim:: ConvexHullExample + :save_last_frame: + + class ConvexHullExample(Scene): + def construct(self): + points = np.random.uniform(low=-3, high=3, size=(100, 3)) + points[:,2] = 0 + hull = ConvexHull(*points, color=BLUE) + dots = VGroup(*[Dot(point) for point in points]) + self.add(hull) + self.add(dots) + """ + + def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs) -> None: + # Build Convex Hull + array = np.array(points)[:, :2] + hull = QuickHull(tolerance) + hull.build(array) + + # Extract Vertices + facets = set(hull.facets) - hull.removed + facet = facets.pop() + subfacets = list(facet.subfacets) + while len(subfacets) <= len(facets): + sf = subfacets[-1] + (facet,) = hull.neighbors[sf] - {facet} + (sf,) = facet.subfacets - {sf} + subfacets.append(sf) + + # Setup Vertices as Point3D + coordinates = np.vstack([sf.coordinates for sf in subfacets]) + vertices = np.hstack((coordinates, np.zeros((len(coordinates), 1)))) + + # Call Polygram + super().__init__(vertices, **kwargs) diff --git a/manim/mobject/three_d/polyhedra.py b/manim/mobject/three_d/polyhedra.py index 300cf660a8..9d0ff387c5 100644 --- a/manim/mobject/three_d/polyhedra.py +++ b/manim/mobject/three_d/polyhedra.py @@ -10,11 +10,20 @@ from manim.mobject.graph import Graph from manim.mobject.three_d.three_dimensions import Dot3D from manim.mobject.types.vectorized_mobject import VGroup +from manim.utils.qhull import QuickHull if TYPE_CHECKING: from manim.mobject.mobject import Mobject + from manim.typing import Point3D -__all__ = ["Polyhedron", "Tetrahedron", "Octahedron", "Icosahedron", "Dodecahedron"] +__all__ = [ + "Polyhedron", + "Tetrahedron", + "Octahedron", + "Icosahedron", + "Dodecahedron", + "ConvexHull3D", +] class Polyhedron(VGroup): @@ -361,3 +370,90 @@ def __init__(self, edge_length: float = 1, **kwargs): ], **kwargs, ) + + +class ConvexHull3D(Polyhedron): + """A convex hull for a set of points + + Parameters + ---------- + points + The points to consider. + tolerance + The tolerance used for quickhull. + kwargs + Forwarded to the parent constructor. + + Examples + -------- + .. manim:: ConvexHull3DExample + :save_last_frame: + + class ConvexHull3DExample(ThreeDScene): + def construct(self): + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + points = [ + [ 1.93192757, 0.44134585, -1.52407061], + [-0.93302521, 1.23206983, 0.64117067], + [-0.44350918, -0.61043677, 0.21723705], + [-0.42640268, -1.05260843, 1.61266094], + [-1.84449637, 0.91238739, -1.85172623], + [ 1.72068132, -0.11880457, 0.51881751], + [ 0.41904805, 0.44938012, -1.86440686], + [ 0.83864666, 1.66653337, 1.88960123], + [ 0.22240514, -0.80986286, 1.34249326], + [-1.29585759, 1.01516189, 0.46187522], + [ 1.7776499, -1.59550796, -1.70240747], + [ 0.80065226, -0.12530398, 1.70063977], + [ 1.28960948, -1.44158255, 1.39938582], + [-0.93538943, 1.33617705, -0.24852643], + [-1.54868271, 1.7444399, -0.46170734] + ] + hull = ConvexHull3D( + *points, + faces_config = {"stroke_opacity": 0}, + graph_config = { + "vertex_type": Dot3D, + "edge_config": { + "stroke_color": BLUE, + "stroke_width": 2, + "stroke_opacity": 0.05, + } + } + ) + dots = VGroup(*[Dot3D(point) for point in points]) + self.add(hull) + self.add(dots) + """ + + def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs): + # Build Convex Hull + array = np.array(points) + hull = QuickHull(tolerance) + hull.build(array) + + # Setup Lists + vertices = [] + faces = [] + + # Extract Faces + c = 0 + d = {} + facets = set(hull.facets) - hull.removed + for facet in facets: + tmp = set() + for subfacet in facet.subfacets: + for point in subfacet.points: + if point not in d: + vertices.append(point.coordinates) + d[point] = c + c += 1 + tmp.add(point) + faces.append([d[point] for point in tmp]) + + # Call Polyhedron + super().__init__( + vertex_coords=vertices, + faces_list=faces, + **kwargs, + ) diff --git a/manim/utils/polylabel.py b/manim/utils/polylabel.py new file mode 100644 index 0000000000..5f22153f10 --- /dev/null +++ b/manim/utils/polylabel.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +from __future__ import annotations + +from queue import PriorityQueue + +import numpy as np + + +class Polygon: + """Polygon class to compute and store associated data.""" + + def __init__(self, rings): + # Flatten Array + csum = np.cumsum([ring.shape[0] for ring in rings]) + self.array = np.concatenate(rings, axis=0) + + # Compute Boundary + self.start = np.delete(self.array, csum - 1, axis=0) + self.stop = np.delete(self.array, csum % csum[-1], axis=0) + self.diff = np.delete(np.diff(self.array, axis=0), csum[:-1] - 1, axis=0) + self.norm = self.diff / np.einsum("ij,ij->i", self.diff, self.diff).reshape( + -1, 1 + ) + + # Compute Centroid + x, y = self.start[:, 0], self.start[:, 1] + xr, yr = self.stop[:, 0], self.stop[:, 1] + self.area = 0.5 * (np.dot(x, yr) - np.dot(xr, y)) + if self.area: + factor = x * yr - xr * y + cx = np.sum((x + xr) * factor) / (6.0 * self.area) + cy = np.sum((y + yr) * factor) / (6.0 * self.area) + self.centroid = np.array([cx, cy]) + + def compute_distance(self, point): + """Compute the minimum distance from a point to the polygon.""" + scalars = np.einsum("ij,ij->i", self.norm, point - self.start) + clips = np.clip(scalars, 0, 1).reshape(-1, 1) + d = np.min(np.linalg.norm(self.start + self.diff * clips - point, axis=1)) + return d if self.inside(point) else -d + + def inside(self, point): + """Check if a point is inside the polygon.""" + # Views + px, py = point + x, y = self.start[:, 0], self.start[:, 1] + xr, yr = self.stop[:, 0], self.stop[:, 1] + + # Count Crossings (enforce short-circuit) + c = (y > py) != (yr > py) + c = px < x[c] + (py - y[c]) * (xr[c] - x[c]) / (yr[c] - y[c]) + return np.sum(c) % 2 == 1 + + +class Cell: + """Cell class to represent a square in the grid covering the polygon.""" + + def __init__(self, c, h, polygon): + self.c = c + self.h = h + self.d = polygon.compute_distance(self.c) + self.p = self.d + self.h * np.sqrt(2) + + def __lt__(self, other): + return self.d < other.d + + def __gt__(self, other): + return self.d > other.d + + +def PolyLabel(rings, precision=1): + """Find the pole of inaccessibility using a grid approach.""" + # Precompute Polygon Data + array = [np.array(ring)[:, :2] for ring in rings] + polygon = Polygon(array) + + # Bounding Box + mins = np.min(polygon.array, axis=0) + maxs = np.max(polygon.array, axis=0) + dims = maxs - mins + s = np.min(dims) + h = s / 2.0 + + # Initial Grid + queue = PriorityQueue() + xv, yv = np.meshgrid(np.arange(mins[0], maxs[0], s), np.arange(mins[1], maxs[1], s)) + for corner in np.vstack([xv.ravel(), yv.ravel()]).T: + queue.put(Cell(corner + h, h, polygon)) + + # Initial Guess + best = Cell(polygon.centroid, 0, polygon) + bbox = Cell(mins + (dims / 2), 0, polygon) + if bbox.d > best.d: + best = bbox + + # While there are cells to consider... + directions = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]]) + while not queue.empty(): + cell = queue.get() + if cell > best: + best = cell + # If a cell is promising, subdivide! + if cell.p - best.d > precision: + h = cell.h / 2.0 + offsets = cell.c + directions * h + queue.put(Cell(offsets[0], h, polygon)) + queue.put(Cell(offsets[1], h, polygon)) + queue.put(Cell(offsets[2], h, polygon)) + queue.put(Cell(offsets[3], h, polygon)) + return best diff --git a/manim/utils/qhull.py b/manim/utils/qhull.py new file mode 100644 index 0000000000..c56df3481e --- /dev/null +++ b/manim/utils/qhull.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +from __future__ import annotations + +import numpy as np + + +class Point: + def __init__(self, coordinates): + self.coordinates = coordinates + + def __hash__(self): + return hash(self.coordinates.tobytes()) + + def __eq__(self, other): + return np.array_equal(self.coordinates, other.coordinates) + + +class SubFacet: + def __init__(self, coordinates): + self.coordinates = coordinates + self.points = frozenset(Point(c) for c in coordinates) + + def __hash__(self): + return hash(self.points) + + def __eq__(self, other): + return self.points == other.points + + +class Facet: + def __init__(self, coordinates, normal=None, internal=None): + self.coordinates = coordinates + self.center = np.mean(coordinates, axis=0) + self.normal = self.compute_normal(internal) + self.subfacets = frozenset( + SubFacet(np.delete(self.coordinates, i, axis=0)) + for i in range(self.coordinates.shape[0]) + ) + + def compute_normal(self, internal): + centered = self.coordinates - self.center + _, _, vh = np.linalg.svd(centered) + normal = vh[-1, :] + normal /= np.linalg.norm(normal) + + if np.dot(normal, self.center - internal) < 0: + normal *= -1 + + return normal + + def __hash__(self): + return hash(self.subfacets) + + def __eq__(self, other): + return self.subfacets == other.subfacets + + +class Horizon: + def __init__(self): + self.facets = set() + self.boundary = [] + + +class QuickHull: + def __init__(self, tolerance=1e-5): + self.facets = [] + self.removed = set() + self.outside = {} + self.neighbors = {} + self.unclaimed = None + self.internal = None + self.tolerance = tolerance + + def initialize(self, points): + # Sample Points + simplex = points[ + np.random.choice(points.shape[0], points.shape[1] + 1, replace=False) + ] + self.unclaimed = points + self.internal = np.mean(simplex, axis=0) + + # Build Simplex + for c in range(simplex.shape[0]): + facet = Facet(np.delete(simplex, c, axis=0), internal=self.internal) + self.classify(facet) + self.facets.append(facet) + + # Attach Neighbors + for f in self.facets: + for sf in f.subfacets: + self.neighbors.setdefault(sf, set()).add(f) + + def classify(self, facet): + if not self.unclaimed.size: + self.outside[facet] = (None, None) + return + + # Compute Projections + projections = (self.unclaimed - facet.center) @ facet.normal + arg = np.argmax(projections) + mask = projections > self.tolerance + + # Identify Eye and Outside Set + eye = self.unclaimed[arg] if projections[arg] > self.tolerance else None + outside = self.unclaimed[mask] + self.outside[facet] = (outside, eye) + self.unclaimed = self.unclaimed[~mask] + + def compute_horizon(self, eye, start_facet): + horizon = Horizon() + self._recursive_horizon(eye, start_facet, horizon) + return horizon + + def _recursive_horizon(self, eye, facet, horizon): + # If the eye is visible from the facet... + if np.dot(facet.normal, eye - facet.center) > 0: + # Label the facet as visible and cross each edge + horizon.facets.add(facet) + for subfacet in facet.subfacets: + neighbor = (self.neighbors[subfacet] - {facet}).pop() + # If the neighbor is not visible, then the edge shared must be on the boundary + if neighbor not in horizon.facets and not self._recursive_horizon( + eye, neighbor, horizon + ): + horizon.boundary.append(subfacet) + return 1 + return 0 + + def build(self, points): + num, dim = points.shape + if (dim == 0) or (num < dim + 1): + raise ValueError("Not enough points supplied to build Convex Hull!") + if dim == 1: + return np.array([np.min(points), np.max(points)]) + + self.initialize(points) + while True: + updated = False + for facet in self.facets: + if facet in self.removed: + continue + outside, eye = self.outside[facet] + if eye is not None: + updated = True + horizon = self.compute_horizon(eye, facet) + for f in horizon.facets: + self.unclaimed = np.vstack((self.unclaimed, self.outside[f][0])) + self.removed.add(f) + for sf in f.subfacets: + self.neighbors[sf].discard(f) + if self.neighbors[sf] == set(): + del self.neighbors[sf] + for sf in horizon.boundary: + nf = Facet( + np.vstack((sf.coordinates, eye)), internal=self.internal + ) + self.classify(nf) + self.facets.append(nf) + for nsf in nf.subfacets: + self.neighbors.setdefault(nsf, set()).add(nf) + if not updated: + break diff --git a/tests/test_graphical_units/control_data/geometry/ConvexHull.npz b/tests/test_graphical_units/control_data/geometry/ConvexHull.npz new file mode 100644 index 0000000000000000000000000000000000000000..8a907a449a854aede37133c4505bfeef1321893f GIT binary patch literal 8945 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 literal 0 HcmV?d00001 diff --git a/tests/test_graphical_units/control_data/geometry/LabeledPolygram.npz b/tests/test_graphical_units/control_data/geometry/LabeledPolygram.npz new file mode 100644 index 0000000000000000000000000000000000000000..2a14d5f2be06e53e8ac4babd48a792ac2c8fc0c2 GIT binary patch literal 5029 zcmeHLdr(tn7XJcL9P5Z(d8nwv>NqG0c4QR;Mtm+6bR!_Lh>&F|q!l5OAOS)E%WP|< zz%tti7$GYxA|Ry88)6b#L|71FuqKiVNhy*bm;iZjL!NsBJF|^;I{VkouEV`^?>F~+ z=R4<~-#O=doO408t2_Y!tC8o|0H5VJWd7H+4mg5%VoYKj?&Fx07_X$O=_s%sRUWw5=Q`w9K}?-M^*d$E4! zqt=pEx<}{wlOF_oTy7*zgj{M5aXG$gD#Z5YXHLOaYueYZ;Z9z@3f3FTum)0Fvi}LW*$C}7vx}wg@$kSRQVe;ShC_4qzTN}eJP^Wn?+)Q}Ivux> z!Pu5Brd~ut;o#``j5TZ9bXzp>w<+DrX8z$2T>DCP1&0DfQ@cMLohCK6KisdHm6m;4 zNlrmf(q1pO>DHtc#A`V0$+h6+-RiEZ?6MuO2go^%2opkR3zj8+`(G!Yuc>}5;d14o zmIRLlIpm{)cNPT7b}97>x4glh1FFi2szIOPOy~J9-+&@XTiYE}&S4LYBkU1hHH^M# zST;VmruqTgKr$`L+QwMqwp@FM)^HrPg$;`-@-njc(|r%;jzb)({t=flctH3S7oS$k zt7x@S(5fr9 zJu#u@vt~Ka!KBf&VIG~qATTnOr(KPkjZZEzPGx(PRrbw7kwf6sJWuqjYV^9vXrmLjasM51OW4|$tKuN5` zv%Cr{Z7BF9Ece6%x57ZoWUsD|4OYf`CH1QECxjthcrfP9q1i-Pfr(T*mz*`#-;I#cW39HBr^PgM(Z z9gd_0GSYjT!kgI?xLh5fuD9~~mTX}<5+A+fT0RT{vVESHX1*z`Zq_$1FK@zpPd6nT z@BV@})$~^@5f3;sqshHndpWk+7qNw9eEczM4^oBcKU}vj7^_=&Jwo~|!_EqfI8Bsn z1kb>vGDWS4&~B=@syJHY?CktI-QN3FCLw=;Jv{&D@090p#Zjq&3|Kj^&n%HdYGQex zS;wKS!CU8-Q)ud(uB{UoBZ_fZ>6A%YxIq23#Gs&{UPV|n(LQ6o#|0A9M+)C$La-O7 zP2|tBr=Q+EF{8V~m)IJ#6NSu~p8;}&L#60-GtAzGhsyo~)W`L0Vo`L)tE$u?wCurK z0}L;sWJm-MB$=wS`nhfgNIFQzYMWf5(_$z{IzZp%+C%BGGIAiV0%rx;D z&V6eyN3!QK*58=I8voo?^f=!Yx_6c?QYwiQZ}-}jZoKExy2$hZBCo>VN^{(8@>ba6f_1DZ!kz?LIedhh zZPxlxpM67+a7wxRlF$Ep-cdVlY-l6BjJ5+Gp2l7u&+rRSgaPoE|04MUqq1j`Hb1+^ zm0d}30$mP>?j#L*Iv@GSDYvQ4r94-YTA3i{BTGk8R*Y0KNT=~x*gbeXvf2h>QH|dq zz4z6QZtuT)gi_1aBbjrCw@VelD|qF*%kJMkkDxu>_7RfI&y!$f+B$>ML z-JL_<=>=lNxc@A0FTh_fZ8K2YH_LY+M@qMfU>~$76f|D7oX?Y)Hv5+j>ZMCrK>>n< zW>!4gAN5ORft9``1TvKV=Ymi+p0?ISHA}h7QaW4r6PR+A@U$+moeQ#i&%%O2?!!nM Ix>RQSCrd~puK)l5 literal 0 HcmV?d00001 diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index 10ea6094e9..8306e52658 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -138,6 +138,20 @@ def test_RoundedRectangle(scene): scene.add(a) +@frames_comparison +def test_ConvexHull(scene): + a = ConvexHull( + *[ + np.array([-2.753, -0.612, 0]), + np.array([0.226, -1.766, 0]), + np.array([1.950, 1.260, 0]), + np.array([-2.754, 0.949, 0]), + np.array([1.679, 2.220, 0]), + ] + ) + scene.add(a) + + @frames_comparison def test_Arrange(scene): s1 = Square() @@ -269,3 +283,20 @@ def test_LabeledArrow(scene): "0.5", start=LEFT * 3, end=RIGHT * 3 + UP * 2, label_position=0.5, font_size=15 ) scene.add(l_arrow) + + +@frames_comparison +def test_LabeledPolygram(scene): + polygram = LabeledPolygram( + [ + [-2.5, -2.5, 0], + [2.5, -2.5, 0], + [2.5, 2.5, 0], + [-2.5, 2.5, 0], + [-2.5, -2.5, 0], + ], + [[-1, -1, 0], [0.5, -1, 0], [0.5, 0.5, 0], [-1, 0.5, 0], [-1, -1, 0]], + [[1, 1, 0], [2, 1, 0], [2, 2, 0], [1, 2, 0], [1, 1, 0]], + label="C", + ) + scene.add(polygram) From 9bf93609b15e094d52708fecb5d3f609aeb198a3 Mon Sep 17 00:00:00 2001 From: Jay Gupta <66860950+JayGupta797@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:39:20 -0700 Subject: [PATCH 2/4] Update labeled.py --- manim/mobject/geometry/labeled.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manim/mobject/geometry/labeled.py b/manim/mobject/geometry/labeled.py index a333b2b076..02194f48b4 100644 --- a/manim/mobject/geometry/labeled.py +++ b/manim/mobject/geometry/labeled.py @@ -46,12 +46,11 @@ class Label(VGroup): class LabelExample(Scene): def construct(self): label = Label( - label = 'Text', - font_size = 20, + label = Text('Label Text', font='sans-serif'), label_color = WHITE, label_frame = True ) - label.move_to(UP) + label.scale(3) self.add(label) """ From b627556fe6a4023ce783437df01ef14ec65d134f Mon Sep 17 00:00:00 2001 From: Jay Gupta <66860950+JayGupta797@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:48:36 -0700 Subject: [PATCH 3/4] general fixes fixed added utils (i.e., Incomplete ordering and Explicit returns mixed with implicit), added :quality: high to docstrings, made ConvexHullExample determined --- manim/mobject/geometry/labeled.py | 3 +++ manim/mobject/geometry/polygram.py | 15 +++++++++++++-- manim/utils/polylabel.py | 6 ++++++ manim/utils/qhull.py | 3 ++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/manim/mobject/geometry/labeled.py b/manim/mobject/geometry/labeled.py index 02194f48b4..36120c08d8 100644 --- a/manim/mobject/geometry/labeled.py +++ b/manim/mobject/geometry/labeled.py @@ -42,6 +42,7 @@ class Label(VGroup): -------- .. manim:: LabelExample :save_last_frame: + :quality: high class LabelExample(Scene): def construct(self): @@ -250,6 +251,7 @@ class LabeledPolygram(Polygram): -------- .. manim:: LabeledPolygramExample :save_last_frame: + :quality: high class LabeledPolygramExample(Scene): def construct(self): @@ -288,6 +290,7 @@ def construct(self): .. manim:: LabeledCountryExample :save_last_frame: + :quality: high import requests import json diff --git a/manim/mobject/geometry/polygram.py b/manim/mobject/geometry/polygram.py index df849af89c..5d9fcc9466 100644 --- a/manim/mobject/geometry/polygram.py +++ b/manim/mobject/geometry/polygram.py @@ -784,11 +784,22 @@ class ConvexHull(Polygram): -------- .. manim:: ConvexHullExample :save_last_frame: + :quality: high class ConvexHullExample(Scene): def construct(self): - points = np.random.uniform(low=-3, high=3, size=(100, 3)) - points[:,2] = 0 + points = [ + [-2.35, -2.25, 0], + [1.65, -2.25, 0], + [2.65, -0.25, 0], + [1.65, 1.75, 0], + [-0.35, 2.75, 0], + [-2.35, 0.75, 0], + [-0.35, -1.25, 0], + [0.65, -0.25, 0], + [-1.35, 0.25, 0], + [0.15, 0.75, 0] + ] hull = ConvexHull(*points, color=BLUE) dots = VGroup(*[Dot(point) for point in points]) self.add(hull) diff --git a/manim/utils/polylabel.py b/manim/utils/polylabel.py index 5f22153f10..81e26b6f3f 100644 --- a/manim/utils/polylabel.py +++ b/manim/utils/polylabel.py @@ -67,6 +67,12 @@ def __lt__(self, other): def __gt__(self, other): return self.d > other.d + def __le__(self, other): + return self.d <= other.d + + def __ge__(self, other): + return self.d >= other.d + def PolyLabel(rings, precision=1): """Find the pole of inaccessibility using a grid approach.""" diff --git a/manim/utils/qhull.py b/manim/utils/qhull.py index c56df3481e..d13c551630 100644 --- a/manim/utils/qhull.py +++ b/manim/utils/qhull.py @@ -124,7 +124,8 @@ def _recursive_horizon(self, eye, facet, horizon): ): horizon.boundary.append(subfacet) return 1 - return 0 + else: + return 0 def build(self, points): num, dim = points.shape From 03cc46c656f5fab1c5418c7a1776ed5d62086f81 Mon Sep 17 00:00:00 2001 From: Jay Gupta <66860950+JayGupta797@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:44:50 -0700 Subject: [PATCH 4/4] typing added typing to `qhull.py` and `polylabel.py` for debugging. simplified test cases for `ConvexHull` and `ConvexHull3D` and rewrote control data. added tip to LabeledPolygon documentation. --- manim/mobject/geometry/labeled.py | 4 + manim/mobject/three_d/polyhedra.py | 1 + manim/utils/polylabel.py | 64 ++++++++++--- manim/utils/qhull.py | 90 ++++++++++++------ .../control_data/geometry/ConvexHull.npz | Bin 8945 -> 9555 bytes .../control_data/polyhedra/ConvexHull3D.npz | Bin 0 -> 14916 bytes tests/test_graphical_units/test_geometry.py | 10 +- tests/test_graphical_units/test_polyhedra.py | 14 +++ 8 files changed, 134 insertions(+), 49 deletions(-) create mode 100644 tests/test_graphical_units/control_data/polyhedra/ConvexHull3D.npz diff --git a/manim/mobject/geometry/labeled.py b/manim/mobject/geometry/labeled.py index 36120c08d8..00ceb77699 100644 --- a/manim/mobject/geometry/labeled.py +++ b/manim/mobject/geometry/labeled.py @@ -247,6 +247,10 @@ class LabeledPolygram(Polygram): If the input is open, LabeledPolygram will attempt to close it. This may cause the polygon to intersect itself leading to unexpected results. + .. tip:: + Make sure the precision corresponds to the scale of your inputs! + For instance, if the bounding box of your polygon stretches from 0 to 10,000, a precision of 1.0 or 10.0 should be sufficient. + Examples -------- .. manim:: LabeledPolygramExample diff --git a/manim/mobject/three_d/polyhedra.py b/manim/mobject/three_d/polyhedra.py index 9d0ff387c5..8046f6066c 100644 --- a/manim/mobject/three_d/polyhedra.py +++ b/manim/mobject/three_d/polyhedra.py @@ -388,6 +388,7 @@ class ConvexHull3D(Polyhedron): -------- .. manim:: ConvexHull3DExample :save_last_frame: + :quality: high class ConvexHull3DExample(ThreeDScene): def construct(self): diff --git a/manim/utils/polylabel.py b/manim/utils/polylabel.py index 81e26b6f3f..8379b3ddd9 100644 --- a/manim/utils/polylabel.py +++ b/manim/utils/polylabel.py @@ -2,14 +2,24 @@ from __future__ import annotations from queue import PriorityQueue +from typing import TYPE_CHECKING import numpy as np +if TYPE_CHECKING: + from manim.typing import Point3D -class Polygon: - """Polygon class to compute and store associated data.""" - def __init__(self, rings): +class Polygon: + def __init__(self, rings: list[np.ndarray]) -> None: + """ + Initializes the Polygon with the given rings. + + Parameters + ---------- + rings : list[np.ndarray] + List of arrays, each representing a polygonal ring + """ # Flatten Array csum = np.cumsum([ring.shape[0] for ring in rings]) self.array = np.concatenate(rings, axis=0) @@ -32,14 +42,14 @@ def __init__(self, rings): cy = np.sum((y + yr) * factor) / (6.0 * self.area) self.centroid = np.array([cx, cy]) - def compute_distance(self, point): + def compute_distance(self, point: np.ndarray) -> float: """Compute the minimum distance from a point to the polygon.""" scalars = np.einsum("ij,ij->i", self.norm, point - self.start) clips = np.clip(scalars, 0, 1).reshape(-1, 1) d = np.min(np.linalg.norm(self.start + self.diff * clips - point, axis=1)) return d if self.inside(point) else -d - def inside(self, point): + def inside(self, point: np.ndarray) -> bool: """Check if a point is inside the polygon.""" # Views px, py = point @@ -53,29 +63,55 @@ def inside(self, point): class Cell: - """Cell class to represent a square in the grid covering the polygon.""" - - def __init__(self, c, h, polygon): + def __init__(self, c: np.ndarray, h: float, polygon: Polygon) -> None: + """ + Initializes the Cell, a square in the mesh covering the polygon. + + Parameters + ---------- + c : np.ndarray + Center coordinates of the Cell. + h : float + Half-Size of the Cell. + polygon : Polygon + Polygon object for which the distance is computed. + """ self.c = c self.h = h self.d = polygon.compute_distance(self.c) self.p = self.d + self.h * np.sqrt(2) - def __lt__(self, other): + def __lt__(self, other: Cell) -> bool: return self.d < other.d - def __gt__(self, other): + def __gt__(self, other: Cell) -> bool: return self.d > other.d - def __le__(self, other): + def __le__(self, other: Cell) -> bool: return self.d <= other.d - def __ge__(self, other): + def __ge__(self, other: Cell) -> bool: return self.d >= other.d -def PolyLabel(rings, precision=1): - """Find the pole of inaccessibility using a grid approach.""" +def PolyLabel(rings: list[list[Point3D]], precision: float = 0.01) -> Cell: + """ + Finds the pole of inaccessibility (the point that is farthest from the edges of the polygon) + using an iterative grid-based approach. + + Parameters + ---------- + rings : list[list[Point3D]] + A list of lists, where each list is a sequence of points representing the rings of the polygon. + Typically, multiple rings indicate holes in the polygon. + precision : float, optional + The precision of the result (default is 0.01). + + Returns + ------- + Cell + A Cell containing the pole of inaccessibility to a given precision. + """ # Precompute Polygon Data array = [np.array(ring)[:, :2] for ring in rings] polygon = Polygon(array) diff --git a/manim/utils/qhull.py b/manim/utils/qhull.py index d13c551630..9758e0605a 100644 --- a/manim/utils/qhull.py +++ b/manim/utils/qhull.py @@ -5,30 +5,30 @@ class Point: - def __init__(self, coordinates): + def __init__(self, coordinates: np.ndarray) -> None: self.coordinates = coordinates - def __hash__(self): + def __hash__(self) -> int: return hash(self.coordinates.tobytes()) - def __eq__(self, other): + def __eq__(self, other: Point) -> bool: return np.array_equal(self.coordinates, other.coordinates) class SubFacet: - def __init__(self, coordinates): + def __init__(self, coordinates: np.ndarray) -> None: self.coordinates = coordinates self.points = frozenset(Point(c) for c in coordinates) - def __hash__(self): + def __hash__(self) -> int: return hash(self.points) - def __eq__(self, other): + def __eq__(self, other: SubFacet) -> bool: return self.points == other.points class Facet: - def __init__(self, coordinates, normal=None, internal=None): + def __init__(self, coordinates: np.ndarray, internal: np.ndarray) -> None: self.coordinates = coordinates self.center = np.mean(coordinates, axis=0) self.normal = self.compute_normal(internal) @@ -37,41 +37,68 @@ def __init__(self, coordinates, normal=None, internal=None): for i in range(self.coordinates.shape[0]) ) - def compute_normal(self, internal): + def compute_normal(self, internal: np.ndarray) -> np.ndarray: centered = self.coordinates - self.center _, _, vh = np.linalg.svd(centered) normal = vh[-1, :] normal /= np.linalg.norm(normal) + # If the normal points towards the internal point, flip it! if np.dot(normal, self.center - internal) < 0: normal *= -1 return normal - def __hash__(self): + def __hash__(self) -> int: return hash(self.subfacets) - def __eq__(self, other): + def __eq__(self, other: Facet) -> bool: return self.subfacets == other.subfacets class Horizon: - def __init__(self): - self.facets = set() - self.boundary = [] + def __init__(self) -> None: + self.facets: set[Facet] = set() + self.boundary: list[SubFacet] = [] class QuickHull: - def __init__(self, tolerance=1e-5): - self.facets = [] - self.removed = set() - self.outside = {} - self.neighbors = {} - self.unclaimed = None - self.internal = None + """ + QuickHull algorithm for constructing a convex hull from a set of points. + + Parameters + ---------- + tolerance: float, optional + A tolerance threshold for determining when points lie on the convex hull (default is 1e-5). + + Attributes + ---------- + facets: list[Facet] + List of facets considered. + removed: set[Facet] + Set of internal facets that have been removed from the hull during the construction process. + outside: dict[Facet, tuple[np.ndarray, np.ndarray | None]] + Dictionary mapping each facet to its outside points and eye point. + neighbors: dict[SubFacet, set[Facet]] + Mapping of subfacets to their neighboring facets. Each subfacet links precisely two neighbors. + unclaimed: np.ndarray | None + Points that have not yet been classified as inside or outside the current hull. + internal: np.ndarray | None + An internal point (i.e., the center of the initial simplex) used as a reference during hull construction. + tolerance: float + The tolerance used to determine if points are considered outside the current hull. + """ + + def __init__(self, tolerance: float = 1e-5) -> None: + self.facets: list[Facet] = [] + self.removed: set[Facet] = set() + self.outside: dict[Facet, tuple[np.ndarray, np.ndarray | None]] = {} + self.neighbors: dict[SubFacet, set[Facet]] = {} + self.unclaimed: np.ndarray | None = None + self.internal: np.ndarray | None = None self.tolerance = tolerance - def initialize(self, points): + def initialize(self, points: np.ndarray) -> None: # Sample Points simplex = points[ np.random.choice(points.shape[0], points.shape[1] + 1, replace=False) @@ -90,7 +117,7 @@ def initialize(self, points): for sf in f.subfacets: self.neighbors.setdefault(sf, set()).add(f) - def classify(self, facet): + def classify(self, facet: Facet) -> None: if not self.unclaimed.size: self.outside[facet] = (None, None) return @@ -106,14 +133,19 @@ def classify(self, facet): self.outside[facet] = (outside, eye) self.unclaimed = self.unclaimed[~mask] - def compute_horizon(self, eye, start_facet): + def compute_horizon(self, eye: np.ndarray, start_facet: Facet) -> Horizon: horizon = Horizon() self._recursive_horizon(eye, start_facet, horizon) return horizon - def _recursive_horizon(self, eye, facet, horizon): + def _recursive_horizon( + self, eye: np.ndarray, facet: Facet, horizon: Horizon + ) -> int: + visible = np.dot(facet.normal, eye - facet.center) > 0 + if not visible: + return False # If the eye is visible from the facet... - if np.dot(facet.normal, eye - facet.center) > 0: + else: # Label the facet as visible and cross each edge horizon.facets.add(facet) for subfacet in facet.subfacets: @@ -123,16 +155,14 @@ def _recursive_horizon(self, eye, facet, horizon): eye, neighbor, horizon ): horizon.boundary.append(subfacet) - return 1 - else: - return 0 + return True - def build(self, points): + def build(self, points: np.ndarray): num, dim = points.shape if (dim == 0) or (num < dim + 1): raise ValueError("Not enough points supplied to build Convex Hull!") if dim == 1: - return np.array([np.min(points), np.max(points)]) + raise ValueError("The Convex Hull of 1D data is its min-max!") self.initialize(points) while True: diff --git a/tests/test_graphical_units/control_data/geometry/ConvexHull.npz b/tests/test_graphical_units/control_data/geometry/ConvexHull.npz index 8a907a449a854aede37133c4505bfeef1321893f..6ccb415ee008e3f1ce66011f73b1093eb5046a4f 100644 GIT binary patch literal 9555 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 z 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 diff --git a/tests/test_graphical_units/control_data/polyhedra/ConvexHull3D.npz b/tests/test_graphical_units/control_data/polyhedra/ConvexHull3D.npz new file mode 100644 index 0000000000000000000000000000000000000000..eaa726b0fbb054e8235eff3c1fc3ed1e01a9301c GIT binary patch literal 14916 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 literal 0 HcmV?d00001 diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index 8306e52658..962aec301b 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -142,11 +142,11 @@ def test_RoundedRectangle(scene): def test_ConvexHull(scene): a = ConvexHull( *[ - np.array([-2.753, -0.612, 0]), - np.array([0.226, -1.766, 0]), - np.array([1.950, 1.260, 0]), - np.array([-2.754, 0.949, 0]), - np.array([1.679, 2.220, 0]), + [-2.7, -0.6, 0], + [0.2, -1.7, 0], + [1.9, 1.2, 0], + [-2.7, 0.9, 0], + [1.6, 2.2, 0], ] ) scene.add(a) diff --git a/tests/test_graphical_units/test_polyhedra.py b/tests/test_graphical_units/test_polyhedra.py index bc13676fde..9679bed3a1 100644 --- a/tests/test_graphical_units/test_polyhedra.py +++ b/tests/test_graphical_units/test_polyhedra.py @@ -24,3 +24,17 @@ def test_Icosahedron(scene): @frames_comparison def test_Dodecahedron(scene): scene.add(Dodecahedron()) + + +@frames_comparison +def test_ConvexHull3D(scene): + a = ConvexHull3D( + *[ + [-2.7, -0.6, 3.5], + [0.2, -1.7, -2.8], + [1.9, 1.2, 0.7], + [-2.7, 0.9, 1.9], + [1.6, 2.2, -4.2], + ] + ) + scene.add(a)