namespace Flyshot.ControllerClientCompat; /// /// 对飞拍稠密关节轨迹的首尾采样点做速度整形,降低启动和结束时的单步角度变化。 /// internal static class FlyshotTrajectoryEdgeShaper { /// /// 首尾整形默认覆盖的采样点数(含锚点)。 /// internal const int DefaultEdgePointCount = 10; /// /// 对稠密关节轨迹做首尾整形,时间列保持不变,首段采用 ease-in,尾段采用 ease-out。 /// /// 输入稠密关节轨迹,每行格式为 [time, j1..jN],关节单位为弧度。 /// 保留旧签名兼容调用方;当前实现不再按角度阈值扩窗。 /// 单侧整形覆盖的采样点数(含锚点),默认首尾各 10 点。 /// 经过首尾整形后的新轨迹;若不满足整形条件则返回原轨迹副本。 internal static IReadOnlyList> ShapeDenseJointTrajectory( IReadOnlyList> denseJointTrajectory, double maxEdgeStepDegrees = 0.0, int maxWindowPoints = DefaultEdgePointCount) { ArgumentNullException.ThrowIfNull(denseJointTrajectory); if (denseJointTrajectory.Count == 0) { return Array.Empty>(); } var copiedRows = denseJointTrajectory .Select(static row => row.ToArray()) .ToArray(); if (copiedRows.Length < 5 || maxWindowPoints < 2) { return copiedRows; } var lastIndex = copiedRows.Length - 1; var window = Math.Min(maxWindowPoints, lastIndex / 2); if (window < 2) { return copiedRows; } // 以原始轨迹为参考估计窗口边界的速度,并在位移累计量上做单段单调整形, // 目标是让首尾 10 点表现为更平滑的加减速,而不是硬匹配高阶导数导致振荡。 var originalRows = copiedRows .Select(static row => row.ToArray()) .ToArray(); ApplyLeadingHermiteBlend(copiedRows, originalRows, window); ApplyTrailingHermiteBlend(copiedRows, originalRows, window); return copiedRows; } /// /// 对首段做单段 Hermite 累计位移整形:起点速度为 0,窗口末端按原轨迹边界速度接回中段。 /// private static void ApplyLeadingHermiteBlend(double[][] rows, double[][] originalRows, int window) { var startRow = originalRows[0]; var endRow = originalRows[window]; var totalDuration = endRow[0] - startRow[0]; if (totalDuration <= 0.0) { return; } for (var jointIndex = 1; jointIndex < startRow.Length; jointIndex++) { var delta = endRow[jointIndex] - startRow[jointIndex]; if (Math.Abs(delta) <= 1e-12) { continue; } var endVelocity = EstimateVelocity(originalRows, window, jointIndex); var normalizedEndSlope = ClampNormalizedSlope((endVelocity * totalDuration) / delta); for (var index = 1; index < window; index++) { var normalizedTime = (rows[index][0] - startRow[0]) / totalDuration; var shapedValue = startRow[jointIndex] + (delta * EvaluateHermiteProgress(normalizedTime, startSlope: 0.0, endSlope: normalizedEndSlope)); var blendWeight = Math.Pow(1.0 - normalizedTime, 2.0); rows[index][jointIndex] = Lerp(originalRows[index][jointIndex], shapedValue, blendWeight); } } } /// /// 对尾段做单段 Hermite 累计位移整形:窗口起点按原轨迹边界速度接入,终点速度减到 0。 /// private static void ApplyTrailingHermiteBlend(double[][] rows, double[][] originalRows, int window) { var startIndex = rows.Length - 1 - window; var startRow = originalRows[startIndex]; var endRow = originalRows[^1]; var totalDuration = endRow[0] - startRow[0]; if (totalDuration <= 0.0) { return; } for (var jointIndex = 1; jointIndex < startRow.Length; jointIndex++) { var delta = endRow[jointIndex] - startRow[jointIndex]; if (Math.Abs(delta) <= 1e-12) { continue; } var startVelocity = EstimateVelocity(originalRows, startIndex, jointIndex); var normalizedStartSlope = ClampNormalizedSlope((startVelocity * totalDuration) / delta); for (var index = 1; index < window; index++) { var normalizedTime = (rows[startIndex + index][0] - startRow[0]) / totalDuration; var shapedValue = startRow[jointIndex] + (delta * EvaluateHermiteProgress(normalizedTime, startSlope: normalizedStartSlope, endSlope: 0.0)); var blendWeight = Math.Pow(normalizedTime, 2.0); rows[startIndex + index][jointIndex] = Lerp(originalRows[startIndex + index][jointIndex], shapedValue, blendWeight); } } } /// /// 估算给定行在原始轨迹上的一阶导,首尾退化为单边差分。 /// private static double EstimateVelocity(double[][] rows, int index, int jointIndex) { if (index <= 0) { var dt = rows[1][0] - rows[0][0]; return dt <= 0.0 ? 0.0 : (rows[1][jointIndex] - rows[0][jointIndex]) / dt; } if (index >= rows.Length - 1) { var dt = rows[^1][0] - rows[^2][0]; return dt <= 0.0 ? 0.0 : (rows[^1][jointIndex] - rows[^2][jointIndex]) / dt; } var previousDt = rows[index][0] - rows[index - 1][0]; var nextDt = rows[index + 1][0] - rows[index][0]; if (previousDt <= 0.0 || nextDt <= 0.0) { return 0.0; } var backward = (rows[index][jointIndex] - rows[index - 1][jointIndex]) / previousDt; var forward = (rows[index + 1][jointIndex] - rows[index][jointIndex]) / nextDt; return (backward + forward) / 2.0; } /// /// 估算给定行在原始轨迹上的二阶导,端点退化为 0 以避免放大边界噪声。 /// private static double EstimateAcceleration(double[][] rows, int index, int jointIndex) { if (index <= 0 || index >= rows.Length - 1) { return 0.0; } var previousDt = rows[index][0] - rows[index - 1][0]; var nextDt = rows[index + 1][0] - rows[index][0]; if (previousDt <= 0.0 || nextDt <= 0.0) { return 0.0; } var backward = (rows[index][jointIndex] - rows[index - 1][jointIndex]) / previousDt; var forward = (rows[index + 1][jointIndex] - rows[index][jointIndex]) / nextDt; var averageDt = (previousDt + nextDt) / 2.0; return averageDt <= 0.0 ? 0.0 : (forward - backward) / averageDt; } /// /// 计算 Hermite 累计位移曲线在 0..1 归一化时间上的进度值。 /// private static double EvaluateHermiteProgress(double normalizedTime, double startSlope, double endSlope) { var u = Math.Clamp(normalizedTime, 0.0, 1.0); var u2 = u * u; var u3 = u2 * u; var h00 = (2.0 * u3) - (3.0 * u2) + 1.0; var h10 = u3 - (2.0 * u2) + u; var h01 = (-2.0 * u3) + (3.0 * u2); var h11 = u3 - u2; return (h00 * 0.0) + (h10 * startSlope) + (h01 * 1.0) + (h11 * endSlope); } /// /// 把归一化边界斜率限制在单调 Hermite 常见的稳定区间内,避免过冲和窗口内振荡。 /// private static double ClampNormalizedSlope(double normalizedSlope) { if (double.IsNaN(normalizedSlope) || double.IsInfinity(normalizedSlope)) { return 0.0; } return Math.Clamp(normalizedSlope, 0.0, 3.0); } /// /// 在线性插值基础上做温和混合,避免首尾窗口为了追赶锚点而产生过大的局部跃度。 /// private static double Lerp(double originalValue, double shapedValue, double weight) { var clampedWeight = Math.Clamp(weight, 0.0, 1.0); return originalValue + ((shapedValue - originalValue) * clampedWeight); } }