diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 2cd7aff807..b363892f68 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -14,6 +14,7 @@ "RightAngle", ] +import warnings from typing import TYPE_CHECKING, Any, Literal, cast import numpy as np @@ -193,7 +194,10 @@ def _pointify( return mob.get_center() else: return mob.get_boundary_point(direction) - return np.array(mob_or_point) + point = np.array(mob_or_point) + if len(point) == 2: + point = np.hstack([point, 0]) + return point def set_path_arc(self, new_value: float) -> None: self.path_arc = new_value @@ -503,10 +507,16 @@ class Arrow(Line): ---------- args Arguments to be passed to :class:`Line`. + start + Start point of the Arrow. + end + End point of the Arrow. stroke_width The thickness of the arrow. Influenced by :attr:`max_stroke_width_to_length_ratio`. buff The distance of the arrow from its start and end points. + tip_shape + Shape of the tip at the end of the Arrow. max_tip_length_to_length_ratio :attr:`tip_length` scales with the length of the arrow. Increasing this ratio raises the max value of :attr:`tip_length`. max_stroke_width_to_length_ratio @@ -587,16 +597,27 @@ def construct(self): def __init__( self, *args: Any, + start: Point3DLike = LEFT, + end: Point3DLike = RIGHT, stroke_width: float = 6, buff: float = MED_SMALL_BUFF, + tip_shape: type[ArrowTip] = ArrowTriangleFilledTip, max_tip_length_to_length_ratio: float = 0.25, max_stroke_width_to_length_ratio: float = 5, **kwargs: Any, ) -> None: self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio self.max_stroke_width_to_length_ratio = max_stroke_width_to_length_ratio - tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip) - super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc] + if len(args) == 0: + if len(start) == 2: + start = np.hstack([start, 0]) + if len(end) == 2: + end = np.hstack([end, 0]) + super().__init__( + start=start, end=end, buff=buff, stroke_width=stroke_width, **kwargs + ) + else: + super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc] # TODO, should this be affected when # Arrow.set_stroke is called? self.initial_stroke_width = self.stroke_width @@ -707,6 +728,9 @@ def _set_stroke_width_from_length(self) -> Self: return self +_UNSET = object() + + class Vector(Arrow): """A vector specialized for use in graphs. @@ -718,9 +742,15 @@ class Vector(Arrow): Parameters ---------- direction - The direction of the arrow. + The direction of the vector. + start + Starting point of the Vector. Default is ORIGIN. + end + End point of the vector. If it is 'None', then it is set to 'RIGHT'. buff - The distance of the vector from its endpoints. + The distance to shorten the vector from both ends. See buff parameter's docstring in :class:`~.Line`. + tip_shape + Shape of the tip. kwargs Additional arguments to be passed to :class:`Arrow` @@ -739,43 +769,90 @@ def construct(self): def __init__( self, - direction: Vector2DLike | Vector3DLike = RIGHT, + direction: Vector2DLike | Vector3DLike = _UNSET, + start: Point3DLike = ORIGIN, + end: Point3DLike | None = None, buff: float = 0, + tip_shape: type[ArrowTip] = ArrowTriangleFilledTip, **kwargs: Any, ) -> None: self.buff = buff - if len(direction) == 2: - direction = np.hstack([direction, 0]) - - super().__init__(ORIGIN, direction, buff=buff, **kwargs) + if direction is not _UNSET and end is not None: + raise ValueError( + "You can specify either the 'end' or the 'direction' of the Vector at the same time, not both." + ) + if direction is not _UNSET: + if len(direction) == 2: + direction = np.hstack([direction, 0]) + end = start + direction + elif end is None: + end = RIGHT + super().__init__(start=start, end=end, buff=buff, tip_shape=tip_shape, **kwargs) def coordinate_label( self, - integer_labels: bool = True, + show_labels_as_integers: bool = False, n_dim: int = 2, color: ParsableManimColor | None = None, + show_start_label: bool = False, + show_end_label: bool = True, + decimals: int = 2, + buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + buff_multiplier: float = 3.75, **kwargs: Any, - ) -> Matrix: - """Creates a label based on the coordinates of the vector. + ) -> Matrix | VGroup: + """Creates start_label and end_label based on the coordinate of start and end point of the vector. Parameters ---------- - integer_labels + show_labels_as_integers Whether or not to round the coordinates to integers. n_dim The number of dimensions of the vector. color - Sets the color of label, optional. + Sets the color of label. Default color is WHITE. + show_start_label + Shows the coordinate of starting point of the vector, if it is set to True. Default value is False. + show_end_label + Shows the coordinate of the end point of the vector, if it is set to True. Default value is True. + decimals + number of digits in decimal place, if 'show_labels_as_integers' is set to False. Default is 2. + buff + buffer distance along the vector's direction at which the coordinate is to be shown. + buff_multiplier + Stretches the buff. kwargs Additional arguments to be passed to :class:`~.Matrix`. Returns ------- - :class:`~.Matrix` + :class:`~.Matrix` | VGroup The label. + Return type is Matrix, when only 1 of the label out of start_label and end_label is to be shown. + Return type is VGroup, when both the labels are to be shown. Examples -------- + class VectorCoordinateLabel1(Scene): + def construct(self): + plane = NumberPlane() + vec = Vector(start = [-2,-2,0], direction = [3,2.6]) + label = vec.coordinate_label(color = RED, show_start_label=True) + self.add(plane, vec, label) + vec.add_updater(lambda x,dt: x.rotate(2*PI*dt*0.2, about_point= x.get_center())) + def upd_label(mob): + mob.become(vec.coordinate_label(color = RED, show_start_label=True)) + label.add_updater(upd_label) + self.wait(5) + + class VectorCoordinateLabel2(Scene): + def construct(self): + plane = NumberPlane() + vec_1 = Vector(color = ORANGE, tip_shape=StealthTip) + vec_2 = Vector(start = [-1,-2], end = [3,2], color = GREEN, tip_shape= StealthTip) + label_2 = vec_2.coordinate_label(color = RED, show_start_label= True) + self.add(plane,vec_1, vec_2, label_2) + .. manim:: VectorCoordinateLabel :save_last_frame: @@ -790,26 +867,54 @@ def construct(self): self.add(plane, vec_1, vec_2, label_1, label_2) """ + if not show_start_label and not show_end_label: + raise ValueError( + "At least one of show_start_label or show_end_label must be set to True" + ) + # avoiding circular imports from ..matrix import Matrix - vect = np.array(self.get_end()) - if integer_labels: - vect = np.round(vect).astype(int) - vect = vect[:n_dim] - vect = vect.reshape((n_dim, 1)) - label = Matrix(vect, **kwargs) - label.scale(LARGE_BUFF - 0.2) - - shift_dir = np.array(self.get_end()) - if shift_dir[0] >= 0: # Pointing right - shift_dir -= label.get_left() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * LEFT - else: # Pointing left - shift_dir -= label.get_right() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * RIGHT - label.shift(shift_dir) + start_coordinate = np.array(self.get_start()) + end_coordinate = np.array(self.get_end()) + if show_labels_as_integers: + start_coordinate = np.round(start_coordinate).astype(int) + end_coordinate = np.round(end_coordinate).astype(int) + start_coordinate = start_coordinate[:n_dim] + end_coordinate = end_coordinate[:n_dim] + start_coordinate = np.round(start_coordinate, decimals=decimals).reshape( + (n_dim, 1) + ) + end_coordinate = np.round(end_coordinate, decimals=decimals).reshape((n_dim, 1)) + start_label = Matrix(start_coordinate, **kwargs) + end_label = Matrix(end_coordinate, **kwargs) + start_label.scale(LARGE_BUFF - 0.2) + end_label.scale(LARGE_BUFF - 0.2) + + vector_direction = self.get_unit_vector() + start_label_shift_dir = ( + self.get_start() + - vector_direction * buff_multiplier * buff + - start_label.get_center() + ) + end_label_shift_dir = ( + self.get_end() + + vector_direction * buff_multiplier * buff + - end_label.get_center() + ) + + start_label.shift(start_label_shift_dir) + end_label.shift(end_label_shift_dir) + if color is not None: - label.set_color(color) - return label + start_label.set_color(color) + end_label.set_color(color) + + if show_start_label and show_end_label: + return VGroup(start_label, end_label) + elif show_end_label: + return end_label + return start_label class DoubleArrow(Arrow): @@ -818,9 +923,9 @@ class DoubleArrow(Arrow): Parameters ---------- args - Arguments to be passed to :class:`Arrow` + Positional arguments to be passed to :class:`Arrow` kwargs - Additional arguments to be passed to :class:`Arrow` + Keyword arguments to be passed to :class:`Arrow` .. seealso:: @@ -857,12 +962,34 @@ def construct(self): self.add(box, d1, d2, d3) """ - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + *args: Any, + start: Point3DLike = LEFT, + end: Point3DLike = RIGHT, + tip_shape_at_start: type[ArrowTip] = ArrowTriangleFilledTip, + tip_shape_at_end: type[ArrowTip] = ArrowTriangleFilledTip, + **kwargs: Any, + ) -> None: + if "tip_shape_start" in kwargs: + warnings.warn( + "tip_shape_start is deprecated. Use tip_shape_at_start instead.", + DeprecationWarning, + stacklevel=2, + ) + tip_shape_at_start = kwargs.pop("tip_shape_start") if "tip_shape_end" in kwargs: - kwargs["tip_shape"] = kwargs.pop("tip_shape_end") - tip_shape_start = kwargs.pop("tip_shape_start", ArrowTriangleFilledTip) - super().__init__(*args, **kwargs) - self.add_tip(at_start=True, tip_shape=tip_shape_start) + warnings.warn( + "tip_shape_end is deprecated. Use tip_shape_at_end instead.", + DeprecationWarning, + stacklevel=2, + ) + tip_shape_at_end = kwargs.pop("tip_shape_end") + if len(args) == 0: + super().__init__(start=start, end=end, tip_shape=tip_shape_at_end, **kwargs) + else: + super().__init__(*args, tip_shape=tip_shape_at_end, **kwargs) + self.add_tip(at_start=True, tip_shape=tip_shape_at_start) class Angle(VMobject, metaclass=ConvertToOpenGL): diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index 56afea2095..0f40615d63 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -229,7 +229,8 @@ def write_vector_coordinates(self, vector: Vector, **kwargs: Any) -> Matrix: :class:`.Matrix` The column matrix representing the vector. """ - coords: Matrix = vector.coordinate_label(**kwargs) + coords = vector.coordinate_label(**kwargs) + assert isinstance(coords, Matrix) self.play(Write(coords)) return coords @@ -516,14 +517,21 @@ def vector_to_coords( else: arrow = Vector(vector) show_creation = True - array = arrow.coordinate_label(integer_labels=integer_labels) + array = arrow.coordinate_label( + show_labels_as_integers=integer_labels, + show_start_label=False, + show_end_label=True, + ) + assert isinstance(array, Matrix) x_line = Line(ORIGIN, vector[0] * RIGHT) y_line = Line(x_line.get_end(), arrow.get_end()) x_line.set_color(X_COLOR) y_line.set_color(Y_COLOR) temp = array.get_entries() x_coord = temp.submobjects[0] + assert isinstance(x_coord, MathTex) y_coord = temp.submobjects[1] + assert isinstance(y_coord, MathTex) x_coord_start = self.position_x_coordinate(x_coord.copy(), x_line, vector) y_coord_start = self.position_y_coordinate(y_coord.copy(), y_line, vector) brackets = array.get_brackets() diff --git a/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz b/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz index 8cec4d850a..bb8e09c813 100644 Binary files a/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz and b/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz differ diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index 9f9b6eda59..de47a300b9 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -35,8 +35,8 @@ def test_CustomDoubleArrow(scene): a = DoubleArrow( np.array([-1, -1, 0]), np.array([1, 1, 0]), - tip_shape_start=ArrowCircleTip, - tip_shape_end=ArrowSquareFilledTip, + tip_shape_at_start=ArrowCircleTip, + tip_shape_at_end=ArrowSquareFilledTip, ) scene.add(a)