step.motion.path.passes.decompose

decompose pass — split a _Then leg at every known after_cm boundary.

A conditional segment whose stop condition is a sequential _Then chain that contains a bare relative after_cm leaf covers a KNOWN travel distance over that leaf, surrounded by unknown sensor-driven portions. drive_forward().until(after_cm(12) + over_line(sensor)) drives a fixed 12 cm and THEN keeps driving until it crosses a line — the first 12 cm are geometrically known, the rest is not. ...until(over_line(sensor) + after_cm(5)) drives until the line, then a known 5 cm further.

The raw lowering can’t recover that known distance: recover_known_distance only promotes a bare after_cm (any combined _Then wrapper is rejected because the combined condition may stop early — see known_distance.py). So the whole leg stays distance_m=None, has_known_endpoint=False and is invisible to the geometry passes (merge / to_absolute / splinify).

a + b builds _Then(a, b) and + is LEFT-associative, so a chain after_cm(20) + after_cm(10) + over_line is the LEFT-nested tree _Then(_Then(after_cm(20), after_cm(10)), over_line). This pass flattens the _Then tree into its ordered leaf sequence, then walks the leaves left→right, splitting at EVERY bare-relative-after_cm boundary — leading, trailing, or interleaved.

It splits such a leg in TIME into segments of the SAME drive (identical kind/axis/sign/speed_scale/heading), one segment per GROUP:

  1. Each bare relative after_cm leaf becomes its OWN known leg — condition = that after_cm, run through recover_known_distance so distance_m / has_known_endpoint get filled. This leg is now a known-endpoint leg the downstream passes can absolutize / splinify.

  2. Consecutive NON-(bare-relative-after_cm) leaves are GROUPED into one unknown sensor leg — their order-preserving _Then rebuilt (so over_line’s on_black + on_white stay together as ONE leg, not split), with distance_m=None, has_known_endpoint=False.

Each split-out after_cm becomes a REAL known distance because the executor calls condition.start() at every segment start, so each after_cm measures from its own segment’s start. Examples (sensor = on_black/over_line):

  • after_cm(12) + over_line[known(0.12), sensor] (leading).

  • on_black + after_cm(5)[sensor, known(0.05)] (trailing).

  • on_black + after_cm + on_black + after_cm[sensor, known, sensor, known].

  • over_line + after_cm(5)[sensor(_Then(on_black, on_white)), known(0.05)].

  • pure over_line (no after_cm) → unchanged [seg].

Guards (passed through UNCHANGED):

  • A bare after_cm (not wrapped in _Then) — already promoted at lowering by recover_known_distance; left alone.

  • A _Then with NO bare-relative after_cm anywhere (e.g. over_line = on_black > on_white) — may stop early; left whole, not fragmented.

  • An absolute after_cm leaf — measures from odometry origin, not segment start; treated as a sensor leaf (grouped, never promoted to a known leg).

  • turn / arc kinds — path length doesn’t map to a usable travel distance.

  • SideAction / None deferred placeholders — pass through untouched.

Representation declaration is intentionally left undeclared (defaults to EITHER / SAME / non-terminal); run this BEFORE to_absolute / splinify so the split-out known leg can be optimized.

Classes

DecomposePass

Split a _Then leg at every bare after_cm boundary.

Module Contents

class step.motion.path.passes.decompose.DecomposePass

Split a _Then leg at every bare after_cm boundary.

Pure node→node pass. For each linear / follow_line Segment whose condition is a sequential _Then containing a bare relative after_cm leaf, splits it into one known-endpoint leg per after_cm leaf (distance recovered) and one grouped unknown sensor leg per run of consecutive non-after_cm leaves — leading, trailing, and interleaved. A _Then with no bare after_cm anywhere, and all non-decomposable nodes, pass through unchanged.

Representation/terminal contract left undeclared (defaults to EITHER / SAME / non-terminal).

name = 'decompose'
run(nodes: list[step.motion.path.ir.PathNode | None]) list[step.motion.path.ir.PathNode | None]