Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
205 changes: 166 additions & 39 deletions manim/mobject/geometry/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"RightAngle",
]

import warnings
from typing import TYPE_CHECKING, Any, Literal, cast

import numpy as np
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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`

Expand All @@ -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:

Expand All @@ -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):
Expand All @@ -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::
Expand Down Expand Up @@ -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):
Expand Down
12 changes: 10 additions & 2 deletions manim/scene/vector_space_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
Binary file not shown.
4 changes: 2 additions & 2 deletions tests/test_graphical_units/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading