Source code for simvx.core.nodes_2d.path

"""Path2D and PathFollow2D -- 2D path-following nodes."""

from __future__ import annotations

import math

from ..descriptors import Property, Signal
from ..math.types import Vec2
from .node2d import Node2D


[docs] class Path2D(Node2D): """Holds a Curve2D for path-following, positioned in 2D space.""" gizmo_colour = Property((1.0, 0.8, 0.0, 0.7), hint="Editor gizmo colour") def __init__(self, **kwargs): super().__init__(**kwargs) from ..math.types import Curve2D self.curve: Curve2D = Curve2D()
[docs] def get_gizmo_lines(self) -> list[tuple[Vec2, Vec2]]: """Return connected line segments sampling the curve.""" if self.curve.point_count < 2: return [] pts = self.curve.get_baked_points() if len(pts) < 2: return [] gp = self.world_position lines: list[tuple[Vec2, Vec2]] = [] prev = Vec2(pts[0].x + gp.x, pts[0].y + gp.y) for i in range(1, len(pts)): cur = Vec2(pts[i].x + gp.x, pts[i].y + gp.y) lines.append((prev, cur)) prev = cur return lines
[docs] class PathFollow2D(Node2D): """Moves along a parent Path2D's curve based on progress. Must be a child of a Path2D node. Updates position (and optionally rotation) each frame based on the current progress along the curve. """ h_offset = Property(0.0, hint="Horizontal offset from path") v_offset = Property(0.0, hint="Vertical offset from path") rotates = Property(True, hint="Auto-rotate to face path direction") loop = Property(True, hint="Wrap around at end of path") cubic_interp = Property(True, hint="Use cubic (vs linear) interpolation") loop_completed = Signal() def __init__(self, **kwargs): super().__init__(**kwargs) self._progress: float = 0.0 @property def progress(self) -> float: """Distance along the curve (0 to curve length).""" return self._progress @progress.setter def progress(self, value: float): curve = self._get_curve() if curve is None: self._progress = value return curve_length = curve.get_baked_length() if curve_length < 1e-10: self._progress = 0.0 return if self.loop: if value > curve_length: value = value % curve_length self.loop_completed() elif value < 0: value = curve_length + (value % curve_length) if curve_length > 0 else 0.0 self.loop_completed() else: value = max(0.0, min(value, curve_length)) self._progress = value @property def progress_ratio(self) -> float: """Normalized progress (0.0 to 1.0).""" curve = self._get_curve() if curve is None: return 0.0 curve_length = curve.get_baked_length() if curve_length < 1e-10: return 0.0 return self._progress / curve_length @progress_ratio.setter def progress_ratio(self, value: float): curve = self._get_curve() if curve is None: return curve_length = curve.get_baked_length() self.progress = value * curve_length def _get_curve(self): """Get the Curve2D from the parent Path2D, or None.""" if isinstance(self.parent, Path2D): return self.parent.curve return None
[docs] def process(self, dt: float): self._update_transform()
def _update_transform(self): curve = self._get_curve() if curve is None or curve.point_count < 2: return pos, angle = curve.sample_baked_with_rotation(self._progress) if self.h_offset != 0.0 or self.v_offset != 0.0: c, s = math.cos(angle), math.sin(angle) pos = Vec2( pos.x + c * self.h_offset - s * self.v_offset, pos.y + s * self.h_offset + c * self.v_offset, ) self.position = pos if self.rotates: self.rotation = angle