Align terminal rail normals and object space rendering

This commit is contained in:
tian 2026-03-23 12:25:00 +08:00
parent e9128282b5
commit 1cf4fe5967
13 changed files with 568 additions and 173 deletions

View File

@ -56,6 +56,7 @@
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalPlanarPoseBuilderTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ObjectSpaceOrientationHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailOffsetResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\CanonicalTrackedPositionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" />

View File

@ -338,6 +338,7 @@
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailOffsetResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalRailPoseBuilder.cs" />
<Compile Include="src\Utils\CoordinateSystem\CanonicalTrackedPositionResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />
<Compile Include="src\Utils\CoordinateSystem\HoistingCoordinateHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\LocalAxisDirection.cs" />

View File

@ -27,6 +27,61 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_ShouldCreatePlaneParallelToOpticalAxisAndContainBothPoints()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
AssemblyInstallationReferenceResult result =
AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
Assert.AreEqual(3f, result.OffsetDistance, 1e-4f);
Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneSpanDirection, result.OpticalAxisDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(result.PlaneNormal, result.PlaneSpanDirection), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(pickPoint1 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
Assert.AreEqual(0f, Vector3.Dot(pickPoint2 - result.AnchorPoint, result.PlaneNormal), 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_ShouldUseOpticalAxisProjectionAsInstallationCenterLine()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3f, 6f);
AssemblyInstallationReferenceResult result =
AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2);
Assert.AreEqual(0f, result.InstallLineBasePoint.X, 1e-4f);
Assert.AreEqual(3f, result.InstallLineBasePoint.Y, 1e-4f);
Assert.AreEqual(0f, result.InstallLineBasePoint.Z, 1e-4f);
Assert.AreEqual(4f, result.AnchorPoint.X, 1e-4f);
Assert.AreEqual(3f, result.AnchorPoint.Y, 1e-4f);
Assert.AreEqual(0f, result.AnchorPoint.Z, 1e-4f);
}
[TestMethod]
public void BuildFromTwoPoints_WhenPlanePointsAreNearlyAxisAligned_ShouldThrow()
{
Vector3 axisBase = new Vector3(0f, 0f, 0f);
Vector3 axisDirection = Vector3.UnitX;
Vector3 pickPoint1 = new Vector3(2f, 3f, 4f);
Vector3 pickPoint2 = new Vector3(6f, 3.00001f, 4.00001f);
InvalidOperationException ex = Assert.ThrowsException<InvalidOperationException>(
() => AssemblyInstallationReferenceBuilder.Build(axisBase, axisDirection, pickPoint1, pickPoint2));
StringAssert.Contains(ex.Message, "无法确定安装参考面");
}
[TestMethod]
public void Build_WhenPickPointTooCloseToOpticalAxis_ShouldThrow()
{

View File

@ -137,6 +137,23 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertVector(mappedUp, 0, 0, 1);
}
[TestMethod]
public void PreferredNormal_ShouldOverrideCanonicalUpForRailFrame()
{
bool ok = CanonicalRailPoseBuilder.TryCreateLocalFrame(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0),
Vector3.UnitZ,
Vector3.UnitY,
out RailLocalFrame frame);
Assert.IsTrue(ok);
AssertVector(frame.Forward, 1, 0, 0);
AssertVector(frame.Normal, 0, 1, 0);
AssertVector(frame.Lateral, 0, 0, -1);
}
private static void AssertColumn(Matrix4x4 matrix, int column, double x, double y, double z)
{
switch (column)

View File

@ -0,0 +1,40 @@
using System.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class ObjectSpaceOrientationHelperTests
{
[TestMethod]
public void CalculateAxes_ShouldHonorProvidedUpReference()
{
var segmentDirection = new Vector3(10f, 0f, 0f);
var upReference = new Vector3(0f, 1f, 0f);
var (_, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference);
Assert.AreEqual(0.0, up.X, 1e-6);
Assert.AreEqual(1.0, System.Math.Abs(up.Y), 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6);
}
[TestMethod]
public void CalculateAxes_ForVerticalSegment_ShouldUseHorizontalDirectionWithProvidedUpReference()
{
var segmentDirection = new Vector3(0f, 10f, 0f);
var upReference = new Vector3(0f, 1f, 0f);
var horizontalDirection = new Vector3(1f, 0f, 0f);
var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(segmentDirection, upReference, horizontalDirection);
Assert.AreEqual(0.0, right.X, 1e-6);
Assert.AreEqual(0.0, right.Y, 1e-6);
Assert.AreEqual(1.0, right.Z, 1e-6);
Assert.AreEqual(1.0, up.X, 1e-6);
Assert.AreEqual(0.0, up.Y, 1e-6);
Assert.AreEqual(0.0, up.Z, 1e-6);
}
}
}

View File

@ -671,6 +671,7 @@ namespace NavisworksTransport.Core.Animation
LogManager.Debug($"[移动到起点] Rail路径调整: 参考点=({_pathPoints[0].X:F2},{_pathPoints[0].Y:F2},{_pathPoints[0].Z:F2}), 物体中心=({startPosition.X:F2},{startPosition.Y:F2},{startPosition.Z:F2}), 物体高度={objectHeight:F2}, 安装={_route.RailMountMode}, 对接={(PathRoute.IsTopPayloadAnchorForMountMode(_route.RailMountMode) ? "" : "")}");
if (RailPathPoseHelper.TryCreateRailRotation(
_route,
previousPoint,
_pathPoints[0],
nextPoint,
@ -1034,6 +1035,7 @@ namespace NavisworksTransport.Core.Animation
if (_route.PathType == PathType.Rail &&
RailPathPoseHelper.TryCreateRailRotation(
_route,
previousFramePoint,
framePosition,
nextFramePoint,

View File

@ -737,6 +737,12 @@ namespace NavisworksTransport
/// </summary>
public RailPathDefinitionMode RailPathDefinitionMode { get; set; }
/// <summary>
/// Rail 路径优先法向(宿主坐标)。
/// 仅在存在显式安装面/业务法向时使用;为空时退回当前默认求解。
/// </summary>
public Point3D RailPreferredNormal { get; set; }
// 数据库分析相关属性
/// <summary>
/// 碰撞数量(从数据库加载)
@ -777,6 +783,7 @@ namespace NavisworksTransport
IsCurved = false;
RailMountMode = RailMountMode.UnderRail;
RailPathDefinitionMode = RailPathDefinitionMode.LegacySuspensionPoint;
RailPreferredNormal = null;
}
/// <summary>

View File

@ -297,6 +297,11 @@ namespace NavisworksTransport
/// </summary>
public Vector3D HorizontalDirection { get; set; }
/// <summary>
/// 通行空间的优先 up 方向(例如 Rail 安装面法向)
/// </summary>
public Vector3D UpDirection { get; set; }
public override string ToString()
{
return $"LineMarker[{FromIndex}->{ToIndex}, 类型={SegmentType}, 起点=({StartPoint.X:F2},{StartPoint.Y:F2},{StartPoint.Z:F2}), 终点=({EndPoint.X:F2},{EndPoint.Y:F2},{EndPoint.Z:F2})]";
@ -1658,7 +1663,8 @@ namespace NavisworksTransport
var endPoint = edge.SampledPoints.Last();
// 计算长方体的轴向量right和up向量
var (right, up) = CalculateCuboidAxes(startPoint, endPoint);
var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp);
// 根据路径类型调整起点和终点位置
// 关键修复垂直偏移应该沿着长方体的轴线方向up向量平移
@ -1710,7 +1716,8 @@ namespace NavisworksTransport
SampledPoints = edge.SampledPoints,
Height = height, // 根据模式设置高度
Width = width, // 根据模式设置宽度
Opacity = lineOpacity // 根据模式设置透明度
Opacity = lineOpacity, // 根据模式设置透明度
UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(lineMarker);
}
@ -1736,7 +1743,8 @@ namespace NavisworksTransport
var endPoint = edge.Trajectory.Te;
// 计算长方体的轴向量right和up向量
var (right, up) = CalculateCuboidAxes(startPoint, endPoint);
var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
var (right, up) = CalculateCuboidAxes(startPoint, endPoint, preferredUp);
// 根据路径类型调整起点和终点位置
// 关键修复垂直偏移应该沿着长方体的轴线方向up向量平移
@ -1818,7 +1826,8 @@ namespace NavisworksTransport
SampledPoints = adjustedSampledPoints,
Height = height, // 根据模式设置高度
Width = width, // 根据模式设置宽度
Opacity = arcLineOpacity // 根据模式设置透明度
Opacity = arcLineOpacity, // 根据模式设置透明度
UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(arcMarker);
@ -1838,73 +1847,13 @@ namespace NavisworksTransport
/// <param name="endPoint">路径段终点</param>
/// <param name="horizontalDirection">水平段方向向量(用于垂直路径渲染时确定通行空间方向)</param>
/// <returns>元组right向量up向量</returns>
private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D horizontalDirection = null)
private (Vector3D right, Vector3D up) CalculateCuboidAxes(Point3D startPoint, Point3D endPoint, Vector3D upReference = null, Vector3D horizontalDirection = null)
{
// 计算路径段的方向向量
var segmentDirection = new Vector3D(
endPoint.X - startPoint.X,
endPoint.Y - startPoint.Y,
endPoint.Z - startPoint.Z
);
var segmentLength = Math.Sqrt(
segmentDirection.X * segmentDirection.X +
segmentDirection.Y * segmentDirection.Y +
segmentDirection.Z * segmentDirection.Z
);
// 归一化方向向量
var normalizedDirection = new Vector3D(
segmentDirection.X / segmentLength,
segmentDirection.Y / segmentLength,
segmentDirection.Z / segmentLength
);
// 使用宿主 up 方向构造水平 right 向量,避免把世界 Z 写死到 Y-up 项目里。
var hostUp = GetHostUpVector();
var right = Cross(normalizedDirection, hostUp);
// 如果方向向量接近宿主 up垂直段则使用水平段方向重新构造 right。
double parallelToUp = Math.Abs(
normalizedDirection.X * hostUp.X +
normalizedDirection.Y * hostUp.Y +
normalizedDirection.Z * hostUp.Z);
if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
right = Cross(horizontalDirection, hostUp);
}
else
{
right = Math.Abs(hostUp.X) < 0.9
? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
: Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
// 归一化right向量
var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
if (rightLength > 0.001)
{
right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
}
// 计算高度向量(垂直于方向向量和宽度向量)- 这是长方体的轴线方向
// up = direction × right
var up = new Vector3D(
normalizedDirection.Y * right.Z - normalizedDirection.Z * right.Y,
normalizedDirection.Z * right.X - normalizedDirection.X * right.Z,
normalizedDirection.X * right.Y - normalizedDirection.Y * right.X
);
// 归一化up向量
var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
if (upLength > 0.001)
{
up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
}
return (right, up);
return ObjectSpaceOrientationHelper.CalculateAxes(
startPoint,
endPoint,
upReference ?? GetHostUpVector(),
horizontalDirection);
}
/// <summary>
@ -2009,7 +1958,8 @@ namespace NavisworksTransport
CalculatePassageSpaceParameters(visualization, isVerticalSegment, isTurn90Horizontal, isFirstOrLastSegment);
// 计算长方体的轴向量right和up向量
var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, horizontalDirection);
var preferredUp = ResolveObjectSpaceUpDirection(visualization.PathRoute);
var (right, up) = CalculateCuboidAxes(startPoint.Position, endPoint.Position, preferredUp, horizontalDirection);
// 根据路径类型调整起点和终点位置
// 关键修复垂直偏移应该沿着长方体的轴线方向up向量平移而不是简单地添加到Z坐标上
@ -2099,7 +2049,8 @@ namespace NavisworksTransport
Height = height,
Width = width,
Opacity = lineOpacity,
HorizontalDirection = horizontalDirection // 设置水平段方向向量
HorizontalDirection = horizontalDirection, // 设置水平段方向向量
UpDirection = preferredUp
};
visualization.PathLineMarkers.Add(lineMarker);
}
@ -2797,7 +2748,7 @@ namespace NavisworksTransport
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
/// <param name="horizontalDirection">水平段方向向量(用于垂直路径渲染时确定通行空间方向)</param>
private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null)
private void RenderCuboidMarker(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, Vector3D horizontalDirection = null, Vector3D upReference = null)
{
try
{
@ -2817,47 +2768,11 @@ namespace NavisworksTransport
// 归一化方向向量
direction = new Vector3D(direction.X / length, direction.Y / length, direction.Z / length);
var hostUp = GetHostUpVector();
var right = Cross(direction, hostUp);
double parallelToUp = Math.Abs(
direction.X * hostUp.X +
direction.Y * hostUp.Y +
direction.Z * hostUp.Z);
if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
right = Cross(horizontalDirection, hostUp);
}
else
{
right = Math.Abs(hostUp.X) < 0.9
? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
: Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
// 归一化right向量
var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
if (rightLength > 0.001)
{
right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
}
// 计算高度向量(垂直于方向向量和宽度向量)
var up = new Vector3D(
direction.Y * right.Z - direction.Z * right.Y,
direction.Z * right.X - direction.X * right.Z,
direction.X * right.Y - direction.Y * right.X
);
// 归一化up向量
var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
if (upLength > 0.001)
{
up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
}
var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(
startPoint,
endPoint,
upReference ?? GetHostUpVector(),
horizontalDirection);
// 计算长方体的边界框和向量
var halfWidth = width / 2.0;
@ -2894,7 +2809,7 @@ namespace NavisworksTransport
/// <param name="height">高度</param>
/// <param name="length">指定长度(覆盖默认计算值)</param>
/// <param name="horizontalDirection">水平段方向向量(用于垂直路径渲染时确定通行空间方向)</param>
private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null)
private void RenderCuboidMarkerWithLength(Graphics graphics, Point3D startPoint, Point3D endPoint, double width, double height, double length, Vector3D horizontalDirection = null, Vector3D upReference = null)
{
try
{
@ -2912,47 +2827,11 @@ namespace NavisworksTransport
direction = new Vector3D(direction.X / directionLength, direction.Y / directionLength, direction.Z / directionLength);
var hostUp = GetHostUpVector();
var right = Cross(direction, hostUp);
double parallelToUp = Math.Abs(
direction.X * hostUp.X +
direction.Y * hostUp.Y +
direction.Z * hostUp.Z);
if (parallelToUp > 0.9)
{
if (horizontalDirection != null)
{
right = Cross(horizontalDirection, hostUp);
}
else
{
right = Math.Abs(hostUp.X) < 0.9
? Normalize(Cross(hostUp, new Vector3D(1, 0, 0)))
: Normalize(Cross(hostUp, new Vector3D(0, 1, 0)));
}
}
// 归一化right向量
var rightLength = Math.Sqrt(right.X * right.X + right.Y * right.Y + right.Z * right.Z);
if (rightLength > 0.001)
{
right = new Vector3D(right.X / rightLength, right.Y / rightLength, right.Z / rightLength);
}
// 计算高度向量(垂直于方向向量和宽度向量)
var up = new Vector3D(
direction.Y * right.Z - direction.Z * right.Y,
direction.Z * right.X - direction.X * right.Z,
direction.X * right.Y - direction.Y * right.X
);
// 归一化up向量
var upLength = Math.Sqrt(up.X * up.X + up.Y * up.Y + up.Z * up.Z);
if (upLength > 0.001)
{
up = new Vector3D(up.X / upLength, up.Y / upLength, up.Z / upLength);
}
var (right, up) = ObjectSpaceOrientationHelper.CalculateAxes(
startPoint,
endPoint,
upReference ?? GetHostUpVector(),
horizontalDirection);
// 计算长方体的边界框和向量
var halfWidth = width / 2.0;
@ -3009,7 +2888,8 @@ namespace NavisworksTransport
lineMarker.EndPoint,
width,
lineMarker.Height,
lineMarker.HorizontalDirection
lineMarker.HorizontalDirection,
lineMarker.UpDirection
);
}
}
@ -3052,11 +2932,25 @@ namespace NavisworksTransport
width,
lineMarker.Height,
_passageAlongPath,
lineMarker.HorizontalDirection
lineMarker.HorizontalDirection,
lineMarker.UpDirection
);
}
}
private Vector3D ResolveObjectSpaceUpDirection(PathRoute route)
{
if (route?.PathType == NavisworksTransport.PathType.Rail && route.RailPreferredNormal != null)
{
return Normalize(new Vector3D(
route.RailPreferredNormal.X,
route.RailPreferredNormal.Y,
route.RailPreferredNormal.Z));
}
return GetHostUpVector();
}
/// <summary>
/// 计算采样点处的切线方向
/// </summary>

View File

@ -154,6 +154,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private Vector3 _assemblyInstallationPlaneNormal;
private Vector3 _assemblyInstallationPlaneSpanDirection;
private double _assemblyInstallationOffsetDistanceInMeters;
private readonly List<Point3D> _assemblyInstallationSeedPoints = new List<Point3D>();
private readonly List<Point3D> _assemblyEndFaceSeedPoints = new List<Point3D>();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
@ -1347,6 +1348,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
await SafeExecuteAsync(() =>
{
ClearNonGridPathVisualizations("[直线装配] 捕获箱体");
var document = NavisApplication.ActiveDocument;
var selectedItem = document?.CurrentSelection?.SelectedItems?.FirstOrDefault();
if (selectedItem == null)
@ -1379,6 +1382,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}, "捕获终点箱体");
}
private void ClearNonGridPathVisualizations(string context)
{
if (PathPointRenderPlugin.Instance == null)
{
return;
}
try
{
PathPointRenderPlugin.Instance.ClearPathsExcept(
"grid_visualization_all",
"grid_visualization_channel",
"grid_visualization_unknown",
"grid_visualization_obstacle",
"grid_visualization_door");
LogManager.Info($"{context}:已清除现有路径可视化显示(保留网格可视化)");
}
catch (Exception ex)
{
LogManager.Error($"{context}:清除现有路径可视化失败: {ex.Message}", ex);
throw;
}
}
private async Task ExecuteGenerateAssemblyReferenceRodAsync()
{
await SafeExecuteAsync(() =>
@ -1546,7 +1573,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。");
}
UpdateMainStatus("请在终点箱体表面点击安装点,系统将按该点计算安装参考面和终点锚点。");
_assemblyInstallationSeedPoints.Clear();
UpdateMainStatus("请在终点箱体表面连续点击两个安装面点,系统将按两点和光轴计算安装参考面与安装点。");
LogManager.Info("[直线装配] 已进入安装点拾取模式");
}, "选择安装点");
}
@ -1640,7 +1668,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
BuildAndRenderAssemblyInstallationReference(pickResult.Point);
_assemblyInstallationSeedPoints.Add(pickResult.Point);
RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
int pickedCount = _assemblyInstallationSeedPoints.Count;
LogManager.Info($"[直线装配] 已记录安装面点 {pickedCount}: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})");
if (pickedCount < 2)
{
UpdateMainStatus("已记录安装面点 1/2请继续在同一安装面上点击第二个点。");
return;
}
BuildAndRenderAssemblyInstallationReference(
_assemblyInstallationSeedPoints[0],
_assemblyInstallationSeedPoints[1]);
CleanupAssemblyInstallationSelection();
}, "处理安装点拾取");
}
@ -1665,6 +1707,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RailPathDefinitionMode = RailPathDefinitionMode.RailCenterLine
};
if (_hasAssemblyInstallationReference)
{
route.RailPreferredNormal = new Point3D(
_assemblyInstallationPlaneNormal.X,
_assemblyInstallationPlaneNormal.Y,
_assemblyInstallationPlaneNormal.Z);
}
route.AddPoint(new PathPoint(startPoint, "装配起点", PathPointType.StartPoint));
route.AddPoint(new PathPoint(endPoint, "装配终点", PathPointType.EndPoint));
@ -1706,7 +1756,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
GetAssemblyAnchorText());
OnPropertyChanged(nameof(PathRoutes));
LogManager.Info($"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})");
LogManager.Info(
$"[直线装配] 已生成并完成路径: {route.Name},起点=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}),终点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2})" +
$"{(route.RailPreferredNormal != null ? $"=({route.RailPreferredNormal.X:F3}, {route.RailPreferredNormal.Y:F3}, {route.RailPreferredNormal.Z:F3})" : string.Empty)}");
}
private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
@ -1882,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
: string.Format("{0}点=未选择", anchorText);
string installationText = _hasAssemblyInstallationReference
? string.Format(
"选定安装点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m",
"安装面点中点=({0:F2}, {1:F2}, {2:F2}),安装点=({3:F2}, {4:F2}, {5:F2}),偏距={6:F3}m",
_assemblyInstallationPickPoint.X,
_assemblyInstallationPickPoint.Y,
_assemblyInstallationPickPoint.Z,
@ -2214,7 +2266,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderStyleName.AssemblyGuideLine);
}
private void BuildAndRenderAssemblyInstallationReference(Point3D pickPoint)
private void BuildAndRenderAssemblyInstallationReference(Point3D firstPickPoint, Point3D secondPickPoint)
{
Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
Point3D sphereCenterPoint = new Point3D(
@ -2230,10 +2282,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
AssemblyInstallationReferenceResult result = AssemblyInstallationReferenceBuilder.Build(
new Vector3((float)opticalAxisReferencePoint.X, (float)opticalAxisReferencePoint.Y, (float)opticalAxisReferencePoint.Z),
opticalAxisDirection,
new Vector3((float)pickPoint.X, (float)pickPoint.Y, (float)pickPoint.Z));
new Vector3((float)firstPickPoint.X, (float)firstPickPoint.Y, (float)firstPickPoint.Z),
new Vector3((float)secondPickPoint.X, (float)secondPickPoint.Y, (float)secondPickPoint.Z));
_hasAssemblyInstallationReference = true;
_assemblyInstallationPickPoint = pickPoint;
_assemblyInstallationPickPoint = AssemblyEndFaceAnalyzer.ToPoint3D((result.PickPoint + result.SecondaryPickPoint) * 0.5f);
_assemblyInstallationBaseAnchorPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.AnchorPoint);
_assemblyInstallationPlaneNormal = result.PlaneNormal;
_assemblyInstallationPlaneSpanDirection = result.PlaneSpanDirection;
@ -2242,7 +2295,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
UpdateAssemblyInstallationAnchorFromVerticalOffset();
RenderAssemblyInstallationPickPoint(_assemblyInstallationPickPoint);
RenderAssemblyInstallationPickPoints(_assemblyInstallationSeedPoints);
RenderAssemblyInstallationCenterLine(AssemblyEndFaceAnalyzer.ToPoint3D(result.InstallLineBasePoint), result.OpticalAxisDirection);
RefreshAssemblyTerminalObjectInfo();
@ -2250,14 +2303,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
UpdateMainStatus(
$"安装参考已计算:安装点=({_assemblyInstallationAnchorPoint.X:F2}, {_assemblyInstallationAnchorPoint.Y:F2}, {_assemblyInstallationAnchorPoint.Z:F2}),偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m");
LogManager.Info(
$"[直线装配] 安装参考已计算: 选定安装点=({pickPoint.X:F3}, {pickPoint.Y:F3}, {pickPoint.Z:F3}), " +
$"[直线装配] 安装参考已计算: 安装面点1=({firstPickPoint.X:F3}, {firstPickPoint.Y:F3}, {firstPickPoint.Z:F3}), " +
$"安装面点2=({secondPickPoint.X:F3}, {secondPickPoint.Y:F3}, {secondPickPoint.Z:F3}), " +
$"安装点=({_assemblyInstallationAnchorPoint.X:F3}, {_assemblyInstallationAnchorPoint.Y:F3}, {_assemblyInstallationAnchorPoint.Z:F3}), " +
$"偏距={_assemblyInstallationOffsetDistanceInMeters:F3}m, 平面法向=({result.PlaneNormal.X:F4}, {result.PlaneNormal.Y:F4}, {result.PlaneNormal.Z:F4})");
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(AssemblyInstallationPointButtonText));
}
private void RenderAssemblyInstallationPickPoint(Point3D pickPoint)
private void RenderAssemblyInstallationPickPoints(IReadOnlyList<Point3D> pickPoints)
{
var renderPlugin = PathPointRenderPlugin.Instance;
if (renderPlugin == null)
@ -2271,7 +2325,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyInstallationPickPointPathId,
Description = "终端安装点"
};
AddVisualizationPoint(markerRoute, pickPoint, "安装点", PathPointType.WayPoint);
for (int i = 0; i < pickPoints.Count; i++)
{
AddVisualizationPoint(markerRoute, pickPoints[i], $"安装面点{i + 1}", PathPointType.WayPoint);
}
renderPlugin.RenderPointOnly(markerRoute);
}
@ -2462,6 +2519,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_assemblyInstallationPlaneNormal = default(Vector3);
_assemblyInstallationPlaneSpanDirection = default(Vector3);
_assemblyInstallationOffsetDistanceInMeters = 0.0;
_assemblyInstallationSeedPoints.Clear();
_assemblyAnchorVerticalOffsetInMeters = DefaultAssemblyAnchorVerticalOffsetInMeters;
OnPropertyChanged(nameof(AssemblyAnchorVerticalOffsetInMeters));
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));

View File

@ -8,6 +8,7 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
public Vector3 OpticalAxisBasePoint { get; set; }
public Vector3 OpticalAxisDirection { get; set; }
public Vector3 PickPoint { get; set; }
public Vector3 SecondaryPickPoint { get; set; }
public Vector3 PlaneNormal { get; set; }
public Vector3 PlaneSpanDirection { get; set; }
public float OffsetDistance { get; set; }
@ -57,6 +58,72 @@ namespace NavisworksTransport.Utils.GeometryAnalysis
OpticalAxisBasePoint = opticalAxisBasePoint,
OpticalAxisDirection = axisDir,
PickPoint = pickPoint,
SecondaryPickPoint = pickPoint,
PlaneNormal = planeNormal,
PlaneSpanDirection = planeSpanDirection,
OffsetDistance = offsetDistance,
InstallLineBasePoint = installLineBasePoint,
AnchorPoint = anchorPoint
};
}
public static AssemblyInstallationReferenceResult Build(
Vector3 opticalAxisBasePoint,
Vector3 opticalAxisDirection,
Vector3 firstPickPoint,
Vector3 secondPickPoint)
{
float axisLength = opticalAxisDirection.Length();
if (axisLength < 1e-6f)
{
throw new InvalidOperationException("光轴方向长度过小,无法计算安装参考。");
}
Vector3 axisDir = opticalAxisDirection / axisLength;
Vector3 pickSegment = secondPickPoint - firstPickPoint;
Vector3 axisAlignedComponent = axisDir * Vector3.Dot(pickSegment, axisDir);
Vector3 planeSpanDirection = pickSegment - axisAlignedComponent;
float spanLength = planeSpanDirection.Length();
if (spanLength < MinimumOffsetDistance)
{
throw new InvalidOperationException("两次安装点拾取几乎与光轴平行,无法确定安装参考面,请重新选择。");
}
planeSpanDirection /= spanLength;
Vector3 midpoint = (firstPickPoint + secondPickPoint) * 0.5f;
Vector3 planeNormal = Vector3.Cross(axisDir, planeSpanDirection);
float normalLength = planeNormal.Length();
if (normalLength < 1e-6f)
{
throw new InvalidOperationException("安装参考面法向计算失败,请重新选择。");
}
planeNormal /= normalLength;
Vector3 midpointOffset = midpoint - opticalAxisBasePoint;
float signedOffset = Vector3.Dot(midpointOffset, planeNormal);
if (Math.Abs(signedOffset) < MinimumOffsetDistance)
{
throw new InvalidOperationException("安装面中点距离光轴过小,无法确定安装参考面,请重新选择。");
}
if (signedOffset < 0f)
{
planeNormal = -planeNormal;
signedOffset = -signedOffset;
}
float offsetDistance = signedOffset;
Vector3 installLineBasePoint = opticalAxisBasePoint + planeNormal * offsetDistance;
Vector3 anchorPoint = ProjectPointToLine(midpoint, installLineBasePoint, axisDir);
return new AssemblyInstallationReferenceResult
{
OpticalAxisBasePoint = opticalAxisBasePoint,
OpticalAxisDirection = axisDir,
PickPoint = firstPickPoint,
SecondaryPickPoint = secondPickPoint,
PlaneNormal = planeNormal,
PlaneSpanDirection = planeSpanDirection,
OffsetDistance = offsetDistance,

View File

@ -17,6 +17,23 @@ namespace NavisworksTransport.Utils.CoordinateSystem
Vector3 nextPoint,
Vector3 canonicalUp,
out RailLocalFrame frame)
{
return TryCreateLocalFrame(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
null,
out frame);
}
public static bool TryCreateLocalFrame(
Vector3 previousPoint,
Vector3 currentPoint,
Vector3 nextPoint,
Vector3 canonicalUp,
Vector3? preferredNormal,
out RailLocalFrame frame)
{
frame = null;
@ -25,6 +42,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
currentPoint,
nextPoint,
canonicalUp,
preferredNormal,
out var forward,
out var lateral,
out var up))
@ -44,6 +62,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
out Vector3 forward,
out Vector3 lateral,
out Vector3 up)
{
return TryCreateBasis(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
null,
out forward,
out lateral,
out up);
}
public static bool TryCreateBasis(
Vector3 previousPoint,
Vector3 currentPoint,
Vector3 nextPoint,
Vector3 canonicalUp,
Vector3? preferredNormal,
out Vector3 forward,
out Vector3 lateral,
out Vector3 up)
{
forward = default;
lateral = default;
@ -61,7 +100,13 @@ namespace NavisworksTransport.Utils.CoordinateSystem
}
forward = Vector3.Normalize(tangent);
Vector3 projectedUp = canonicalUp - Vector3.Dot(canonicalUp, forward) * forward;
Vector3 referenceUp = canonicalUp;
if (preferredNormal.HasValue && preferredNormal.Value.LengthSquared() >= TangentEpsilon)
{
referenceUp = Vector3.Normalize(preferredNormal.Value);
}
Vector3 projectedUp = referenceUp - Vector3.Dot(referenceUp, forward) * forward;
if (projectedUp.LengthSquared() < TangentEpsilon)
{
projectedUp = Math.Abs(forward.Z) < 0.9f
@ -95,6 +140,7 @@ namespace NavisworksTransport.Utils.CoordinateSystem
nextPoint,
canonicalUp,
convention,
null,
0.0,
out rotation);
}
@ -107,6 +153,27 @@ namespace NavisworksTransport.Utils.CoordinateSystem
ModelAxisConvention convention,
double localUpRotationDegrees,
out Quaternion rotation)
{
return TryCreateQuaternion(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
convention,
null,
localUpRotationDegrees,
out rotation);
}
public static bool TryCreateQuaternion(
Vector3 previousPoint,
Vector3 currentPoint,
Vector3 nextPoint,
Vector3 canonicalUp,
ModelAxisConvention convention,
Vector3? preferredNormal,
double localUpRotationDegrees,
out Quaternion rotation)
{
rotation = Quaternion.Identity;
@ -115,7 +182,15 @@ namespace NavisworksTransport.Utils.CoordinateSystem
throw new ArgumentNullException(nameof(convention));
}
if (!TryCreateBasis(previousPoint, currentPoint, nextPoint, canonicalUp, out var forward, out _, out var up))
if (!TryCreateBasis(
previousPoint,
currentPoint,
nextPoint,
canonicalUp,
preferredNormal,
out var forward,
out _,
out var up))
{
return false;
}

View File

@ -0,0 +1,92 @@
using System;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.CoordinateSystem
{
public static class ObjectSpaceOrientationHelper
{
public static (Vector3D right, Vector3D up) CalculateAxes(
Point3D startPoint,
Point3D endPoint,
Vector3D upReference,
Vector3D horizontalDirection = null)
{
var segmentDirection = new Vector3(
(float)(endPoint.X - startPoint.X),
(float)(endPoint.Y - startPoint.Y),
(float)(endPoint.Z - startPoint.Z));
var (right, up) = CalculateAxes(
segmentDirection,
ToNumerics(upReference),
horizontalDirection == null ? (Vector3?)null : ToNumerics(horizontalDirection));
return (ToNavisworks(right), ToNavisworks(up));
}
public static (Vector3 right, Vector3 up) CalculateAxes(
Vector3 segmentDirection,
Vector3 upReference,
Vector3? horizontalDirection = null)
{
double segmentLength = Math.Sqrt(
segmentDirection.X * segmentDirection.X +
segmentDirection.Y * segmentDirection.Y +
segmentDirection.Z * segmentDirection.Z);
if (segmentLength < 1e-9)
{
throw new InvalidOperationException("路径段长度过小,无法计算通行空间方向。");
}
var normalizedDirection = Vector3.Normalize(segmentDirection);
var normalizedUpReference = Normalize(upReference);
var right = Cross(normalizedDirection, normalizedUpReference);
double parallelToUp = Math.Abs(Vector3.Dot(normalizedDirection, normalizedUpReference));
if (parallelToUp > 0.9)
{
if (horizontalDirection.HasValue)
{
right = Cross(horizontalDirection.Value, normalizedUpReference);
}
else
{
right = Math.Abs(normalizedUpReference.X) < 0.9f
? Normalize(Cross(normalizedUpReference, new Vector3(1f, 0f, 0f)))
: Normalize(Cross(normalizedUpReference, new Vector3(0f, 1f, 0f)));
}
}
right = Normalize(right);
var up = Normalize(Cross(normalizedDirection, right));
return (right, up);
}
private static Vector3 Cross(Vector3 a, Vector3 b)
{
return Vector3.Cross(a, b);
}
private static Vector3 Normalize(Vector3 vector)
{
if (vector.LengthSquared() < 1e-9f)
{
return Vector3.Zero;
}
return Vector3.Normalize(vector);
}
private static Vector3 ToNumerics(Vector3D vector)
{
return new Vector3((float)vector.X, (float)vector.Y, (float)vector.Z);
}
private static Vector3D ToNavisworks(Vector3 vector)
{
return new Vector3D(vector.X, vector.Y, vector.Z);
}
}
}

View File

@ -135,7 +135,7 @@ namespace NavisworksTransport.Utils
var adapterForRail = CoordinateSystemManager.Instance.CreateHostAdapter();
var canonicalReferencePointForRail = adapterForRail.ToCanonicalPoint(referencePoint);
if (!TryCreateCanonicalLocalFrame(previousPoint, referencePoint, nextPoint, out RailLocalFrame frame))
if (!TryCreateCanonicalLocalFrame(route, previousPoint, referencePoint, nextPoint, out RailLocalFrame frame))
{
var canonicalCenterPointForRailFallback = new Point3D(
canonicalReferencePointForRail.X,
@ -165,6 +165,22 @@ namespace NavisworksTransport.Utils
out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
null,
previousPoint,
currentPoint,
nextPoint,
out linearTransform);
}
public static bool TryCreateRailLinearTransform(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
route,
previousPoint,
currentPoint,
nextPoint,
@ -177,6 +193,7 @@ namespace NavisworksTransport.Utils
/// 通过模型局部轴约定显式指定本地哪个轴代表 forward/up。
/// </summary>
public static bool TryCreateRailLinearTransform(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@ -184,6 +201,7 @@ namespace NavisworksTransport.Utils
out Matrix3 linearTransform)
{
return TryCreateRailLinearTransform(
route,
previousPoint,
currentPoint,
nextPoint,
@ -193,6 +211,7 @@ namespace NavisworksTransport.Utils
}
public static bool TryCreateRailLinearTransform(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@ -217,6 +236,7 @@ namespace NavisworksTransport.Utils
canonicalCurrentPoint,
canonicalNextPoint,
HostCoordinateAdapter.CanonicalUpVector3,
ResolvePreferredNormal(route),
out RailLocalFrame frame))
{
return false;
@ -231,6 +251,7 @@ namespace NavisworksTransport.Utils
canonicalNextPoint,
HostCoordinateAdapter.CanonicalUpVector3,
axisConvention,
ResolvePreferredNormal(route),
localUpRotationDegrees,
out var correctedRotation)
? correctedRotation
@ -270,6 +291,22 @@ namespace NavisworksTransport.Utils
out Rotation3D rotation)
{
return TryCreateRailRotation(
null,
previousPoint,
currentPoint,
nextPoint,
out rotation);
}
public static bool TryCreateRailRotation(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
out Rotation3D rotation)
{
return TryCreateRailRotation(
route,
previousPoint,
currentPoint,
nextPoint,
@ -285,6 +322,24 @@ namespace NavisworksTransport.Utils
out Rotation3D rotation)
{
return TryCreateRailRotation(
null,
previousPoint,
currentPoint,
nextPoint,
axisConvention,
out rotation);
}
public static bool TryCreateRailRotation(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
ModelAxisConvention axisConvention,
out Rotation3D rotation)
{
return TryCreateRailRotation(
route,
previousPoint,
currentPoint,
nextPoint,
@ -294,6 +349,7 @@ namespace NavisworksTransport.Utils
}
public static bool TryCreateRailRotation(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@ -305,6 +361,7 @@ namespace NavisworksTransport.Utils
LogRotationConstructorConventionOnce();
if (!TryCreateRailLinearTransform(
route,
previousPoint,
currentPoint,
nextPoint,
@ -446,7 +503,7 @@ namespace NavisworksTransport.Utils
private static Vector3D ResolveRailNormal(Point3D previousPoint, Point3D currentPoint, Point3D nextPoint)
{
if (TryCreateCanonicalLocalFrame(previousPoint, currentPoint, nextPoint, out RailLocalFrame frame))
if (TryCreateCanonicalLocalFrame(null, previousPoint, currentPoint, nextPoint, out RailLocalFrame frame))
{
return ToNavVector(frame.Normal);
}
@ -479,6 +536,7 @@ namespace NavisworksTransport.Utils
}
private static bool TryCreateCanonicalLocalFrame(
PathRoute route,
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
@ -490,9 +548,37 @@ namespace NavisworksTransport.Utils
ToNumerics(adapter.ToCanonicalPoint(currentPoint)),
ToNumerics(adapter.ToCanonicalPoint(nextPoint)),
HostCoordinateAdapter.CanonicalUpVector3,
ResolvePreferredNormal(route),
out frame);
}
private static Vector3? ResolvePreferredNormal(PathRoute route)
{
if (route?.RailPreferredNormal == null)
{
return null;
}
var preferredNormal = new Vector3(
(float)route.RailPreferredNormal.X,
(float)route.RailPreferredNormal.Y,
(float)route.RailPreferredNormal.Z);
if (preferredNormal.LengthSquared() < 1e-9f)
{
return null;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Vector3 canonicalPreferredNormal = adapter.ToCanonicalVector3(preferredNormal);
if (canonicalPreferredNormal.LengthSquared() < 1e-9f)
{
return null;
}
return Vector3.Normalize(canonicalPreferredNormal);
}
private static Vector3D Normalize(Vector3D vector)
{
double lengthSquared = vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z;