diff --git a/manim/mobject/geometry/arc.py b/manim/mobject/geometry/arc.py index 32b1133a6b..61e38cbff3 100644 --- a/manim/mobject/geometry/arc.py +++ b/manim/mobject/geometry/arc.py @@ -68,6 +68,7 @@ def construct(self): import manim.mobject.geometry.tips as tips from manim.mobject.geometry.line import Line + from manim.mobject.geometry.tips import ArrowTip from manim.mobject.mobject import Mobject from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex from manim.mobject.text.text_mobject import Text @@ -114,7 +115,7 @@ def __init__( def add_tip( self, tip: tips.ArrowTip | None = None, - tip_shape: type[tips.ArrowTip] | None = None, + tip_shape: type[ArrowTip] | None = None, tip_length: float | None = None, tip_width: float | None = None, at_start: bool = False, @@ -483,10 +484,13 @@ def __init__( ) arc_height = radius - np.sqrt(radius**2 - halfdist**2) angle = np.arccos((radius - arc_height) / radius) * sign - super().__init__(radius=radius, angle=angle, **kwargs) if angle == 0: self.set_points_as_corners(np.array([LEFT, RIGHT])) + + start = self.make3Dpoint(start) + end = self.make3Dpoint(end) + self.put_start_and_end_on(start, end) if radius is None: @@ -499,6 +503,21 @@ def __init__( else: self.radius = np.inf + def make3Dpoint( + self, + point: Point3DLike, + ) -> Point3D: + """Transforms a point, i.e. a 1D numpy array of shape (2,) into 1D numpy array of shape (3,). + + Parameters + ---------- + point + The point which is to be converted to a 3D point. + """ + if len(point) == 2: + return np.hstack([point, 0]) + return np.array(point) + class TangentialArc(ArcBetweenPoints): """ @@ -556,9 +575,6 @@ def __init__( corner: Any = (1, 1), **kwargs: Any, ): - self.line1 = line1 - self.line2 = line2 - intersection_point = line_intersection( [line1.get_start(), line1.get_end()], [line2.get_start(), line2.get_end()] ) @@ -594,26 +610,38 @@ def __init__( class CurvedArrow(ArcBetweenPoints): def __init__( - self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any + self, + start_point: Point3DLike, + end_point: Point3DLike, + tip_shape: type[ArrowTip] | None = None, + **kwargs: Any, ) -> None: from manim.mobject.geometry.tips import ArrowTriangleFilledTip - tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip) + if tip_shape is None: + tip_shape = ArrowTriangleFilledTip super().__init__(start_point, end_point, **kwargs) self.add_tip(tip_shape=tip_shape) class CurvedDoubleArrow(CurvedArrow): def __init__( - self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any + self, + start_point: Point3DLike, + end_point: Point3DLike, + tip_shape_at_start: type[ArrowTip] | None = None, + tip_shape_at_end: type[ArrowTip] | None = None, + **kwargs: Any, ) -> None: - if "tip_shape_end" in kwargs: - kwargs["tip_shape"] = kwargs.pop("tip_shape_end") from manim.mobject.geometry.tips import ArrowTriangleFilledTip - tip_shape_start = kwargs.pop("tip_shape_start", ArrowTriangleFilledTip) - super().__init__(start_point, end_point, **kwargs) - self.add_tip(at_start=True, tip_shape=tip_shape_start) + if tip_shape_at_start is None: + tip_shape_at_start = ArrowTriangleFilledTip + if tip_shape_at_end is None: + tip_shape_at_end = ArrowTriangleFilledTip + + super().__init__(start_point, end_point, tip_shape=tip_shape_at_end, **kwargs) + self.add_tip(at_start=True, tip_shape=tip_shape_at_start) class Circle(Arc): @@ -621,8 +649,14 @@ class Circle(Arc): Parameters ---------- + center + Center of the Circle. + If it's not provided by user, then it defaults to ORIGIN, + as the arc_center in Arc class is set to default value of ORIGIN. + radius + Radius of the Circle color - The color of the shape. + The stroke color of the circle. kwargs Additional arguments to be passed to :class:`Arc` @@ -645,8 +679,11 @@ def __init__( self, radius: float | None = None, color: ParsableManimColor = RED, + center: Point3DLike | None = None, **kwargs: Any, ) -> None: + if center is not None: + kwargs["arc_center"] = center super().__init__( radius=radius, start_angle=0, @@ -697,14 +734,24 @@ def construct(self): group = Group(group1, group2, group3).arrange(buff=1) self.add(group) """ - # Ignores dim_to_match and stretch; result will always be a circle - # TODO: Perhaps create an ellipse class to handle single-dimension stretching + if dim_to_match != 0: + warnings.warn( + "The 'dim_to_match' is ignored " + "and it will be deprecated in a future version of Manim Community.", + DeprecationWarning, + stacklevel=2, + ) - # Something goes wrong here when surrounding lines? - # TODO: Figure out and fix - self.replace(mobject, dim_to_match, stretch) + if stretch: + warnings.warn( + "The 'stretch' parameter is ignored " + "and it will be removed in a future version of Manim Community.", + DeprecationWarning, + stacklevel=2, + ) self.width = np.sqrt(mobject.width**2 + mobject.height**2) + self.move_to(mobject.get_center()) return self.scale(buffer_factor) def point_at_angle(self, angle: float) -> Point3D: @@ -768,7 +815,7 @@ def construct(self): ) # np.linalg.norm returns floating[Any] which is not compatible with float radius = cast(float, np.linalg.norm(p1 - center)) - return Circle(radius=radius, **kwargs).shift(center) + return Circle(center=center, radius=radius, **kwargs) class Dot(Circle): @@ -812,7 +859,7 @@ def __init__( **kwargs: Any, ) -> None: super().__init__( - arc_center=point, + center=point, radius=radius, stroke_width=stroke_width, fill_opacity=fill_opacity, @@ -822,7 +869,21 @@ def __init__( class AnnotationDot(Dot): - """A dot with bigger radius and bold stroke to annotate scenes.""" + """A dot with bigger radius and bold stroke to annotate scenes. + Parameters + ---------- + point + Location of the AnnotationDot. + radius + Radius of the AnnotationDot. + stroke_width + Stroke width of the AnnotationDot's circumference. + stroke_color + Stroke color of the AnnotationDot's circumference. + fill_color + Fill color of the AnnotationDot. + + """ def __init__( self, @@ -830,9 +891,12 @@ def __init__( stroke_width: float = 5, stroke_color: ParsableManimColor = WHITE, fill_color: ParsableManimColor = BLUE, + *, + point: Point3DLike = ORIGIN, **kwargs: Any, ) -> None: super().__init__( + point=point, radius=radius, stroke_width=stroke_width, stroke_color=stroke_color, @@ -846,6 +910,8 @@ class LabeledDot(Dot): Parameters ---------- + point + Location of the LabeledDot. label The label of the :class:`Dot`. This is rendered as :class:`~.MathTex` by default (i.e., when passing a :class:`str`), but other classes @@ -881,6 +947,8 @@ def __init__( label: str | SingleStringMathTex | Text | Tex, radius: float | None = None, buff: float = SMALL_BUFF, + *, + point: Point3DLike = ORIGIN, **kwargs: Any, ) -> None: if isinstance(label, str): @@ -894,7 +962,7 @@ def __init__( radius = buff + float( np.linalg.norm([rendered_label.width, rendered_label.height]) / 2 ) - super().__init__(radius=radius, **kwargs) + super().__init__(point=point, radius=radius, **kwargs) rendered_label.move_to(self.get_center()) self.add(rendered_label) @@ -904,6 +972,8 @@ class Ellipse(Circle): Parameters ---------- + center + The ellipse is centered at this point. width The horizontal width of the ellipse. height @@ -924,8 +994,17 @@ def construct(self): self.add(ellipse_group) """ - def __init__(self, width: float = 2, height: float = 1, **kwargs: Any) -> None: - super().__init__(**kwargs) + def __init__( + self, + width: float = 2, + height: float = 1, + *, + center: Point3DLike = ORIGIN, + **kwargs: Any, + ) -> None: + if "arc_center" in kwargs: + center = kwargs.pop("arc_center") + super().__init__(center=center, **kwargs) self.stretch_to_fit_width(width) self.stretch_to_fit_height(height) @@ -985,11 +1064,16 @@ def __init__( fill_opacity: float = 1, stroke_width: float = 0, color: ParsableManimColor = WHITE, + *, + arc_center: Point3DLike | None = None, **kwargs: Any, ) -> None: + if arc_center is None: + arc_center = ORIGIN self.inner_radius = inner_radius self.outer_radius = outer_radius super().__init__( + arc_center=arc_center, start_angle=start_angle, angle=angle, fill_opacity=fill_opacity, @@ -1009,12 +1093,15 @@ def generate_points(self) -> None: for radius in (self.inner_radius, self.outer_radius) ) outer_arc.reverse_points() + # Cairo and OpenGL renderer both use non-zero winding rule. + # That's why the points of outer_arc are reversed. + # If instead of outer_arc, the points of inner_arc is reversed, the output is exactly the same. self.append_points(inner_arc.points) self.add_line_to(outer_arc.points[0]) self.append_points(outer_arc.points) self.add_line_to(inner_arc.points[0]) - def init_points(self) -> None: + def init_points(self) -> None: # used by OpenGLRenderer. self.generate_points() @@ -1035,8 +1122,26 @@ def construct(self): self.add(sector, sector2) """ - def __init__(self, radius: float = 1, **kwargs: Any) -> None: - super().__init__(inner_radius=0, outer_radius=radius, **kwargs) + def __init__( + self, + radius: float = 1, + *, + arc_center: Point3DLike | None = None, + start_angle: float = 0, + angle: float = TAU / 4, + **kwargs: Any, + ) -> None: + if arc_center is None: + arc_center = ORIGIN + + super().__init__( + arc_center=arc_center, + start_angle=start_angle, + angle=angle, + inner_radius=0, + outer_radius=radius, + **kwargs, + ) class Annulus(Circle): @@ -1071,13 +1176,30 @@ def __init__( stroke_width: float = 0, color: ParsableManimColor = WHITE, mark_paths_closed: bool = False, + *, + center: Point3DLike | None = None, **kwargs: Any, ) -> None: + if mark_paths_closed is not False: + warnings.warn( + "mark_path_closed is an unused parameter ." + "It will be removed in future version of Manim Community.", + DeprecationWarning, + stacklevel=2, + ) + if "arc_center" in kwargs: + center = kwargs.pop("arc_center") + if center is None: + center = ORIGIN self.mark_paths_closed = mark_paths_closed # is this even used? self.inner_radius = inner_radius self.outer_radius = outer_radius super().__init__( - fill_opacity=fill_opacity, stroke_width=stroke_width, color=color, **kwargs + center=center, + fill_opacity=fill_opacity, + stroke_width=stroke_width, + color=color, + **kwargs, ) def generate_points(self) -> None: @@ -1085,6 +1207,9 @@ def generate_points(self) -> None: outer_circle = Circle(radius=self.outer_radius) inner_circle = Circle(radius=self.inner_radius) inner_circle.reverse_points() + # Cairo and OpenGL renderer both use non-zero winding rule. + # That's why the points of inner_circle are reversed. + # If instead of inner_circle, the points of outer_circle are reversed, the output is exactly the same. self.append_points(outer_circle.points) self.append_points(inner_circle.points) self.shift(self.arc_center) diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index 9f9b6eda59..047d2a56f4 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -278,8 +278,8 @@ def test_CurvedArrowCustomTip(scene): double_arrow = CurvedDoubleArrow( LEFT, RIGHT, - tip_shape_start=ArrowCircleTip, - tip_shape_end=ArrowSquareFilledTip, + tip_shape_at_start=ArrowCircleTip, + tip_shape_at_end=ArrowSquareFilledTip, ) scene.add(arrow, double_arrow)