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

Coord, bezier_control: tuple[Coord, ...] | Coord | None = None, id: str = "") -> Waypoint: Creates a new Waypoint and appends adds it to the Path.

query_waypoint

str) -> Waypoint: Returns the waypoint with the given waypoint_id.

step

base_character.EventHandler) -> Coord: 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(coord: Coord, bezier_control: tuple[Coord, ...] | Coord | None = None, id: str = "") -> Waypoint:
            Creates a new Waypoint and appends adds it to the Path.
        query_waypoint(waypoint_id: str) -> Waypoint:
            Returns the waypoint with the given waypoint_id.
        step(event_handler: base_character.EventHandler) -> Coord:
            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:
        """
        Initializes 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 ValueError(f"({self.speed=}) Speed must be greater than 0.")

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

        Args:
            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 id:
            found_unique = False
            current_id = len(self.waypoints)
            while not found_unique:
                id = f"{len(self.waypoints)}"
                if id not in self.waypoint_lookup:
                    found_unique = True
                else:
                    current_id += 1
        new_waypoint = Waypoint(id, coord, bezier_control=bezier_control)
        self._add_waypoint_to_path(new_waypoint)
        return new_waypoint

    def _add_waypoint_to_path(self, waypoint: Waypoint) -> None:
        """Adds a waypoint to the path and updates 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:
        """Returns 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 ValueError(f"Waypoint with id {waypoint_id} not found.")
        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 coordinate of the final waypoint in the path
            return self.segments[-1].end.coord
        else:
            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)
        else:  # if the distance_to_travel is further than the last waypoint, preserve the distance from the start of the final segment
            active_segment = self.segments[-1]
            distance_to_travel += active_segment.distance
        if active_segment.distance == 0:
            segment_distance_to_travel_factor = 0.0
        else:
            segment_distance_to_travel_factor = distance_to_travel / active_segment.distance

        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: typing.Any) -> bool:
        if not isinstance(other, Path):
            return NotImplemented
        return self.path_id == other.path_id

    def __hash__(self):
        return hash(self.path_id)

__post_init__()

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

Source code in terminaltexteffects/engine/motion.py
def __post_init__(self) -> None:
    """
    Initializes 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 ValueError(f"({self.speed=}) Speed must be greater than 0.")

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

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

Parameters:

Name Type Description Default
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,
    id: str = "",
) -> Waypoint:
    """Creates a new Waypoint and appends adds it to the Path.

    Args:
        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 id:
        found_unique = False
        current_id = len(self.waypoints)
        while not found_unique:
            id = f"{len(self.waypoints)}"
            if id not in self.waypoint_lookup:
                found_unique = True
            else:
                current_id += 1
    new_waypoint = Waypoint(id, coord, bezier_control=bezier_control)
    self._add_waypoint_to_path(new_waypoint)
    return new_waypoint

query_waypoint(waypoint_id)

Returns 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:
    """Returns 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 ValueError(f"Waypoint with id {waypoint_id} not found.")
    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 coordinate of the final waypoint in the path
        return self.segments[-1].end.coord
    else:
        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)
    else:  # if the distance_to_travel is further than the last waypoint, preserve the distance from the start of the final segment
        active_segment = self.segments[-1]
        distance_to_travel += active_segment.distance
    if active_segment.distance == 0:
        segment_distance_to_travel_factor = 0.0
    else:
        segment_distance_to_travel_factor = distance_to_travel / active_segment.distance

    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