# deke-linear
Constant-TCP-speed Cartesian polyline following for serial manipulators.
Where the `deke-topp*` retimers are **time-optimal** (maximise speed under v/a/j
caps), `deke-linear` holds a **constant TCP travel speed** — the requirement for
welding and similar process motions — and degrades gracefully near singularities
rather than failing. It is a CNC-style constant-feedrate interpolator in three
stages, the latter two of which implement the `deke_types` `Planner` / `Retimer`
traits so they compose with the rest of the ecosystem.
## Pipeline
```
&[DAffine3] poses + FollowConfig
│
[A] path::condition → Vec<CartesianRun> split at sharp corners; shallow
│ corners smoothed (squiggle Catmull–Rom),
│ arc-length parameterised
[B] CartesianLinearPlanner : Planner dense IK → DP branch track
│ (analytic IK, manipulability-weighted,
│ no Jacobian inversion → singularity-safe)
[C] ConstantSpeedRetimer : Retimer constant feedrate held where the joint
│ v/a/j MVC allows; smooth dips elsewhere
LinearFollower::follow → SRobotTraj per-run trajectories stitched at rest
```
- **Corners (hybrid).** A vertex whose turn angle exceeds
`PathConditioning::sharp_corner_angle` is *sharp*: the path splits there into runs
that start and stop at rest, keeping the vertex exact. Shallower corners are
smoothed by the Catmull–Rom spline and traversed without stopping.
- **`forbid_interior_dips`.** By default the speed dips smoothly where the joint
v/a/j geometry forces it (a shallow corner, a near-singular patch). Set this flag
to require the commanded speed to hold flat through a run's interior — the only
sub-commanded speed allowed is the rest ramp at each run's start and end. If a dip
would be forced anywhere in the interior, the retime fails with
`SpeedDipRequired { run, s, feasible_speed, commanded }` instead of slowing down.
- **Singularity tolerance.** The planner inverts each pose with the chain's analytic
IK (every branch, limit-filtered, no Jacobian inversion) and routes a continuous
joint track that maximises manipulability `√det(J·Jᵀ)`. The retimer's
feasible-speed ceiling is `min_j v_max,j / |q'_j(s)|`, so as a singularity is
approached the commanded speed dips smoothly to zero instead of demanding infinite
joint speed.
## Example
```rust
use deke_linear::{LinearFollower, FollowConfig};
use deke_types::glam::DAffine3;
let follower = LinearFollower::new(&chain); // any ContinuousFKChain + IkSolver
let (traj, diag) = follower.follow(&poses, &cfg)?; // poses: &[DAffine3], cfg: FollowConfig<N>
```
## Free tool-axis yaw (functional redundancy)
A tool that is rotationally symmetric about one of its axes — a welding torch,
spray head — leaves the rotation about that axis free (a 5-DOF task on a 6-DOF
arm). Declaring it lets the planner spend that DOF to steer around singularities.
```rust
use deke_linear::{FollowConfig, JointLimits, RedundantAxis, RedundantOptions};
use std::time::Duration;
let cfg = FollowConfig::weld(35.0, joint_limits, Duration::from_millis(8)) // 35 in/min
.with_redundancy(RedundantOptions {
axis: RedundantAxis::PosZ, // ±X/±Y/±Z or Custom(unit vec), tool frame
yaw_window: (-45f64.to_radians(), 45f64.to_radians()),
..RedundantOptions::default()
});
```
Yaw is a smooth scalar, so it is gridded coarsely and resolved by a **single global
DP** over `(station) × (yaw × branch)` — exact, so it finds the globally optimal yaw
track in one pass. A manipulability node cost steers off singularities, a yaw-rate
edge penalty keeps the spin smooth, and the velocity reconfiguration test rejects
discontinuous edges; the yaw may sweep freely across the window (bounded only by
per-step `max_yaw_step`). That coarse `ψ(s)` schedule is then refined at fine
arc-length spacing, with analytic IK placing the arm exactly each step
(predictor–corrector). The free axis is configurable because tool frames differ
(this project is Z-forward; others are X-forward).
## Reconfiguration by joint velocity
The planner takes a `max_velocity` and per-joint velocity ceilings: any edge that
would drive **any** joint past a configurable fraction (default 90%) of its limit
*at that speed* is treated as a reconfiguration/discontinuity and rejected. At weld
speeds (20–50 IPM) a joint approaching its velocity limit is the signature of a
singularity or wrist flip, so this cleanly separates "needs to slow down" from
"can't be done continuously." `FollowConfig::weld()` wires it up; otherwise set
`PlannerOptions::{max_velocity, joint_v_max, reconfig_vel_fraction}`.
## Scope / notes
- **Orientation** is a full TCP pose per vertex (slerped along the path). For a
symmetric tool, declare the free axis via `with_redundancy` (see above) to let the
planner resolve the yaw.
- Joint **velocity** is enforced exactly; **acceleration/jerk** are enforced by the
path-tangent projection plus a jerk-limited integrator. The `q''(s)·ṡ²` curvature
cross-term is a deliberate first-pass approximation (negligible at process speeds;
see the corner tests).
- Geometry runs in `f32` (squiggle); kinematics and timing in `f64`.
## License
Apache-2.0.