using System;
using System.Collections.Generic;
namespace OpenNest.CNC
{
///
/// Re-times s with trapezoidal acceleration, in the style of
/// Grbl's lookahead planner. Each cutting run (moves between a pierce and the next travel)
/// starts and ends at rest. Corners slow to a junction-deviation speed, arcs are capped at
/// their centripetal limit, and a backward then forward pass keeps every change of speed
/// within the acceleration. Travel and pierces are planned on their own, from rest to rest.
///
public static class MotionPlanner
{
private const double Straight = 0.999999;
///
/// Replaces each step's with its accelerated time,
/// from its . Pierces keep their dwell.
///
public static void Plan(List steps, MotionProfile motion)
{
ArgumentNullException.ThrowIfNull(steps);
ArgumentNullException.ThrowIfNull(motion);
var start = 0;
while (start < steps.Count)
{
var end = start + 1;
if (IsCutting(steps[start]))
{
while (end < steps.Count && IsCutting(steps[end]) && steps[end].Kind == steps[start].Kind)
end++;
PlanRun(steps, start, end, motion.CutAcceleration, motion.CornerDeviation);
}
else if (steps[start].Kind == CutSimulationStepKind.Rapid)
{
PlanRun(steps, start, end, motion.RapidAcceleration, 0);
}
start = end;
}
}
private static bool IsCutting(CutSimulationStep step) =>
step.Kind is CutSimulationStepKind.Cut or CutSimulationStepKind.Scribe;
private static void PlanRun(List steps, int start, int end, double acceleration, double deviation)
{
if (acceleration <= 0)
return; // Keep the constant-speed timing.
var count = end - start;
var maxSpeed = new double[count];
var entry = new double[count + 1]; // entry[count] is the run's exit: at rest.
for (var i = 0; i < count; i++)
{
var step = steps[start + i];
var nominal = step.NominalSpeed;
maxSpeed[i] = step.IsArc && step.Radius > 0
? System.Math.Min(nominal, System.Math.Sqrt(acceleration * step.Radius))
: nominal;
entry[i] = i == 0 ? 0 : JunctionSpeed(steps[start + i - 1], step, acceleration, deviation, System.Math.Min(maxSpeed[i - 1], maxSpeed[i]));
}
// Backward: every step must be able to slow to the next one's entry.
for (var i = count - 1; i >= 0; i--)
entry[i] = System.Math.Min(entry[i], Reachable(entry[i + 1], acceleration, steps[start + i].Length));
// Forward: and to reach that entry from its own.
for (var i = 0; i < count; i++)
entry[i + 1] = System.Math.Min(entry[i + 1], Reachable(entry[i], acceleration, steps[start + i].Length));
for (var i = 0; i < count; i++)
Profile(steps[start + i], entry[i], entry[i + 1], maxSpeed[i], acceleration);
}
private static double Reachable(double speed, double acceleration, double length) =>
System.Math.Sqrt(speed * speed + 2 * acceleration * length);
///
/// The fastest the head may pass from into :
/// v² = a·δ·sin(θ/2) / (1 − sin(θ/2)), θ being the angle the path turns through.
///
internal static double JunctionSpeed(CutSimulationStep previous, CutSimulationStep next, double acceleration, double deviation, double cap)
{
var exit = previous.TangentAt(1);
var enter = next.TangentAt(0);
var cosTheta = -(exit.X * enter.X + exit.Y * enter.Y);
if (cosTheta < -Straight)
return cap;
if (cosTheta > Straight || deviation <= 0)
return 0;
var sinHalf = System.Math.Sqrt(0.5 * (1 - cosTheta));
var speed = System.Math.Sqrt(acceleration * deviation * sinHalf / (1 - sinHalf));
return System.Math.Min(cap, speed);
}
private static void Profile(CutSimulationStep step, double entry, double exit, double maxSpeed, double acceleration)
{
var length = step.Length;
if (length <= 0 || maxSpeed <= 0)
return; // Nothing to accelerate: keep the constant-speed timing.
var cruise = maxSpeed;
var accelDistance = (cruise * cruise - entry * entry) / (2 * acceleration);
var decelDistance = (cruise * cruise - exit * exit) / (2 * acceleration);
if (accelDistance + decelDistance > length)
{
// Too short to reach full speed: a triangle peaking where the two ramps meet.
cruise = System.Math.Sqrt((2 * acceleration * length + entry * entry + exit * exit) / 2);
cruise = System.Math.Max(cruise, System.Math.Max(entry, exit));
accelDistance = (cruise * cruise - entry * entry) / (2 * acceleration);
decelDistance = (cruise * cruise - exit * exit) / (2 * acceleration);
}
var cruiseDistance = System.Math.Max(0, length - accelDistance - decelDistance);
var seconds = (cruise - entry) / acceleration + (cruise - exit) / acceleration
+ (cruise > 0 ? cruiseDistance / cruise : 0);
step.EntrySpeed = entry;
step.CruiseSpeed = cruise;
step.ExitSpeed = exit;
step.Acceleration = acceleration;
step.Duration = TimeSpan.FromSeconds(seconds);
}
}
}