Source code for manim.mobject.three_d.three_dimensions

"""Three-dimensional mobjects."""

from __future__ import annotations

__all__ = [
    "ThreeDVMobject",
    "Surface",
    "Sphere",
    "Dot3D",
    "Cube",
    "Prism",
    "Cone",
    "Arrow3D",
    "Cylinder",
    "Line3D",
    "Torus",
]

from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, Literal, Self

import numpy as np

from manim import config, logger
from manim.constants import *
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.polygram import Square
from manim.mobject.mobject import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
from manim.utils.color import (
    BLUE,
    BLUE_D,
    BLUE_E,
    LIGHT_GREY,
    WHITE,
    ManimColor,
    ParsableManimColor,
    interpolate_color,
)
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector

if TYPE_CHECKING:
    from manim.mobject.graphing.coordinate_systems import ThreeDAxes
    from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike


[docs] class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL): u_index: int v_index: int u1: float u2: float v1: float v2: float def __init__(self, shade_in_3d: bool = True, **kwargs: Any): super().__init__(shade_in_3d=shade_in_3d, **kwargs)
[docs] class Surface(VGroup, metaclass=ConvertToOpenGL): """Creates a Parametric Surface using a checkerboard pattern. Parameters ---------- func The function defining the :class:`Surface`. u_range The range of the ``u`` variable: ``(u_min, u_max)``. v_range The range of the ``v`` variable: ``(v_min, v_max)``. resolution The number of samples taken of the :class:`Surface`. A tuple can be used to define different resolutions for ``u`` and ``v`` respectively. fill_color The color of the :class:`Surface`. Ignored if ``checkerboard_colors`` is set. fill_opacity The opacity of the :class:`Surface`, from 0 being fully transparent to 1 being fully opaque. Defaults to 1. checkerboard_colors ng individual faces alternating colors. Overrides ``fill_color``. stroke_color Color of the stroke surrounding each face of :class:`Surface`. stroke_width Width of the stroke surrounding each face of :class:`Surface`. Defaults to 0.5. should_make_jagged Changes the anchor mode of the Bézier curves from smooth to jagged. Defaults to ``False``. Examples -------- .. manim:: ParaSurface :save_last_frame: class ParaSurface(ThreeDScene): def func(self, u, v): return np.array([np.cos(u) * np.cos(v), np.cos(u) * np.sin(v), u]) def construct(self): axes = ThreeDAxes(x_range=[-4,4], x_length=8) surface = Surface( lambda u, v: axes.c2p(*self.func(u, v)), u_range=[-PI, PI], v_range=[0, TAU], resolution=8, ) self.set_camera_orientation(theta=70 * DEGREES, phi=75 * DEGREES) self.add(axes, surface) """ def __init__( self, func: Callable[[float, float], np.ndarray], u_range: tuple[float, float] = (0, 1), v_range: tuple[float, float] = (0, 1), resolution: int | Sequence[int] = 32, surface_piece_config: dict = {}, fill_color: ParsableManimColor = BLUE_D, fill_opacity: float = 1.0, checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = [ BLUE_D, BLUE_E, ], stroke_color: ParsableManimColor = LIGHT_GREY, stroke_width: float = 0.5, should_make_jagged: bool = False, pre_function_handle_to_anchor_scale_factor: float = 0.00001, **kwargs: Any, ) -> None: self.u_range = u_range self.v_range = v_range super().__init__( fill_color=fill_color, fill_opacity=fill_opacity, stroke_color=stroke_color, stroke_width=stroke_width, **kwargs, ) self.resolution = resolution self.surface_piece_config = surface_piece_config self.checkerboard_colors: list[ManimColor] | Literal[False] if checkerboard_colors is False: self.checkerboard_colors = checkerboard_colors else: self.checkerboard_colors = [ManimColor(i) for i in checkerboard_colors] self.should_make_jagged = should_make_jagged self.pre_function_handle_to_anchor_scale_factor = ( pre_function_handle_to_anchor_scale_factor ) self._func = func self._setup_in_uv_space() self.apply_function(lambda p: func(p[0], p[1])) if self.should_make_jagged: self.make_jagged() def func(self, u: float, v: float) -> np.ndarray: return self._func(u, v) def _get_u_values_and_v_values(self) -> tuple[np.ndarray, np.ndarray]: if isinstance(self.resolution, int): u_res = v_res = self.resolution else: u_res, v_res = self.resolution u_values = np.linspace(*self.u_range, u_res + 1) v_values = np.linspace(*self.v_range, v_res + 1) return u_values, v_values def _setup_in_uv_space(self) -> None: u_values, v_values = self._get_u_values_and_v_values() faces = VGroup() for i in range(len(u_values) - 1): for j in range(len(v_values) - 1): u1, u2 = u_values[i : i + 2] v1, v2 = v_values[j : j + 2] face = ThreeDVMobject() face.set_points_as_corners( [ [u1, v1, 0], [u2, v1, 0], [u2, v2, 0], [u1, v2, 0], [u1, v1, 0], ], ) faces.add(face) face.u_index = i face.v_index = j face.u1 = u1 face.u2 = u2 face.v1 = v1 face.v2 = v2 faces.set_fill(color=self.fill_color, opacity=self.fill_opacity) faces.set_stroke( color=self.stroke_color, width=self.stroke_width, opacity=self.stroke_opacity, ) self.add(*faces) if self.checkerboard_colors: self.set_fill_by_checkerboard(*self.checkerboard_colors)
[docs] def set_fill_by_checkerboard( self, *colors: ParsableManimColor, opacity: float | None = None ) -> Self: """Sets the fill_color of each face of :class:`Surface` in an alternating pattern. Parameters ---------- colors List of colors for alternating pattern. opacity The fill_opacity of :class:`Surface`, from 0 being fully transparent to 1 being fully opaque. Returns ------- :class:`~.Surface` The parametric surface with an alternating pattern. """ n_colors = len(colors) for face in self: c_index = (face.u_index + face.v_index) % n_colors face.set_fill(colors[c_index], opacity=opacity) return self
[docs] def set_fill_by_value( self, axes: ThreeDAxes, colorscale: Iterable[ParsableManimColor] | Iterable[tuple[ParsableManimColor, float]] | None = None, axis: int = 2, **kwargs: Any, ) -> Self: """Sets the color of each mobject of a parametric surface to a color relative to its axis-value. Parameters ---------- axes The axes for the parametric surface, which will be used to map axis-values to colors. colorscale A list of colors, ordered from lower axis-values to higher axis-values. If a list of tuples is passed containing colors paired with numbers, then those numbers will be used as the pivots. axis The chosen axis to use for the color mapping. (0 = x, 1 = y, 2 = z) Returns ------- :class:`~.Surface` The parametric surface with a gradient applied by value. For chaining. Examples -------- .. manim:: FillByValueExample :save_last_frame: class FillByValueExample(ThreeDScene): def construct(self): resolution_fa = 8 self.set_camera_orientation(phi=75 * DEGREES, theta=-160 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) def param_surface(u, v): x = u y = v z = np.sin(x) * np.cos(y) return z surface_plane = Surface( lambda u, v: axes.c2p(u, v, param_surface(u, v)), resolution=(resolution_fa, resolution_fa), v_range=[0, 5], u_range=[0, 5], ) surface_plane.set_style(fill_opacity=1) surface_plane.set_fill_by_value(axes=axes, colorscale=[(RED, -0.5), (YELLOW, 0), (GREEN, 0.5)], axis=2) self.add(axes, surface_plane) """ if "colors" in kwargs and colorscale is None: colorscale = kwargs.pop("colors") if kwargs: raise ValueError( "Unsupported keyword argument(s): " f"{', '.join(str(key) for key in kwargs)}" ) if colorscale is None: logger.warning( "The value passed to the colorscale keyword argument was None, " "the surface fill color has not been changed" ) return self colorscale_list = list(colorscale) ranges = [axes.x_range, axes.y_range, axes.z_range] assert isinstance(colorscale_list, list) new_colors: list[ManimColor] if type(colorscale_list[0]) is tuple and len(colorscale_list[0]) == 2: new_colors, pivots = [ [ManimColor(i) for i, j in colorscale_list], [j for i, j in colorscale_list], ] else: new_colors = [ManimColor(i) for i in colorscale_list] current_range = ranges[axis] assert current_range is not None pivot_min = current_range[0] pivot_max = current_range[1] pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1) pivots = np.arange( start=pivot_min, stop=pivot_max + pivot_frequency, step=pivot_frequency, ) for mob in self.family_members_with_points(): axis_value = axes.point_to_coords(mob.get_midpoint())[axis] if axis_value <= pivots[0]: mob.set_color(new_colors[0]) elif axis_value >= pivots[-1]: mob.set_color(new_colors[-1]) else: for i, pivot in enumerate(pivots): if pivot > axis_value: color_index = (axis_value - pivots[i - 1]) / ( pivots[i] - pivots[i - 1] ) color_index = min(color_index, 1) mob_color = interpolate_color( new_colors[i - 1], new_colors[i], color_index, ) if config.renderer == RendererType.OPENGL: assert isinstance(mob, OpenGLMobject) mob.set_color(mob_color, recurse=False) elif config.renderer == RendererType.CAIRO: mob.set_color(mob_color, family=False) break return self
# Specific shapes
[docs] class Sphere(Surface): """A three-dimensional sphere. Parameters ---------- center Center of the :class:`Sphere`. radius The radius of the :class:`Sphere`. resolution The number of samples taken of the :class:`Sphere`. A tuple can be used to define different resolutions for ``u`` and ``v`` respectively. u_range The range of the ``u`` variable: ``(u_min, u_max)``. v_range The range of the ``v`` variable: ``(v_min, v_max)``. Examples -------- .. manim:: ExampleSphere :save_last_frame: class ExampleSphere(ThreeDScene): def construct(self): self.set_camera_orientation(phi=PI / 6, theta=PI / 6) sphere1 = Sphere( center=(3, 0, 0), radius=1, resolution=(20, 20), u_range=[0.001, PI - 0.001], v_range=[0, TAU] ) sphere1.set_color(RED) self.add(sphere1) sphere2 = Sphere(center=(-1, -3, 0), radius=2, resolution=(18, 18)) sphere2.set_color(GREEN) self.add(sphere2) sphere3 = Sphere(center=(-1, 2, 0), radius=2, resolution=(16, 16)) sphere3.set_color(BLUE) self.add(sphere3) """ def __init__( self, center: Point3DLike = ORIGIN, radius: float = 1, resolution: int | Sequence[int] | None = None, u_range: tuple[float, float] = (0, TAU), v_range: tuple[float, float] = (0, PI), **kwargs: Any, ) -> None: if config.renderer == RendererType.OPENGL: res_value = (101, 51) elif config.renderer == RendererType.CAIRO: res_value = (24, 12) else: raise Exception("Unknown renderer") resolution = resolution if resolution is not None else res_value self.radius = radius super().__init__( self.func, resolution=resolution, u_range=u_range, v_range=v_range, **kwargs, ) self.shift(center)
[docs] def func(self, u: float, v: float) -> Point3D: """The z values defining the :class:`Sphere` being plotted. Returns ------- :class:`Point3D` The z values defining the :class:`Sphere`. """ return self.radius * np.array( [np.cos(u) * np.sin(v), np.sin(u) * np.sin(v), -np.cos(v)], )
[docs] class Dot3D(Sphere): """A spherical dot. Parameters ---------- point The location of the dot. radius The radius of the dot. color The color of the :class:`Dot3D`. resolution The number of samples taken of the :class:`Dot3D`. A tuple can be used to define different resolutions for ``u`` and ``v`` respectively. Examples -------- .. manim:: Dot3DExample :save_last_frame: class Dot3DExample(ThreeDScene): def construct(self): self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) axes = ThreeDAxes() dot_1 = Dot3D(point=axes.coords_to_point(0, 0, 1), color=RED) dot_2 = Dot3D(point=axes.coords_to_point(2, 0, 0), radius=0.1, color=BLUE) dot_3 = Dot3D(point=[0, 0, 0], radius=0.1, color=ORANGE) self.add(axes, dot_1, dot_2,dot_3) """ def __init__( self, point: Point3D = ORIGIN, radius: float = DEFAULT_DOT_RADIUS, color: ParsableManimColor = WHITE, resolution: int | tuple[int, int] | None = (8, 8), **kwargs: Any, ) -> None: super().__init__(center=point, radius=radius, resolution=resolution, **kwargs) self.set_color(color)
[docs] class Cube(VGroup): """A three-dimensional cube. Parameters ---------- side_length Length of each side of the :class:`Cube`. fill_opacity The opacity of the :class:`Cube`, from 0 being fully transparent to 1 being fully opaque. Defaults to 0.75. fill_color The color of the :class:`Cube`. stroke_width The width of the stroke surrounding each face of the :class:`Cube`. Examples -------- .. manim:: CubeExample :save_last_frame: class CubeExample(ThreeDScene): def construct(self): self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) axes = ThreeDAxes() cube = Cube(side_length=3, fill_opacity=0.7, fill_color=BLUE) self.add(cube) """ def __init__( self, side_length: float = 2, fill_opacity: float = 0.75, fill_color: ParsableManimColor = BLUE, stroke_width: float = 0, **kwargs: Any, ) -> None: self.side_length = side_length super().__init__( fill_color=fill_color, fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs, )
[docs] def generate_points(self) -> None: """Creates the sides of the :class:`Cube`.""" for vect in IN, OUT, LEFT, RIGHT, UP, DOWN: face = Square( side_length=self.side_length, shade_in_3d=True, joint_type=LineJointType.BEVEL, ) face.flip() face.shift(self.side_length * OUT / 2.0) face.apply_matrix(z_to_vector(vect)) self.add(face)
def init_points(self) -> None: self.generate_points()
[docs] class Prism(Cube): """A right rectangular prism (or rectangular cuboid). Defined by the length of each side in ``[x, y, z]`` format. Parameters ---------- dimensions Dimensions of the :class:`Prism` in ``[x, y, z]`` format. Examples -------- .. manim:: ExamplePrism :save_last_frame: class ExamplePrism(ThreeDScene): def construct(self): self.set_camera_orientation(phi=60 * DEGREES, theta=150 * DEGREES) prismSmall = Prism(dimensions=[1, 2, 3]).rotate(PI / 2) prismLarge = Prism(dimensions=[1.5, 3, 4.5]).move_to([2, 0, 0]) self.add(prismSmall, prismLarge) """ def __init__( self, dimensions: Vector3DLike = [3, 2, 1], **kwargs: Any, ) -> None: self.dimensions = dimensions super().__init__(**kwargs)
[docs] def generate_points(self) -> None: """Creates the sides of the :class:`Prism`.""" super().generate_points() for dim, value in enumerate(self.dimensions): self.rescale_to_fit(value, dim, stretch=True)
[docs] class Cone(Surface): """A circular cone. Can be defined using 2 parameters: its height, and its base radius. The polar angle, theta, can be calculated using arctan(base_radius / height) The spherical radius, r, is calculated using the pythagorean theorem. Parameters ---------- base_radius The base radius from which the cone tapers. height The height measured from the plane formed by the base_radius to the apex of the cone. direction The direction of the apex. show_base Whether to show the base plane or not. v_range The azimuthal angle to start and end at. u_min The radius at the apex. checkerboard_colors Show checkerboard grid texture on the cone. Examples -------- .. manim:: ExampleCone :save_last_frame: class ExampleCone(ThreeDScene): def construct(self): axes = ThreeDAxes() cone = Cone(direction=X_AXIS+Y_AXIS+2*Z_AXIS, resolution=8) self.set_camera_orientation(phi=5*PI/11, theta=PI/9) self.add(axes, cone) """ def __init__( self, base_radius: float = 1, height: float = 1, direction: Vector3DLike = Z_AXIS, show_base: bool = False, v_range: tuple[float, float] = (0, TAU), u_min: float = 0, checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = False, **kwargs: Any, ) -> None: self.direction = np.array(direction) self.theta = PI - np.arctan(base_radius / height) super().__init__( self.func, v_range=v_range, u_range=(u_min, np.sqrt(base_radius**2 + height**2)), checkerboard_colors=checkerboard_colors, **kwargs, ) # used for rotations self.new_height = height self._current_theta = 0 self._current_phi = 0 self.base_circle = Circle( radius=base_radius, color=self.fill_color, fill_opacity=self.fill_opacity, stroke_width=0, ) self.base_circle.shift(height * IN) self._set_start_and_end_attributes(direction) if show_base: self.add(self.base_circle) self._rotate_to_direction()
[docs] def func(self, u: float, v: float) -> Point3D: """Converts from spherical coordinates to cartesian. Parameters ---------- u The radius. v The azimuthal angle. Returns ------- :class:`numpy.array` Points defining the :class:`Cone`. """ r = u phi = v return np.array( [ r * np.sin(self.theta) * np.cos(phi), r * np.sin(self.theta) * np.sin(phi), r * np.cos(self.theta), ], )
[docs] def get_start(self) -> Point3D: return self.start_point.get_center()
[docs] def get_end(self) -> Point3D: return self.end_point.get_center()
def _rotate_to_direction(self) -> None: x, y, z = self.direction r = np.sqrt(x**2 + y**2 + z**2) theta = np.arccos(z / r) if r > 0 else 0 if x == 0: if y == 0: # along the z axis phi = 0 else: phi = np.arctan(np.inf) if y < 0: phi += PI else: phi = np.arctan(y / x) if x < 0: phi += PI # Undo old rotation (in reverse order) self.rotate(-self._current_phi, Z_AXIS, about_point=ORIGIN) self.rotate(-self._current_theta, Y_AXIS, about_point=ORIGIN) # Do new rotation self.rotate(theta, Y_AXIS, about_point=ORIGIN) self.rotate(phi, Z_AXIS, about_point=ORIGIN) # Store values self._current_theta = theta self._current_phi = phi
[docs] def set_direction(self, direction: Vector3DLike) -> None: """Changes the direction of the apex of the :class:`Cone`. Parameters ---------- direction The direction of the apex. """ self.direction = np.array(direction) self._rotate_to_direction()
[docs] def get_direction(self) -> Vector3D: """Returns the current direction of the apex of the :class:`Cone`. Returns ------- direction : :class:`numpy.array` The direction of the apex. """ return self.direction
def _set_start_and_end_attributes(self, direction: Vector3D) -> None: normalized_direction = direction * np.linalg.norm(direction) start = self.base_circle.get_center() end = start + normalized_direction * self.new_height self.start_point = VectorizedPoint(start) self.end_point = VectorizedPoint(end) self.add(self.start_point, self.end_point)
[docs] class Cylinder(Surface): """A cylinder, defined by its height, radius and direction, Parameters ---------- radius The radius of the cylinder. height The height of the cylinder. direction The direction of the central axis of the cylinder. v_range The height along the height axis (given by direction) to start and end on. show_ends Whether to show the end caps or not. resolution The number of samples taken of the :class:`Cylinder`. A tuple can be used to define different resolutions for ``u`` and ``v`` respectively. Examples -------- .. manim:: ExampleCylinder :save_last_frame: class ExampleCylinder(ThreeDScene): def construct(self): axes = ThreeDAxes() cylinder = Cylinder(radius=2, height=3) self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, cylinder) """ def __init__( self, radius: float = 1, height: float = 2, direction: Vector3DLike = Z_AXIS, v_range: tuple[float, float] = (0, TAU), show_ends: bool = True, resolution: int | tuple[int, int] = (24, 24), **kwargs: Any, ) -> None: self._height = height self.radius = radius super().__init__( self.func, resolution=resolution, u_range=(-self._height / 2, self._height / 2), v_range=v_range, **kwargs, ) if show_ends: self.add_bases() self._current_phi = 0 self._current_theta = 0 self.set_direction(direction)
[docs] def func(self, u: float, v: float) -> np.ndarray: """Converts from cylindrical coordinates to cartesian. Parameters ---------- u The height. v The azimuthal angle. Returns ------- :class:`numpy.ndarray` Points defining the :class:`Cylinder`. """ height = u phi = v r = self.radius return np.array([r * np.cos(phi), r * np.sin(phi), height])
[docs] def add_bases(self) -> None: """Adds the end caps of the cylinder.""" opacity: float if config.renderer == RendererType.OPENGL: assert isinstance(self, OpenGLMobject) color = self.color opacity = self.opacity elif config.renderer == RendererType.CAIRO: color = self.fill_color opacity = self.fill_opacity self.base_top = Circle( radius=self.radius, color=color, fill_opacity=opacity, shade_in_3d=True, stroke_width=0, ) self.base_top.shift(self.u_range[1] * IN) self.base_bottom = Circle( radius=self.radius, color=color, fill_opacity=opacity, shade_in_3d=True, stroke_width=0, ) self.base_bottom.shift(self.u_range[0] * IN) self.add(self.base_top, self.base_bottom)
def _rotate_to_direction(self) -> None: x, y, z = self.direction r = np.sqrt(x**2 + y**2 + z**2) theta = np.arccos(z / r) if r > 0 else 0 if x == 0: if y == 0: # along the z axis phi = 0 else: # along the x axis phi = np.arctan(np.inf) if y < 0: phi += PI else: phi = np.arctan(y / x) if x < 0: phi += PI # undo old rotation (in reverse direction) self.rotate(-self._current_phi, Z_AXIS, about_point=ORIGIN) self.rotate(-self._current_theta, Y_AXIS, about_point=ORIGIN) # do new rotation self.rotate(theta, Y_AXIS, about_point=ORIGIN) self.rotate(phi, Z_AXIS, about_point=ORIGIN) # store new values self._current_theta = theta self._current_phi = phi
[docs] def set_direction(self, direction: Vector3DLike) -> None: """Sets the direction of the central axis of the :class:`Cylinder`. Parameters ---------- direction : :class:`numpy.array` The direction of the central axis of the :class:`Cylinder`. """ # if get_norm(direction) is get_norm(self.direction): # pass self.direction = direction self._rotate_to_direction()
[docs] def get_direction(self) -> np.ndarray: """Returns the direction of the central axis of the :class:`Cylinder`. Returns ------- direction : :class:`numpy.array` The direction of the central axis of the :class:`Cylinder`. """ return self.direction
[docs] class Line3D(Cylinder): """A cylindrical line, for use in ThreeDScene. Parameters ---------- start The start point of the line. end The end point of the line. thickness The thickness of the line. color The color of the line. resolution The resolution of the line. By default this value is the number of points the line will sampled at. If you want the line to also come out checkered, use a tuple. For example, for a line made of 24 points with 4 checker points on each cylinder, pass the tuple (4, 24). Examples -------- .. manim:: ExampleLine3D :save_last_frame: class ExampleLine3D(ThreeDScene): def construct(self): axes = ThreeDAxes() line = Line3D(start=np.array([0, 0, 0]), end=np.array([2, 2, 2])) self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, line) """ def __init__( self, start: Point3DLike = LEFT, end: Point3DLike = RIGHT, thickness: float = 0.02, color: ParsableManimColor | None = None, resolution: int | tuple[int, int] = 24, **kwargs: Any, ): self.thickness = thickness self.resolution: tuple[int, int] = ( (2, resolution) if isinstance(resolution, int) else resolution ) start = np.array(start, dtype=np.float64) end = np.array(end, dtype=np.float64) self.set_start_and_end_attrs(start, end, **kwargs) if color is not None: self.set_color(color)
[docs] def set_start_and_end_attrs( self, start: Point3DLike, end: Point3DLike, **kwargs: Any ) -> None: """Sets the start and end points of the line. If either ``start`` or ``end`` are :class:`Mobjects <.Mobject>`, this gives their centers. Parameters ---------- start Starting point or :class:`Mobject`. end Ending point or :class:`Mobject`. """ rough_start = self.pointify(start) rough_end = self.pointify(end) self.vect = rough_end - rough_start self.length = np.linalg.norm(self.vect) self.direction: Vector3D = normalize(self.vect) # Now that we know the direction between them, # we can the appropriate boundary point from # start and end, if they're mobjects self.start = self.pointify(start, self.direction) self.end = self.pointify(end, -self.direction) super().__init__( height=np.linalg.norm(self.vect), radius=self.thickness, direction=self.direction, resolution=self.resolution, **kwargs, ) self.shift((self.start + self.end) / 2)
[docs] def pointify( self, mob_or_point: Mobject | Point3DLike, direction: Vector3DLike | None = None, ) -> Point3D: """Gets a point representing the center of the :class:`Mobjects <.Mobject>`. Parameters ---------- mob_or_point :class:`Mobjects <.Mobject>` or point whose center should be returned. direction If an edge of a :class:`Mobjects <.Mobject>` should be returned, the direction of the edge. Returns ------- :class:`numpy.array` Center of the :class:`Mobjects <.Mobject>` or point, or edge if direction is given. """ if isinstance(mob_or_point, (Mobject, OpenGLMobject)): mob = mob_or_point if direction is None: return mob.get_center() else: return mob.get_boundary_point(direction) return np.array(mob_or_point)
[docs] def get_start(self) -> Point3D: """Returns the starting point of the :class:`Line3D`. Returns ------- start : :class:`numpy.array` Starting point of the :class:`Line3D`. """ return self.start
[docs] def get_end(self) -> Point3D: """Returns the ending point of the :class:`Line3D`. Returns ------- end : :class:`numpy.array` Ending point of the :class:`Line3D`. """ return self.end
[docs] @classmethod def parallel_to( cls, line: Line3D, point: Point3DLike = ORIGIN, length: float = 5, **kwargs: Any, ) -> Line3D: """Returns a line parallel to another line going through a given point. Parameters ---------- line The line to be parallel to. point The point to pass through. length Length of the parallel line. kwargs Additional parameters to be passed to the class. Returns ------- :class:`Line3D` Line parallel to ``line``. Examples -------- .. manim:: ParallelLineExample :save_last_frame: class ParallelLineExample(ThreeDScene): def construct(self): self.set_camera_orientation(PI / 3, -PI / 4) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.parallel_to(line1, color=YELLOW) self.add(ax, line1, line2) """ np_point = np.asarray(point) vect = normalize(line.vect) return cls( np_point + vect * length / 2, np_point - vect * length / 2, **kwargs, )
[docs] @classmethod def perpendicular_to( cls, line: Line3D, point: Point3DLike = ORIGIN, length: float = 5, **kwargs: Any, ) -> Line3D: """Returns a line perpendicular to another line going through a given point. Parameters ---------- line The line to be perpendicular to. point The point to pass through. length Length of the perpendicular line. kwargs Additional parameters to be passed to the class. Returns ------- :class:`Line3D` Line perpendicular to ``line``. Examples -------- .. manim:: PerpLineExample :save_last_frame: class PerpLineExample(ThreeDScene): def construct(self): self.set_camera_orientation(PI / 3, -PI / 4) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.perpendicular_to(line1, color=BLUE) self.add(ax, line1, line2) """ np_point = np.asarray(point) norm = np.cross(line.vect, np_point - line.start) if all(np.linalg.norm(norm) == np.zeros(3)): raise ValueError("Could not find the perpendicular.") start, end = perpendicular_bisector([line.start, line.end], norm) vect = normalize(end - start) return cls( np_point + vect * length / 2, np_point - vect * length / 2, **kwargs, )
[docs] class Arrow3D(Line3D): """An arrow made out of a cylindrical line and a conical tip. Parameters ---------- start The start position of the arrow. end The end position of the arrow. thickness The thickness of the arrow. height The height of the conical tip. base_radius The base radius of the conical tip. color The color of the arrow. resolution The resolution of the arrow line. Examples -------- .. manim:: ExampleArrow3D :save_last_frame: class ExampleArrow3D(ThreeDScene): def construct(self): axes = ThreeDAxes() arrow = Arrow3D( start=np.array([0, 0, 0]), end=np.array([2, 2, 2]), resolution=8 ) self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, arrow) """ def __init__( self, start: Point3DLike = LEFT, end: Point3DLike = RIGHT, thickness: float = 0.02, height: float = 0.3, base_radius: float = 0.08, color: ParsableManimColor = WHITE, resolution: int | tuple[int, int] = 24, **kwargs: Any, ) -> None: super().__init__( start=start, end=end, thickness=thickness, color=color, resolution=resolution, **kwargs, ) self.length = np.linalg.norm(self.vect) self.set_start_and_end_attrs( self.start, self.end - height * self.direction, **kwargs, ) self.cone = Cone( direction=self.direction, base_radius=base_radius, height=height, **kwargs, ) np_end = np.asarray(end, dtype=np.float64) self.cone.shift(np_end) self.end_point = VectorizedPoint(np_end) self.add(self.end_point, self.cone) self.set_color(color)
[docs] def get_end(self) -> np.ndarray: return self.end_point.get_center()
[docs] class Torus(Surface): """A torus. Parameters ---------- major_radius Distance from the center of the tube to the center of the torus. minor_radius Radius of the tube. u_range The range of the ``u`` variable: ``(u_min, u_max)``. v_range The range of the ``v`` variable: ``(v_min, v_max)``. resolution The number of samples taken of the :class:`Torus`. A tuple can be used to define different resolutions for ``u`` and ``v`` respectively. Examples -------- .. manim :: ExampleTorus :save_last_frame: class ExampleTorus(ThreeDScene): def construct(self): axes = ThreeDAxes() torus = Torus() self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, torus) """ def __init__( self, major_radius: float = 3, minor_radius: float = 1, u_range: tuple[float, float] = (0, TAU), v_range: tuple[float, float] = (0, TAU), resolution: int | tuple[int, int] | None = None, **kwargs: Any, ) -> None: if config.renderer == RendererType.OPENGL: res_value = (101, 101) elif config.renderer == RendererType.CAIRO: res_value = (24, 24) resolution = resolution if resolution is not None else res_value self.R = major_radius self.r = minor_radius super().__init__( self.func, u_range=u_range, v_range=v_range, resolution=resolution, **kwargs, )
[docs] def func(self, u: float, v: float) -> Point3D: """The z values defining the :class:`Torus` being plotted. Returns ------- :class:`numpy.ndarray` The z values defining the :class:`Torus`. """ P = np.array([np.cos(u), np.sin(u), 0]) return (self.R - self.r * np.cos(v)) * P - self.r * np.sin(v) * OUT