step.motion.path.passes.spline ============================== .. py:module:: step.motion.path.passes.spline .. autoapi-nested-parse:: Spline-conversion pass — translate a linear/turn sequence into a SplinePath. Unlike merge / corner-cut (pure node-list transforms), this pass *replaces* the entire path with a single ABSOLUTE spline-driven step. It is therefore a "terminal" transform — typically the last entry in a pipeline that opts in to spline mode. The pass densely samples the SAME centripetal Catmull-Rom curve used by the relative ``SplinePath`` / C++ ``SplineMotion`` (see ``catmull_rom_spline.hpp``) and emits ONE :class:`~raccoon.step.motion.goto.GotoWaypoints` side-action that follows those dense samples CLOSED-LOOP on the localization particle filter — so drift is corrected along the whole curve. The relative ``SplineMotion`` / ``smooth_path(spline=True)`` path (which uses :func:`build_spline_step` directly) is UNAFFECTED. Classes ------- .. autoapisummary:: step.motion.path.passes.spline.SplinifyPass Functions --------- .. autoapisummary:: step.motion.path.passes.spline.sample_centripetal_catmull_rom step.motion.path.passes.spline.segments_to_spline_waypoints step.motion.path.passes.spline.build_spline_step Module Contents --------------- .. py:function:: sample_centripetal_catmull_rom(control_points_m: list[tuple[float, float]], spacing_m: float = _SAMPLE_SPACING_M) -> list[tuple[float, float]] Densely sample a centripetal Catmull-Rom curve through ``control_points_m``. Matches ``CatmullRomSpline`` in ``catmull_rom_spline.hpp`` exactly: - Centripetal parameterisation (alpha=0.5): knot spacing is the square root of the chord length between consecutive control points. - Virtual endpoints via REFLECTION (not duplication) so the curve passes through every control point: ``P[-1] = 2*P[0] - P[1]`` and ``P[N] = 2*P[N-1] - P[N-2]``. - Barry-Goldman segment evaluation. Returns a flat list of ``(x_m, y_m)`` points, including the first and last control points, with consecutive points roughly ``spacing_m`` apart so the full path from the first to the last control point is covered. :param control_points_m: At least 2 control points ``(x_m, y_m)``. :param spacing_m: Target arc-length spacing between samples (metres). :returns: Dense ``[(x_m, y_m), ...]`` samples along the curve. .. py:function:: segments_to_spline_waypoints(segments: list[step.motion.path.ir.Segment]) -> list[tuple[float, float]] Compute ``(forward_cm, left_cm)`` control waypoints from a segment sequence. Walks the SAME body-frame pose integration as ``to_absolute`` (+forward is the robot's initial heading, +left is 90° CCW, heading CCW-positive): - ``linear`` advances along its axis and emits the endpoint. - ``diagonal`` holds heading and advances by its known body-frame displacement ``(forward_m, left_m)`` rotated into world, emitting the endpoint. - ``arc`` is SAMPLED into several intermediate control points (≈ one every 18° of sweep, min 2) so the Catmull-Rom traces the arc's curvature; the running pose advances to the arc's exact endpoint/heading. - ``turn`` only updates the running heading (no waypoint) — the corner is rounded by the Catmull-Rom through the neighbouring waypoints. The robot's start position ``(0, 0)`` is *not* included — it is the implicit origin for ``spline()``. .. py:function:: build_spline_step(nodes: list[step.motion.path.ir.PathNode | None]) -> step.motion.spline_path.SplinePath Build a SplinePath from a fully-splinifiable node list. Strict, single-spline builder used by ``smooth_path(spline=True)``: raises ``ValueError`` for any node that cannot be represented as a waypoint — deferred placeholders, side actions, condition-based segments, ``follow_line`` / ``spline`` segments, or segments with unknown endpoints. Arcs and diagonals ARE accepted — they are integrated into control waypoints the Catmull-Rom traces (arcs are sampled along their curvature). A minimum of 2 control waypoints (after sampling) is required so the spline has at least 2 explicit control points. The ``optimize().splinify()`` pass does NOT use this — it splits at barriers instead (see :class:`SplinifyPass`). .. py:class:: SplinifyPass(absolute: bool = False) Terminal pass that collapses each motion RUN into one continuous spline. Rather than demanding the whole path be a single uninterrupted curve, the pass SPLITS at barriers — exactly like ``merge`` / ``cut_corners`` / ``to_absolute`` already do — so a path that mixes motion with side effects still splinifies cleanly instead of raising: - A **blocking** (inline) side action, a deferred (``None``) placeholder, a condition-based / unknown-endpoint segment, a ``follow_line`` / ``spline`` segment, or a ``turn_to_heading`` BREAKS the run: it is passed through unchanged and the motion on either side becomes a separate spline. (A blocking side action must stop the robot anyway, so the velocity break at that one point is unavoidable — the step still fires at the same place.) - A **background** side action does NOT break the run: the spline flows across it (velocity stays continuous) and the step is lifted to the run's start, launched fire-and-forget so it overlaps the motion. Each maximal run of splinifiable segments (``linear`` / ``diagonal`` / ``arc`` / plain ``turn``) with ≥ 2 control waypoints becomes one spline; a run too short to splinify (a lone linear, or turns only) is kept as its raw segments. Two render modes, chosen by the builder from whether ``to_absolute()`` is on: - **relative** (default) → ``Segment(kind="spline")`` per run: the continuous, odometry-driven C++ ``SplineMotion`` follows the centripetal Catmull-Rom. - **absolute** → ``SideAction(SplineFollow)`` per run: a continuous pure-pursuit follower rides the SAME curve closed-loop on the localization particle filter, correcting drift along the whole curve. Holonomic — heading held, independent of the path tangent. Terminal: it replaces every motion run wholesale, so nothing may run after it. .. py:attribute:: name :value: 'splinify' .. py:attribute:: requires .. py:attribute:: terminal :value: True .. py:attribute:: absolute :value: False .. py:method:: run(nodes: list[step.motion.path.ir.PathNode | None]) -> list[step.motion.path.ir.PathNode | None]