Source code for simvx.core.math.curves

"""Curve — 1D property-animation curve with linear interpolation and optional bake."""


# ============================================================================
# Curve
# ============================================================================

[docs] class Curve: """1D curve for property animation — stores (t, value) pairs with linear interpolation.""" def __init__(self, points: list[tuple[float, float]] | None = None): self._points: list[tuple[float, float]] = sorted(points or [], key=lambda p: p[0]) self._baked: list[tuple[float, float]] | None = None
[docs] def add_point(self, t: float, value: float): """Insert a point, maintaining sort order by t.""" self._points.append((float(t), float(value))) self._points.sort(key=lambda p: p[0]) self._baked = None
[docs] def remove_point(self, index: int): """Remove the point at the given index.""" del self._points[index] self._baked = None
[docs] def sample(self, t: float) -> float: """Sample the curve at t using linear interpolation.""" if not self._points: return 0.0 if t <= self._points[0][0]: return self._points[0][1] if t >= self._points[-1][0]: return self._points[-1][1] # Binary search for the segment lo, hi = 0, len(self._points) - 1 while lo < hi - 1: mid = (lo + hi) // 2 if self._points[mid][0] <= t: lo = mid else: hi = mid t0, v0 = self._points[lo] t1, v1 = self._points[hi] dt = t1 - t0 if dt < 1e-10: return v0 frac = (t - t0) / dt return v0 + (v1 - v0) * frac
[docs] def sample_baked(self, t: float) -> float: """Sample using a baked (cached) lookup for performance. Falls back to sample().""" if self._baked is None: self._bake() return self._sample_from(self._baked, t)
def _bake(self, resolution: int = 100): """Bake the curve into a uniform lookup table.""" if not self._points: self._baked = [] return t_min = self._points[0][0] t_max = self._points[-1][0] self._baked = [] for i in range(resolution + 1): t = t_min + (t_max - t_min) * i / resolution self._baked.append((t, self.sample(t))) def _sample_from(self, pts: list[tuple[float, float]], t: float) -> float: if not pts: return 0.0 if t <= pts[0][0]: return pts[0][1] if t >= pts[-1][0]: return pts[-1][1] lo, hi = 0, len(pts) - 1 while lo < hi - 1: mid = (lo + hi) // 2 if pts[mid][0] <= t: lo = mid else: hi = mid t0, v0 = pts[lo] t1, v1 = pts[hi] dt = t1 - t0 if dt < 1e-10: return v0 return v0 + (v1 - v0) * (t - t0) / dt @property def point_count(self) -> int: return len(self._points)
[docs] def __repr__(self): return f"Curve({len(self._points)} points)"