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 and LabeledPolygram #3933

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 250 additions & 33 deletions manim/mobject/geometry/labeled.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,101 @@

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('Label Text', font='sans-serif'),
label_color = WHITE,
label_frame = True
)
label.scale(3)
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):
Expand Down Expand Up @@ -51,11 +135,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)
"""
Expand All @@ -72,42 +154,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):
Expand Down Expand Up @@ -153,3 +218,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.
Comment on lines +224 to +242
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestions as before: don't write the types again in the docstring, and use label_config and frame_config parameters.

Don't forget to include the vertex_groups parameter.



.. 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)
53 changes: 53 additions & 0 deletions 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 Down Expand Up @@ -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)
Loading
Loading