Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ConvexHull, ConvexHull3D, Label and LabeledPolygram #3933

Merged
merged 12 commits into from
Nov 27, 2024
366 changes: 291 additions & 75 deletions manim/mobject/geometry/labeled.py

Large diffs are not rendered by default.

69 changes: 68 additions & 1 deletion manim/mobject/geometry/polygram.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Square",
"RoundedRectangle",
"Cutout",
"ConvexHull",
]


Expand All @@ -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:
Expand All @@ -39,6 +41,7 @@
InternalPoint3D,
InternalPoint3D_Array,
Point3D,
Point3D_Array,
)
from manim.utils.color import ParsableManimColor

Expand Down Expand Up @@ -80,7 +83,7 @@ def construct(self):

def __init__(
self,
*vertex_groups: Point3D,
*vertex_groups: Point3D_Array,
color: ParsableManimColor = BLUE,
**kwargs: Any,
):
Expand Down Expand Up @@ -780,3 +783,67 @@ def __init__(
)
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:
:quality: high

class ConvexHullExample(Scene):
def construct(self):
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)
self.add(dots)
"""

def __init__(
self, *points: Point3D, tolerance: float = 1e-5, **kwargs: Any
) -> 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)
99 changes: 98 additions & 1 deletion manim/mobject/three_d/polyhedra.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -361,3 +370,91 @@ 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:
:quality: high

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,
)
156 changes: 156 additions & 0 deletions manim/utils/polylabel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python
from __future__ import annotations

from queue import PriorityQueue
from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
from collections.abc import Sequence

from manim.typing import Point3D, Point3D_Array


class Polygon:
"""
Initializes the Polygon with the given rings.

Parameters
----------
rings
A collection of closed polygonal ring.
"""

def __init__(self, rings: Sequence[Point3D_Array]) -> None:
# 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: Point3D) -> 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: Point3D) -> bool:
"""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:
Fixed Show fixed Hide fixed
"""
A square in a mesh covering the :class:`~.Polygon` passed as an argument.

Parameters
----------
c
Center coordinates of the Cell.
h
Half-Size of the Cell.
polygon
:class:`~.Polygon` object for which the distance is computed.
"""

def __init__(self, c: Point3D, h: float, polygon: Polygon) -> None:
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: Cell) -> bool:
return self.d < other.d

def __gt__(self, other: Cell) -> bool:
return self.d > other.d

def __le__(self, other: Cell) -> bool:
return self.d <= other.d

def __ge__(self, other: Cell) -> bool:
return self.d >= other.d


def polylabel(rings: Sequence[Point3D_Array], 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
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
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)

# 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
Loading