Skip to content

Path

Module: terminaltexteffects.engine.motion

Represents a path consisting of multiple waypoints for motion.

Attributes:

Name Type Description
path_id str

The unique identifier for the path.

speed float

speed > 0

ease EasingFunction | None

easing function for character movement. Defaults to None.

layer int | None

layer to move the character to, if None, layer is unchanged. Defaults to None.

hold_time int

number of frames to hold the character at the end of the path. Defaults to 0.

loop bool

Whether the path should loop back to the beginning. Default is False.

Methods:

Name Description
new_waypoint

Creates a new Waypoint and appends adds it to the Path.

query_waypoint

Returns the waypoint with the given waypoint_id.

step

Progresses to the next step along the path and returns the coordinate at that step.

Source code in terminaltexteffects/engine/motion.py
@dataclass
class Path:
    """Represents a path consisting of multiple waypoints for motion.

    Attributes:
        path_id (str): The unique identifier for the path.
        speed (float): speed > 0
        ease (easing.EasingFunction | None): easing function for character movement. Defaults to None.
        layer (int | None): layer to move the character to, if None, layer is unchanged. Defaults to None.
        hold_time (int): number of frames to hold the character at the end of the path. Defaults to 0.
        loop (bool): Whether the path should loop back to the beginning. Default is False.

    Methods:
        new_waypoint:
            Creates a new Waypoint and appends adds it to the Path.
        query_waypoint:
            Returns the waypoint with the given waypoint_id.
        step:
            Progresses to the next step along the path and returns the coordinate at that step.

    """

    path_id: str
    speed: float = 1.0
    ease: easing.EasingFunction | None = None
    layer: int | None = None
    hold_time: int = 0
    loop: bool = False

    def __post_init__(self) -> None:
        """Initialize the Path object and calculates the total distance and maximum steps."""
        self.segments: list[Segment] = []
        self.waypoints: list[Waypoint] = []
        self.waypoint_lookup: dict[str, Waypoint] = {}
        self.total_distance: float = 0
        self.current_step: int = 0
        self.max_steps: int = 0
        self.hold_time_remaining = self.hold_time
        self.last_distance_reached: float = 0  # used for animation syncing to distance
        self.origin_segment: Segment | None = None
        if self.speed <= 0:
            raise PathInvalidSpeedError(self.speed)

    def new_waypoint(
        self,
        coord: Coord,
        *,
        bezier_control: tuple[Coord, ...] | Coord | None = None,
        waypoint_id: str = "",
    ) -> Waypoint:
        """Create a new Waypoint and appends adds it to the Path.

        Args:
            waypoint_id (str): Unique identifier for the waypoint. Used to query for the waypoint.
            coord (Coord): coordinate
            bezier_control (tuple[Coord, ...] | Coord | None): coordinate of the control point for a bezier
                curve. Defaults to None.

        Returns:
            Waypoint: The new waypoint.

        """
        if not waypoint_id:
            found_unique = False
            current_id = len(self.waypoints)
            while not found_unique:
                waypoint_id = f"{current_id}"
                if waypoint_id not in self.waypoint_lookup:
                    found_unique = True
                else:
                    current_id += 1
        if waypoint_id in self.waypoint_lookup:
            raise DuplicateWaypointIDError(waypoint_id)
        bezier_control_tuple: tuple[Coord, ...] | None
        if bezier_control and isinstance(bezier_control, Coord):
            bezier_control_tuple = (bezier_control,)
        elif bezier_control and isinstance(bezier_control, tuple):
            bezier_control_tuple = bezier_control
        else:
            bezier_control_tuple = None
        new_waypoint = Waypoint(waypoint_id, coord, bezier_control=bezier_control_tuple)
        self._add_waypoint_to_path(new_waypoint)
        return new_waypoint

    def _add_waypoint_to_path(self, waypoint: Waypoint) -> None:
        """Add a waypoint to the path and update the total distance and maximum steps.

        Args:
            waypoint (Waypoint): waypoint to add

        """
        self.waypoint_lookup[waypoint.waypoint_id] = waypoint
        self.waypoints.append(waypoint)
        if len(self.waypoints) < 2:
            return

        if waypoint.bezier_control:
            distance_from_previous = geometry.find_length_of_bezier_curve(
                self.waypoints[-2].coord,
                waypoint.bezier_control,
                waypoint.coord,
            )
        else:
            distance_from_previous = geometry.find_length_of_line(
                self.waypoints[-2].coord,
                waypoint.coord,
            )
        self.total_distance += distance_from_previous
        self.segments.append(Segment(self.waypoints[-2], waypoint, distance_from_previous))
        self.max_steps = round(self.total_distance / self.speed)

    def query_waypoint(self, waypoint_id: str) -> Waypoint:
        """Return the waypoint with the given waypoint_id.

        Args:
            waypoint_id (str): waypoint_id

        Returns:
            Waypoint: The waypoint with the given waypoint_id.

        """
        waypoint = self.waypoint_lookup.get(waypoint_id, None)
        if not waypoint:
            raise WaypointNotFoundError(waypoint_id)
        return waypoint

    def step(self, event_handler: base_character.EventHandler) -> Coord:
        """Progresses to the next step along the path and returns the coordinate at that step.

        This method is called by the Motion.move() method. It calculates the next coordinate based on the current step,
        total distance, bezier control points, and the easing function if provided. It also handles the triggering
        of segment enter and exit events.

        Args:
            event_handler (base_character.EventHandler): The EventHandler for the character.

        Returns:
            Coord: The next coordinate on the path.

        """
        if not self.max_steps or self.current_step >= self.max_steps or not self.total_distance:
            # if the path has zero distance or there are no more steps, return the final waypoint coordinate
            return self.segments[-1].end.coord
        self.current_step += 1
        if self.ease:
            distance_factor = self.ease(self.current_step / self.max_steps)
        else:
            distance_factor = self.current_step / self.max_steps

        distance_to_travel = distance_factor * self.total_distance
        self.last_distance_reached = distance_to_travel
        for segment in self.segments:
            if distance_to_travel <= segment.distance:
                active_segment = segment
                if not segment.enter_event_triggered:
                    segment.enter_event_triggered = True
                    event_handler._handle_event(event_handler.Event.SEGMENT_ENTERED, segment.end)
                break
            distance_to_travel -= segment.distance
            if not segment.exit_event_triggered:
                segment.exit_event_triggered = True
                event_handler._handle_event(event_handler.Event.SEGMENT_EXITED, segment.end)
        # if the distance_to_travel is further than the last waypoint,
        # preserve the distance from the start of the final segment
        else:
            active_segment = self.segments[-1]
            distance_to_travel += active_segment.distance
        if active_segment.distance == 0:
            segment_distance_to_travel_factor = 0.0
        elif self.ease:
            segment_distance_to_travel_factor = distance_to_travel / active_segment.distance
        else:
            segment_distance_to_travel_factor = min((distance_to_travel / active_segment.distance, 1))

        if active_segment.end.bezier_control:
            next_coord = geometry.find_coord_on_bezier_curve(
                active_segment.start.coord,
                active_segment.end.bezier_control,
                active_segment.end.coord,
                segment_distance_to_travel_factor,
            )
        else:
            next_coord = geometry.find_coord_on_line(
                active_segment.start.coord,
                active_segment.end.coord,
                segment_distance_to_travel_factor,
            )

        return next_coord

    def __eq__(self, other: object) -> bool:
        """Check if two Path objects are equal.

        Args:
            other (object): object to compare

        Returns:
            bool: True if the two Path objects are equal, False otherwise.

        """
        if not isinstance(other, Path):
            return NotImplemented
        return self.path_id == other.path_id

    def __hash__(self) -> int:
        """Return the hash value of the Path.

        Hash is calculated using the path_id.
        """
        return hash(self.path_id)

__eq__(other)

Check if two Path objects are equal.

Parameters:

Name Type Description Default
other object

object to compare

required

Returns:

Name Type Description
bool bool

True if the two Path objects are equal, False otherwise.

Source code in terminaltexteffects/engine/motion.py
def __eq__(self, other: object) -> bool:
    """Check if two Path objects are equal.

    Args:
        other (object): object to compare

    Returns:
        bool: True if the two Path objects are equal, False otherwise.

    """
    if not isinstance(other, Path):
        return NotImplemented
    return self.path_id == other.path_id

__hash__()

Return the hash value of the Path.

Hash is calculated using the path_id.

Source code in terminaltexteffects/engine/motion.py
def __hash__(self) -> int:
    """Return the hash value of the Path.

    Hash is calculated using the path_id.
    """
    return hash(self.path_id)

__post_init__()

Initialize the Path object and calculates the total distance and maximum steps.

Source code in terminaltexteffects/engine/motion.py
def __post_init__(self) -> None:
    """Initialize the Path object and calculates the total distance and maximum steps."""
    self.segments: list[Segment] = []
    self.waypoints: list[Waypoint] = []
    self.waypoint_lookup: dict[str, Waypoint] = {}
    self.total_distance: float = 0
    self.current_step: int = 0
    self.max_steps: int = 0
    self.hold_time_remaining = self.hold_time
    self.last_distance_reached: float = 0  # used for animation syncing to distance
    self.origin_segment: Segment | None = None
    if self.speed <= 0:
        raise PathInvalidSpeedError(self.speed)

new_waypoint(coord, *, bezier_control=None, waypoint_id='')

Create a new Waypoint and appends adds it to the Path.

Parameters:

Name Type Description Default
waypoint_id str

Unique identifier for the waypoint. Used to query for the waypoint.

''
coord Coord

coordinate

required
bezier_control tuple[Coord, ...] | Coord | None

coordinate of the control point for a bezier curve. Defaults to None.

None

Returns:

Name Type Description
Waypoint Waypoint

The new waypoint.

Source code in terminaltexteffects/engine/motion.py
def new_waypoint(
    self,
    coord: Coord,
    *,
    bezier_control: tuple[Coord, ...] | Coord | None = None,
    waypoint_id: str = "",
) -> Waypoint:
    """Create a new Waypoint and appends adds it to the Path.

    Args:
        waypoint_id (str): Unique identifier for the waypoint. Used to query for the waypoint.
        coord (Coord): coordinate
        bezier_control (tuple[Coord, ...] | Coord | None): coordinate of the control point for a bezier
            curve. Defaults to None.

    Returns:
        Waypoint: The new waypoint.

    """
    if not waypoint_id:
        found_unique = False
        current_id = len(self.waypoints)
        while not found_unique:
            waypoint_id = f"{current_id}"
            if waypoint_id not in self.waypoint_lookup:
                found_unique = True
            else:
                current_id += 1
    if waypoint_id in self.waypoint_lookup:
        raise DuplicateWaypointIDError(waypoint_id)
    bezier_control_tuple: tuple[Coord, ...] | None
    if bezier_control and isinstance(bezier_control, Coord):
        bezier_control_tuple = (bezier_control,)
    elif bezier_control and isinstance(bezier_control, tuple):
        bezier_control_tuple = bezier_control
    else:
        bezier_control_tuple = None
    new_waypoint = Waypoint(waypoint_id, coord, bezier_control=bezier_control_tuple)
    self._add_waypoint_to_path(new_waypoint)
    return new_waypoint

query_waypoint(waypoint_id)

Return the waypoint with the given waypoint_id.

Parameters:

Name Type Description Default
waypoint_id str

waypoint_id

required

Returns:

Name Type Description
Waypoint Waypoint

The waypoint with the given waypoint_id.

Source code in terminaltexteffects/engine/motion.py
def query_waypoint(self, waypoint_id: str) -> Waypoint:
    """Return the waypoint with the given waypoint_id.

    Args:
        waypoint_id (str): waypoint_id

    Returns:
        Waypoint: The waypoint with the given waypoint_id.

    """
    waypoint = self.waypoint_lookup.get(waypoint_id, None)
    if not waypoint:
        raise WaypointNotFoundError(waypoint_id)
    return waypoint

step(event_handler)

Progresses to the next step along the path and returns the coordinate at that step.

This method is called by the Motion.move() method. It calculates the next coordinate based on the current step, total distance, bezier control points, and the easing function if provided. It also handles the triggering of segment enter and exit events.

Parameters:

Name Type Description Default
event_handler EventHandler

The EventHandler for the character.

required

Returns:

Name Type Description
Coord Coord

The next coordinate on the path.

Source code in terminaltexteffects/engine/motion.py
def step(self, event_handler: base_character.EventHandler) -> Coord:
    """Progresses to the next step along the path and returns the coordinate at that step.

    This method is called by the Motion.move() method. It calculates the next coordinate based on the current step,
    total distance, bezier control points, and the easing function if provided. It also handles the triggering
    of segment enter and exit events.

    Args:
        event_handler (base_character.EventHandler): The EventHandler for the character.

    Returns:
        Coord: The next coordinate on the path.

    """
    if not self.max_steps or self.current_step >= self.max_steps or not self.total_distance:
        # if the path has zero distance or there are no more steps, return the final waypoint coordinate
        return self.segments[-1].end.coord
    self.current_step += 1
    if self.ease:
        distance_factor = self.ease(self.current_step / self.max_steps)
    else:
        distance_factor = self.current_step / self.max_steps

    distance_to_travel = distance_factor * self.total_distance
    self.last_distance_reached = distance_to_travel
    for segment in self.segments:
        if distance_to_travel <= segment.distance:
            active_segment = segment
            if not segment.enter_event_triggered:
                segment.enter_event_triggered = True
                event_handler._handle_event(event_handler.Event.SEGMENT_ENTERED, segment.end)
            break
        distance_to_travel -= segment.distance
        if not segment.exit_event_triggered:
            segment.exit_event_triggered = True
            event_handler._handle_event(event_handler.Event.SEGMENT_EXITED, segment.end)
    # if the distance_to_travel is further than the last waypoint,
    # preserve the distance from the start of the final segment
    else:
        active_segment = self.segments[-1]
        distance_to_travel += active_segment.distance
    if active_segment.distance == 0:
        segment_distance_to_travel_factor = 0.0
    elif self.ease:
        segment_distance_to_travel_factor = distance_to_travel / active_segment.distance
    else:
        segment_distance_to_travel_factor = min((distance_to_travel / active_segment.distance, 1))

    if active_segment.end.bezier_control:
        next_coord = geometry.find_coord_on_bezier_curve(
            active_segment.start.coord,
            active_segment.end.bezier_control,
            active_segment.end.coord,
            segment_distance_to_travel_factor,
        )
    else:
        next_coord = geometry.find_coord_on_line(
            active_segment.start.coord,
            active_segment.end.coord,
            segment_distance_to_travel_factor,
        )

    return next_coord