NavisworksTransport/src/Core/PathCurveEngine.cs

354 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport
{
/// <summary>
/// 路径曲线化引擎
/// 实现基于圆弧过渡Arc Fillet的路径曲线化功能
/// </summary>
public static class PathCurveEngine
{
/// <summary>
/// 将值限制在指定范围内(.NET Framework 4.8 不支持 Math.Clamp
/// </summary>
private static double Clamp(double value, double min, double max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
/// <summary>
/// 计算圆弧切点和轨迹参数
/// </summary>
/// <param name="pPrev">前一个控制点</param>
/// <param name="pCurr">当前控制点</param>
/// <param name="pNext">下一个控制点</param>
/// <param name="turnRadius">转向半径(米)</param>
/// <returns>圆弧轨迹数据</returns>
public static ArcTrajectory CalculateFillet(
Point3D pPrev,
Point3D pCurr,
Point3D pNext,
double turnRadius)
{
// 1. 计算单位向量
Vector3D v1 = (pPrev - pCurr).Normalize();
Vector3D v2 = (pNext - pCurr).Normalize();
// 2. 计算夹角 α
double cosAlpha = GeometryHelper.DotProduct(
new Point3D(v1.X, v1.Y, v1.Z),
new Point3D(v2.X, v2.Y, v2.Z));
double angleRad = Math.Acos(Clamp(cosAlpha, -1.0, 1.0));
// 如果几乎共线,返回无效轨迹
if (angleRad < 0.01 || angleRad > Math.PI - 0.01)
{
return new ArcTrajectory
{
Ts = pCurr,
Te = pCurr,
ArcCenter = pCurr,
RequestedRadius = turnRadius,
ActualRadius = 0,
DeflectionAngle = angleRad,
ArcLength = 0
};
}
// 3. 计算切线长 Lt = R / tan(α/2)
double Lt = turnRadius / Math.Tan(angleRad / 2.0);
// 4. 安全截断检查
double seg1Length = (pPrev - pCurr).Length;
double seg2Length = (pNext - pCurr).Length;
double maxAllowedLt = Math.Min(seg1Length, seg2Length) * 0.45;
double actualRadius = turnRadius;
if (Lt > maxAllowedLt)
{
Lt = maxAllowedLt;
actualRadius = Lt * Math.Tan(angleRad / 2.0);
}
// 5. 计算切点
Point3D ts = pCurr + v1 * Lt;
Point3D te = pCurr + v2 * Lt;
// 6. 计算圆心 (使用角平分线方向)
Vector3D bisector = (v1 + v2).Normalize();
double distToCenter = actualRadius / Math.Sin(angleRad / 2.0);
Point3D arcCenter = pCurr + bisector * distToCenter;
// 7. 计算圆弧长度
double arcLength = actualRadius * angleRad;
return new ArcTrajectory
{
Ts = ts,
Te = te,
ArcCenter = arcCenter,
RequestedRadius = turnRadius,
ActualRadius = actualRadius,
DeflectionAngle = angleRad,
ArcLength = arcLength
};
}
/// <summary>
/// Rodrigues旋转公式 - 绕任意轴旋转点
/// </summary>
/// <param name="point">要旋转的点</param>
/// <param name="center">旋转中心</param>
/// <param name="axis">旋转轴(单位向量)</param>
/// <param name="angle">旋转角度(弧度)</param>
/// <returns>旋转后的点</returns>
private static Point3D RotatePointAroundAxis(
Point3D point,
Point3D center,
Vector3D axis,
double angle)
{
// 将点转换为相对于中心的向量
Vector3D v = point - center;
// Rodrigues旋转公式
// v_rot = v * cos(θ) + (k × v) * sin(θ) + k * (k · v) * (1 - cos(θ))
Point3D kxv = GeometryHelper.CrossProduct(
new Point3D(axis.X, axis.Y, axis.Z),
new Point3D(v.X, v.Y, v.Z));
double kdv = GeometryHelper.DotProduct(
new Point3D(axis.X, axis.Y, axis.Z),
new Point3D(v.X, v.Y, v.Z));
Vector3D kxvVec = new Vector3D(kxv.X, kxv.Y, kxv.Z);
Vector3D vRot = v * Math.Cos(angle) + kxvVec * Math.Sin(angle) + axis * kdv * (1 - Math.Cos(angle));
return center + vRot;
}
/// <summary>
/// 圆弧采样为离散点序列
/// </summary>
/// <param name="trajectory">圆弧轨迹</param>
/// <param name="samplingStep">采样步长(米)</param>
/// <returns>采样点列表</returns>
public static List<Point3D> SampleArc(
ArcTrajectory trajectory,
double samplingStep)
{
var points = new List<Point3D>();
// 如果圆弧长度很小,直接返回起止点
if (trajectory.ArcLength < 0.001)
{
points.Add(trajectory.Ts);
points.Add(trajectory.Te);
return points;
}
// 根据采样步长计算采样点数量
int sampleCount = Math.Max(2, (int)Math.Ceiling(trajectory.ArcLength / samplingStep));
// 计算起始和结束向量
Vector3D startVec = (trajectory.Ts - trajectory.ArcCenter).Normalize();
Vector3D endVec = (trajectory.Te - trajectory.ArcCenter).Normalize();
// 计算旋转轴(叉乘)
Point3D cross = GeometryHelper.CrossProduct(
new Point3D(startVec.X, startVec.Y, startVec.Z),
new Point3D(endVec.X, endVec.Y, endVec.Z));
Vector3D rotationAxis = new Vector3D(cross.X, cross.Y, cross.Z).Normalize();
// 等角度插值
for (int i = 0; i <= sampleCount; i++)
{
double t = i / (double)sampleCount;
double theta = t * trajectory.DeflectionAngle;
// Rodrigues旋转公式
Point3D sampledPoint = RotatePointAroundAxis(
trajectory.Ts,
trajectory.ArcCenter,
rotationAxis,
theta
);
points.Add(sampledPoint);
}
return points;
}
/// <summary>
/// 构建直线边
/// </summary>
/// <param name="p1">起始点</param>
/// <param name="p2">结束点</param>
/// <param name="samplingStep">采样步长(米)</param>
/// <returns>路径边</returns>
private static PathEdge BuildStraightEdge(
PathPoint p1,
PathPoint p2,
double samplingStep)
{
var edge = new PathEdge
{
StartPointId = p1.Id,
EndPointId = p2.Id,
SegmentType = PathSegmentType.Straight,
PhysicalLength = (p1.Position - p2.Position).Length
};
// 采样直线段
int sampleCount = Math.Max(2, (int)Math.Ceiling(edge.PhysicalLength / samplingStep));
edge.SampledPoints = new List<Point3D>();
for (int i = 0; i <= sampleCount; i++)
{
double t = i / (double)sampleCount;
Point3D sampledPoint = p1.Position + (p2.Position - p1.Position) * t;
edge.SampledPoints.Add(sampledPoint);
}
return edge;
}
/// <summary>
/// 构建包含圆弧的边
/// </summary>
/// <param name="p1">起始点</param>
/// <param name="p2">结束点</param>
/// <param name="arcTrajectory">圆弧轨迹</param>
/// <param name="samplingStep">采样步长(米)</param>
/// <returns>路径边</returns>
private static PathEdge BuildEdgeWithArc(
PathPoint p1,
PathPoint p2,
ArcTrajectory arcTrajectory,
double samplingStep)
{
var edge = new PathEdge
{
StartPointId = p1.Id,
EndPointId = p2.Id,
SegmentType = PathSegmentType.Arc,
Trajectory = arcTrajectory,
PhysicalLength = arcTrajectory.ArcLength
};
// 采样圆弧段
edge.SampledPoints = SampleArc(arcTrajectory, samplingStep);
return edge;
}
/// <summary>
/// 生成采样点序列(用于延迟加载)
/// </summary>
/// <param name="edge">路径边</param>
/// <param name="samplingStep">采样步长(米)</param>
/// <returns>采样点列表</returns>
public static List<Point3D> GenerateSampledPoints(PathEdge edge, double samplingStep)
{
if (edge.SegmentType == PathSegmentType.Straight)
{
// 直线段采样
int sampleCount = Math.Max(2, (int)Math.Ceiling(edge.PhysicalLength / samplingStep));
var points = new List<Point3D>();
for (int i = 0; i <= sampleCount; i++)
{
double t = i / (double)sampleCount;
Point3D p1 = edge.Trajectory?.Ts ?? edge.Trajectory?.Te ?? new Point3D();
Point3D p2 = edge.Trajectory?.Te ?? new Point3D();
Point3D sampledPoint = p1 + (p2 - p1) * t;
points.Add(sampledPoint);
}
return points;
}
else if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null)
{
// 圆弧段采样
return SampleArc(edge.Trajectory, samplingStep);
}
return new List<Point3D>();
}
/// <summary>
/// 对路径应用曲线化处理
/// </summary>
/// <param name="route">待处理的路径</param>
/// <param name="samplingStep">采样步长(米)</param>
public static void ApplyCurvatureToRoute(PathRoute route, double samplingStep)
{
if (route == null)
throw new ArgumentNullException(nameof(route));
route.Edges.Clear();
var sortedPoints = route.Points.OrderBy(p => p.Index).ToList();
if (sortedPoints.Count < 2)
{
route.IsCurved = false;
return;
}
// 处理每一段路径
for (int i = 0; i < sortedPoints.Count - 1; i++)
{
var p1 = sortedPoints[i];
var p2 = sortedPoints[i + 1];
// 判断是否为转折点(需要圆弧过渡)
bool needsArc = (i > 0 && i < sortedPoints.Count - 1);
if (needsArc)
{
var p0 = sortedPoints[i - 1];
var p2Next = sortedPoints[i + 2];
// 获取转向半径(优先使用自定义值)
double radius = p1.CustomTurnRadius ?? route.TurnRadius;
// 计算圆弧
var arcTraj = CalculateFillet(p0.Position, p1.Position, p2Next.Position, radius);
// 创建Edge包含圆弧段
var edge = BuildEdgeWithArc(p1, p2, arcTraj, samplingStep);
route.Edges.Add(edge);
}
else
{
// 直线边
var edge = BuildStraightEdge(p1, p2, samplingStep);
route.Edges.Add(edge);
}
}
route.IsCurved = true;
RecalculateRouteLength(route);
}
/// <summary>
/// 重新计算路径总长度
/// </summary>
/// <param name="route">路径</param>
public static void RecalculateRouteLength(PathRoute route)
{
if (route == null || route.Edges == null)
{
route.TotalLength = 0;
return;
}
route.TotalLength = route.Edges.Sum(e => e.PhysicalLength);
}
}
}