Skip to content

Commit 862504f

Browse files
JayGupta797pre-commit-ci[bot]chopan050
authored
Added ConvexHull, ConvexHull3D, Label and LabeledPolygram (#3933)
* initial commit * Update labeled.py * general fixes fixed added utils (i.e., Incomplete ordering and Explicit returns mixed with implicit), added :quality: high to docstrings, made ConvexHullExample determined * 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. * response to feedback added `label_config` and `box_config` and `frame_config` for additional configuration options and cleaner interface. added `InternalPointND` and `PointND ` and `InternalPointND_Array` and `PointND_Array` for typing. updated docstring. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * typing * Update manim/mobject/geometry/labeled.py Co-authored-by: Francisco Manríquez Novoa <[email protected]> * Update manim/mobject/geometry/labeled.py Co-authored-by: Francisco Manríquez Novoa <[email protected]> * typing, docstring, class name * typing --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Francisco Manríquez Novoa <[email protected]>
1 parent fac0aa5 commit 862504f

File tree

10 files changed

+860
-81
lines changed

10 files changed

+860
-81
lines changed

manim/mobject/geometry/labeled.py

Lines changed: 291 additions & 75 deletions
Large diffs are not rendered by default.

manim/mobject/geometry/polygram.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"Square",
1414
"RoundedRectangle",
1515
"Cutout",
16+
"ConvexHull",
1617
]
1718

1819

@@ -27,6 +28,7 @@
2728
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
2829
from manim.utils.color import BLUE, WHITE, ParsableManimColor
2930
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
31+
from manim.utils.qhull import QuickHull
3032
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
3133

3234
if TYPE_CHECKING:
@@ -39,6 +41,7 @@
3941
InternalPoint3D,
4042
InternalPoint3D_Array,
4143
Point3D,
44+
Point3D_Array,
4245
)
4346
from manim.utils.color import ParsableManimColor
4447

@@ -80,7 +83,7 @@ def construct(self):
8083

8184
def __init__(
8285
self,
83-
*vertex_groups: Point3D,
86+
*vertex_groups: Point3D_Array,
8487
color: ParsableManimColor = BLUE,
8588
**kwargs: Any,
8689
):
@@ -780,3 +783,67 @@ def __init__(
780783
)
781784
for mobject in mobjects:
782785
self.append_points(mobject.force_direction(sub_direction).points)
786+
787+
788+
class ConvexHull(Polygram):
789+
"""Constructs a convex hull for a set of points in no particular order.
790+
791+
Parameters
792+
----------
793+
points
794+
The points to consider.
795+
tolerance
796+
The tolerance used by quickhull.
797+
kwargs
798+
Forwarded to the parent constructor.
799+
800+
Examples
801+
--------
802+
.. manim:: ConvexHullExample
803+
:save_last_frame:
804+
:quality: high
805+
806+
class ConvexHullExample(Scene):
807+
def construct(self):
808+
points = [
809+
[-2.35, -2.25, 0],
810+
[1.65, -2.25, 0],
811+
[2.65, -0.25, 0],
812+
[1.65, 1.75, 0],
813+
[-0.35, 2.75, 0],
814+
[-2.35, 0.75, 0],
815+
[-0.35, -1.25, 0],
816+
[0.65, -0.25, 0],
817+
[-1.35, 0.25, 0],
818+
[0.15, 0.75, 0]
819+
]
820+
hull = ConvexHull(*points, color=BLUE)
821+
dots = VGroup(*[Dot(point) for point in points])
822+
self.add(hull)
823+
self.add(dots)
824+
"""
825+
826+
def __init__(
827+
self, *points: Point3D, tolerance: float = 1e-5, **kwargs: Any
828+
) -> None:
829+
# Build Convex Hull
830+
array = np.array(points)[:, :2]
831+
hull = QuickHull(tolerance)
832+
hull.build(array)
833+
834+
# Extract Vertices
835+
facets = set(hull.facets) - hull.removed
836+
facet = facets.pop()
837+
subfacets = list(facet.subfacets)
838+
while len(subfacets) <= len(facets):
839+
sf = subfacets[-1]
840+
(facet,) = hull.neighbors[sf] - {facet}
841+
(sf,) = facet.subfacets - {sf}
842+
subfacets.append(sf)
843+
844+
# Setup Vertices as Point3D
845+
coordinates = np.vstack([sf.coordinates for sf in subfacets])
846+
vertices = np.hstack((coordinates, np.zeros((len(coordinates), 1))))
847+
848+
# Call Polygram
849+
super().__init__(vertices, **kwargs)

manim/mobject/three_d/polyhedra.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@
1010
from manim.mobject.graph import Graph
1111
from manim.mobject.three_d.three_dimensions import Dot3D
1212
from manim.mobject.types.vectorized_mobject import VGroup
13+
from manim.utils.qhull import QuickHull
1314

1415
if TYPE_CHECKING:
1516
from manim.mobject.mobject import Mobject
17+
from manim.typing import Point3D
1618

17-
__all__ = ["Polyhedron", "Tetrahedron", "Octahedron", "Icosahedron", "Dodecahedron"]
19+
__all__ = [
20+
"Polyhedron",
21+
"Tetrahedron",
22+
"Octahedron",
23+
"Icosahedron",
24+
"Dodecahedron",
25+
"ConvexHull3D",
26+
]
1827

1928

2029
class Polyhedron(VGroup):
@@ -361,3 +370,91 @@ def __init__(self, edge_length: float = 1, **kwargs):
361370
],
362371
**kwargs,
363372
)
373+
374+
375+
class ConvexHull3D(Polyhedron):
376+
"""A convex hull for a set of points
377+
378+
Parameters
379+
----------
380+
points
381+
The points to consider.
382+
tolerance
383+
The tolerance used for quickhull.
384+
kwargs
385+
Forwarded to the parent constructor.
386+
387+
Examples
388+
--------
389+
.. manim:: ConvexHull3DExample
390+
:save_last_frame:
391+
:quality: high
392+
393+
class ConvexHull3DExample(ThreeDScene):
394+
def construct(self):
395+
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
396+
points = [
397+
[ 1.93192757, 0.44134585, -1.52407061],
398+
[-0.93302521, 1.23206983, 0.64117067],
399+
[-0.44350918, -0.61043677, 0.21723705],
400+
[-0.42640268, -1.05260843, 1.61266094],
401+
[-1.84449637, 0.91238739, -1.85172623],
402+
[ 1.72068132, -0.11880457, 0.51881751],
403+
[ 0.41904805, 0.44938012, -1.86440686],
404+
[ 0.83864666, 1.66653337, 1.88960123],
405+
[ 0.22240514, -0.80986286, 1.34249326],
406+
[-1.29585759, 1.01516189, 0.46187522],
407+
[ 1.7776499, -1.59550796, -1.70240747],
408+
[ 0.80065226, -0.12530398, 1.70063977],
409+
[ 1.28960948, -1.44158255, 1.39938582],
410+
[-0.93538943, 1.33617705, -0.24852643],
411+
[-1.54868271, 1.7444399, -0.46170734]
412+
]
413+
hull = ConvexHull3D(
414+
*points,
415+
faces_config = {"stroke_opacity": 0},
416+
graph_config = {
417+
"vertex_type": Dot3D,
418+
"edge_config": {
419+
"stroke_color": BLUE,
420+
"stroke_width": 2,
421+
"stroke_opacity": 0.05,
422+
}
423+
}
424+
)
425+
dots = VGroup(*[Dot3D(point) for point in points])
426+
self.add(hull)
427+
self.add(dots)
428+
"""
429+
430+
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs):
431+
# Build Convex Hull
432+
array = np.array(points)
433+
hull = QuickHull(tolerance)
434+
hull.build(array)
435+
436+
# Setup Lists
437+
vertices = []
438+
faces = []
439+
440+
# Extract Faces
441+
c = 0
442+
d = {}
443+
facets = set(hull.facets) - hull.removed
444+
for facet in facets:
445+
tmp = set()
446+
for subfacet in facet.subfacets:
447+
for point in subfacet.points:
448+
if point not in d:
449+
vertices.append(point.coordinates)
450+
d[point] = c
451+
c += 1
452+
tmp.add(point)
453+
faces.append([d[point] for point in tmp])
454+
455+
# Call Polyhedron
456+
super().__init__(
457+
vertex_coords=vertices,
458+
faces_list=faces,
459+
**kwargs,
460+
)

manim/utils/polylabel.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python
2+
from __future__ import annotations
3+
4+
from queue import PriorityQueue
5+
from typing import TYPE_CHECKING
6+
7+
import numpy as np
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import Sequence
11+
12+
from manim.typing import Point3D, Point3D_Array
13+
14+
15+
class Polygon:
16+
"""
17+
Initializes the Polygon with the given rings.
18+
19+
Parameters
20+
----------
21+
rings
22+
A collection of closed polygonal ring.
23+
"""
24+
25+
def __init__(self, rings: Sequence[Point3D_Array]) -> None:
26+
# Flatten Array
27+
csum = np.cumsum([ring.shape[0] for ring in rings])
28+
self.array = np.concatenate(rings, axis=0)
29+
30+
# Compute Boundary
31+
self.start = np.delete(self.array, csum - 1, axis=0)
32+
self.stop = np.delete(self.array, csum % csum[-1], axis=0)
33+
self.diff = np.delete(np.diff(self.array, axis=0), csum[:-1] - 1, axis=0)
34+
self.norm = self.diff / np.einsum("ij,ij->i", self.diff, self.diff).reshape(
35+
-1, 1
36+
)
37+
38+
# Compute Centroid
39+
x, y = self.start[:, 0], self.start[:, 1]
40+
xr, yr = self.stop[:, 0], self.stop[:, 1]
41+
self.area = 0.5 * (np.dot(x, yr) - np.dot(xr, y))
42+
if self.area:
43+
factor = x * yr - xr * y
44+
cx = np.sum((x + xr) * factor) / (6.0 * self.area)
45+
cy = np.sum((y + yr) * factor) / (6.0 * self.area)
46+
self.centroid = np.array([cx, cy])
47+
48+
def compute_distance(self, point: Point3D) -> float:
49+
"""Compute the minimum distance from a point to the polygon."""
50+
scalars = np.einsum("ij,ij->i", self.norm, point - self.start)
51+
clips = np.clip(scalars, 0, 1).reshape(-1, 1)
52+
d = np.min(np.linalg.norm(self.start + self.diff * clips - point, axis=1))
53+
return d if self.inside(point) else -d
54+
55+
def inside(self, point: Point3D) -> bool:
56+
"""Check if a point is inside the polygon."""
57+
# Views
58+
px, py = point
59+
x, y = self.start[:, 0], self.start[:, 1]
60+
xr, yr = self.stop[:, 0], self.stop[:, 1]
61+
62+
# Count Crossings (enforce short-circuit)
63+
c = (y > py) != (yr > py)
64+
c = px < x[c] + (py - y[c]) * (xr[c] - x[c]) / (yr[c] - y[c])
65+
return np.sum(c) % 2 == 1
66+
67+
68+
class Cell:
69+
"""
70+
A square in a mesh covering the :class:`~.Polygon` passed as an argument.
71+
72+
Parameters
73+
----------
74+
c
75+
Center coordinates of the Cell.
76+
h
77+
Half-Size of the Cell.
78+
polygon
79+
:class:`~.Polygon` object for which the distance is computed.
80+
"""
81+
82+
def __init__(self, c: Point3D, h: float, polygon: Polygon) -> None:
83+
self.c = c
84+
self.h = h
85+
self.d = polygon.compute_distance(self.c)
86+
self.p = self.d + self.h * np.sqrt(2)
87+
88+
def __lt__(self, other: Cell) -> bool:
89+
return self.d < other.d
90+
91+
def __gt__(self, other: Cell) -> bool:
92+
return self.d > other.d
93+
94+
def __le__(self, other: Cell) -> bool:
95+
return self.d <= other.d
96+
97+
def __ge__(self, other: Cell) -> bool:
98+
return self.d >= other.d
99+
100+
101+
def polylabel(rings: Sequence[Point3D_Array], precision: float = 0.01) -> Cell:
102+
"""
103+
Finds the pole of inaccessibility (the point that is farthest from the edges of the polygon)
104+
using an iterative grid-based approach.
105+
106+
Parameters
107+
----------
108+
rings
109+
A list of lists, where each list is a sequence of points representing the rings of the polygon.
110+
Typically, multiple rings indicate holes in the polygon.
111+
precision
112+
The precision of the result (default is 0.01).
113+
114+
Returns
115+
-------
116+
Cell
117+
A Cell containing the pole of inaccessibility to a given precision.
118+
"""
119+
# Precompute Polygon Data
120+
array = [np.array(ring)[:, :2] for ring in rings]
121+
polygon = Polygon(array)
122+
123+
# Bounding Box
124+
mins = np.min(polygon.array, axis=0)
125+
maxs = np.max(polygon.array, axis=0)
126+
dims = maxs - mins
127+
s = np.min(dims)
128+
h = s / 2.0
129+
130+
# Initial Grid
131+
queue = PriorityQueue()
132+
xv, yv = np.meshgrid(np.arange(mins[0], maxs[0], s), np.arange(mins[1], maxs[1], s))
133+
for corner in np.vstack([xv.ravel(), yv.ravel()]).T:
134+
queue.put(Cell(corner + h, h, polygon))
135+
136+
# Initial Guess
137+
best = Cell(polygon.centroid, 0, polygon)
138+
bbox = Cell(mins + (dims / 2), 0, polygon)
139+
if bbox.d > best.d:
140+
best = bbox
141+
142+
# While there are cells to consider...
143+
directions = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]])
144+
while not queue.empty():
145+
cell = queue.get()
146+
if cell > best:
147+
best = cell
148+
# If a cell is promising, subdivide!
149+
if cell.p - best.d > precision:
150+
h = cell.h / 2.0
151+
offsets = cell.c + directions * h
152+
queue.put(Cell(offsets[0], h, polygon))
153+
queue.put(Cell(offsets[1], h, polygon))
154+
queue.put(Cell(offsets[2], h, polygon))
155+
queue.put(Cell(offsets[3], h, polygon))
156+
return best

0 commit comments

Comments
 (0)