diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 919214e..79d391a 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -470,13 +470,6 @@ - - - - PreserveNewest - TransportPlugin.name.txt - - @@ -486,6 +479,11 @@ PreserveNewest resources\default_config.toml + + + PreserveNewest + SQLite.Interop.dll + PreserveNewest @@ -496,6 +494,11 @@ PreserveNewest resources\unit_cube.nwc + + + PreserveNewest + resources\unit_cylinder.nwc + diff --git a/deploy-plugin.bat b/deploy-plugin.bat index 55e87ff..1cce12f 100644 --- a/deploy-plugin.bat +++ b/deploy-plugin.bat @@ -1,6 +1,7 @@ @echo off +setlocal -:: 如果 Navisworks 正在运行,关闭它以释放 DLL 锁定 +:: Stop Navisworks to release locked plugin DLLs. taskkill /F /IM Roamer.exe 2>nul if %errorlevel% == 0 ( echo Navisworks process terminated. @@ -8,16 +9,18 @@ if %errorlevel% == 0 ( ) set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin" +set "BUILD_DIR=bin\x64\Release" if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%" -copy "bin\x64\Release\TransportPlugin.dll" "%TARGET_DIR%\" >nul +copy "%BUILD_DIR%\*.dll" "%TARGET_DIR%\" >nul +echo Plugin DLLs deployed. -:: 复制resources文件夹(包含chart.js等本地资源) -if exist "bin\x64\Release\resources" ( +:: Copy bundled resources. +if exist "%BUILD_DIR%\resources" ( if not exist "%TARGET_DIR%\resources" mkdir "%TARGET_DIR%\resources" - copy "bin\x64\Release\resources\*" "%TARGET_DIR%\resources\" >nul + xcopy "%BUILD_DIR%\resources\*" "%TARGET_DIR%\resources\" /E /I /Y /Q >nul echo Resources folder deployed. ) -echo Plugin deployed successfully! \ No newline at end of file +echo Plugin deployed successfully! diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md index 7caa4f3..a9d16fd 100644 --- a/doc/design/2026/NavisworksAPI使用方法.md +++ b/doc/design/2026/NavisworksAPI使用方法.md @@ -703,6 +703,131 @@ var transform = components.Combine(); // 结果:物体"公转"到错误位置 ``` +#### 11.7.2.1 Rotation3D 构造函数的真实语义(2026-03-19 新增,重要) + +这次在“直线装配参考杆”功能中,验证了一个很容易误用的点: + +- `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` **不是**“传入本地 X/Y/Z 三个轴来直接构造三维姿态” +- 之前如果把它当成“三轴姿态构造器”使用,会得到完全错误的旋转结果 + +根据 Navisworks .NET API HTML 文档: + +- `Rotation3D(UnitVector3D vector1, UnitVector3D vector2)` + - 文档摘要:`Creates rotation that rotates vector1 to same direction as vector2` + - 正确语义:**将向量1旋转到与向量2同方向** +- `Rotation3D(UnitVector3D axis, double angle)` + - 文档摘要:`Creates rotation about given axis by angle in radians` + - 正确语义:**绕指定轴按弧度旋转** +- `Rotation3D(UnitVector3D axis, UnitVector3D vector1, UnitVector3D vector2)` + - 文档摘要:`Creates rotation about axis by angle between v1 and v2 projected onto plane normal to axis.` + - 正确语义:**绕 axis 旋转,旋转角由 vector1 和 vector2 在垂直于 axis 的平面上的投影夹角决定** + +**结论:** + +- 3 参数版本是“轴 + 两个参考向量”的构造,不是“三个基向量/三个坐标轴” +- 如果需求是“让物体本地某个轴对齐任意空间方向”,优先使用 **2 参数版本** + +#### 11.7.2.2 三维参考杆/任意向量对齐的正确用法 + +适用场景: + +- 参考杆、箭头、圆柱体等“有明确本地长度轴”的资源 +- 需要将资源的本地某个轴,对齐到一个任意空间向量 + +例如: + +- 资源中心在原点 +- 长轴沿本地 `+X` +- 目标方向是参考线方向 `referenceDirection` + +那么旋转应直接写成: + +```csharp +var rotation = new Rotation3D( + new UnitVector3D(1, 0, 0), + new UnitVector3D(referenceDirection)); +``` + +其含义非常直接: + +- 把资源的本地 `+X` 轴 +- 旋转到 `referenceDirection` + +这正是“直线装配参考杆”最终验证通过的做法。 + +**不要写成下面这样:** + +```csharp +// ❌ 错误:把三个向量误当成 X/Y/Z 三个姿态轴 +var rotation = new Rotation3D( + new UnitVector3D(tangent), + new UnitVector3D(lateral), + new UnitVector3D(normal)); +``` + +这段代码的真实语义并不是“建立一个由 tangent/lateral/normal 组成的姿态”, +而是“绕 tangent 这个轴,根据 lateral 和 normal 的投影夹角去旋转”, +所以结果通常会完全错误。 + +#### 11.7.2.3 三维旋转与平移补偿必须分开理解 + +即使 `Rotation3D(vector1, vector2)` 构造正确,仍然不能忽略 Navisworks 的旋转中心限制: + +- `OverridePermanentTransform()` 仍然是**增量变换** +- 旋转仍然是**绕世界原点** +- 因此三维姿态正确后,位置仍需要像 yaw 场景一样做补偿 + +对于“资源中心在原点、目标中心为 `targetCenter`”的对象,推荐模式是: + +```csharp +// 1. 从资源原始状态出发 +doc.Models.ResetPermanentTransform(modelItems); + +// 2. 计算旋转:本地 X -> 目标方向 +var rotation = new Rotation3D( + new UnitVector3D(1, 0, 0), + new UnitVector3D(referenceDirection)); + +// 3. 计算原始中心绕世界原点旋转后的位置 +var originalCenter = item.BoundingBox().Center; +var rotationTransform = new Transform3D(rotation); +var linear = rotationTransform.Linear; +var rotatedCenter = new Point3D( + linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, + linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, + linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); + +// 4. 用补偿平移把旋转后的中心送到目标中心 +var translation = new Vector3D( + targetCenter.X - rotatedCenter.X, + targetCenter.Y - rotatedCenter.Y, + targetCenter.Z - rotatedCenter.Z); + +// 5. 组合并应用增量变换 +var transform = new Transform3D(rotation, translation); +doc.Models.OverridePermanentTransform(modelItems, transform, false); +``` + +**关键原则:** + +- 先把“旋转方向”算对 +- 再按“绕原点旋转后的实际位置”计算平移补偿 +- 不要把“姿态构造错误”和“旋转中心补偿缺失”混成一个问题 + +#### 11.7.2.4 这类问题的调试建议 + +当三维物体“位置大概对,但方向明显错”时,优先检查: + +1. 是否误用了 `Rotation3D(UnitVector3D, UnitVector3D, UnitVector3D)` +2. 资源文件的本地长度轴是否真的与你的代码约定一致 +3. 是否把 `ModelItem.Transform` 错当成 override 后的真实世界姿态 + +推荐观察项: + +- 参考线/目标向量本身是否正确 +- 资源本地长度轴约定(例如是否沿本地 `+X`) +- 当前实际显示位置,优先看 `BoundingBox()`,不要只看 `ModelItem.Transform` + #### 11.7.3 正确实现"绕物体中心旋转" **解决方案:手动计算旋转导致的位置偏移并补偿** diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index e7a8cce..17d59b3 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -2,6 +2,10 @@ ## 功能点 +### [2026/3/18] + +1. [x](功能)增加轨上路径(角度可倾斜) + ### [2026/3/11] 1. [x] (功能)给“导出剖面盒”增加导出nwd文件的选项 diff --git a/resources/unit_cylinder.nwc b/resources/unit_cylinder.nwc new file mode 100644 index 0000000..540b4e6 Binary files /dev/null and b/resources/unit_cylinder.nwc differ diff --git a/src/Core/AssemblyReferencePathManager.cs b/src/Core/AssemblyReferencePathManager.cs index 8505cc3..7191415 100644 --- a/src/Core/AssemblyReferencePathManager.cs +++ b/src/Core/AssemblyReferencePathManager.cs @@ -9,8 +9,8 @@ namespace NavisworksTransport.Core { /// /// 直线装配参考路径管理器。 - /// 优先使用单位圆柱体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。 - /// 如果圆柱资源缺失,则退回到单位立方体资源。 + /// 使用单位立方体资源生成一根可点击的临时参考杆,供用户在 3D 视图中选择起点。 + /// 参考杆本质上表达的是一段 start->end 参考轴线,而不是终点处的独立构件。 /// public class AssemblyReferencePathManager { @@ -27,7 +27,6 @@ namespace NavisworksTransport.Core private Point3D _referenceLineEnd = Point3D.Origin; private double _referenceRodDiameterInMeters; private string _referenceResourceName; - public static AssemblyReferencePathManager Instance { get @@ -50,7 +49,10 @@ namespace NavisworksTransport.Core public ModelItem CurrentReferenceRod => _referenceRodModelItem; public bool IsReferenceRodVisible => _isReferenceRodVisible; public bool IsUpdatingReferenceRod => _isUpdatingReferenceRod; - public bool HasReferenceLine => _referenceRodModelItem != null && _isReferenceRodVisible; + public bool HasReferenceLine => VectorLengthSquared(new Vector3D( + _referenceLineEnd.X - _referenceLineStart.X, + _referenceLineEnd.Y - _referenceLineStart.Y, + _referenceLineEnd.Z - _referenceLineStart.Z)) > 1e-9; public Point3D ReferenceLineStart => _referenceLineStart; public Point3D ReferenceLineEnd => _referenceLineEnd; public string ReferenceResourceName => _referenceResourceName; @@ -62,6 +64,7 @@ namespace NavisworksTransport.Core /// /// 创建或更新参考杆。 + /// 先记录参考线主数据,再基于参考线更新可点击实体外壳。 /// public ModelItem CreateOrUpdateReferenceRod(Point3D startPoint, Point3D endPoint, double diameterInMeters) { @@ -80,19 +83,19 @@ namespace NavisworksTransport.Core _isUpdatingReferenceRod = true; try { - EnsureReferenceRodModelLoaded(); - - double lengthInMeters = UnitsConverter.ConvertToMeters(referenceLength); - ScaleReferenceRod(lengthInMeters, diameterInMeters); - PositionReferenceRod(startPoint, endPoint); - - ShowReferenceRod(); - _referenceLineStart = startPoint; _referenceLineEnd = endPoint; _referenceRodDiameterInMeters = diameterInMeters; - LogManager.Info($"[装配参考杆] 已创建/更新,长度={lengthInMeters:F3}m,直径={diameterInMeters:F3}m"); + EnsureReferenceRodModelLoaded(); + + ScaleReferenceRod(referenceLength, diameterInMeters); + PositionReferenceRod(); + + ShowReferenceRod(); + + double referenceLengthInMeters = UnitsConverter.ConvertToMeters(referenceLength); + LogManager.Info($"[装配参考杆] 已创建/更新,长度={referenceLengthInMeters:F3}m,直径={diameterInMeters:F3}m"); return _referenceRodModelItem; } finally @@ -108,7 +111,7 @@ namespace NavisworksTransport.Core return; } - var items = new ModelItemCollection { _referenceRodModelItem }; + var items = new ModelItemCollection { _referenceRodModel.RootItem }; Application.ActiveDocument.Models.SetHidden(items, false); _isReferenceRodVisible = true; } @@ -120,7 +123,7 @@ namespace NavisworksTransport.Core return; } - var items = new ModelItemCollection { _referenceRodModelItem }; + var items = new ModelItemCollection { _referenceRodModel.RootItem }; Application.ActiveDocument.Models.SetHidden(items, true); _isReferenceRodVisible = false; } @@ -199,19 +202,22 @@ namespace NavisworksTransport.Core } _referenceRodModel = doc.Models.Last(); - _referenceRodModelItem = FindFirstGeometryItem(_referenceRodModel.RootItem); + _referenceRodModelItem = _referenceRodModel.RootItem; _isReferenceRodVisible = true; _referenceResourceName = resourceName; LogManager.Info($"[装配参考杆] 已加载资源: {resourceName}"); } - private void ScaleReferenceRod(double lengthInMeters, double diameterInMeters) + private void ScaleReferenceRod(double length, double diameterInMeters) { if (_referenceRodModel == null) { return; } + var doc = Application.ActiveDocument; + double lengthInMeters = UnitsConverter.ConvertToMeters(length); + var currentTransform = _referenceRodModel.Transform; var components = currentTransform.Factor(); components.Scale = new Vector3D( @@ -220,54 +226,36 @@ namespace NavisworksTransport.Core diameterInMeters / ReferenceRodBaseSizeInMeters); Transform3D scaledTransform = components.Combine(); - Application.ActiveDocument.Models.SetModelUnitsAndTransform( + doc.Models.SetModelUnitsAndTransform( _referenceRodModel, _referenceRodModel.Units, scaledTransform, false); + + LogManager.Info( + $"[装配参考杆] 缩放完成,目标长度={lengthInMeters:F3}m,目标直径={diameterInMeters:F3}m,模型单位={_referenceRodModel.Units}"); } - private void PositionReferenceRod(Point3D startPoint, Point3D endPoint) + private void PositionReferenceRod() { - if (_referenceRodModelItem == null) + if (_referenceRodModelItem == null || _referenceRodModel == null) { return; } - var doc = Application.ActiveDocument; - var modelItems = new ModelItemCollection { _referenceRodModelItem }; - var currentScale = _referenceRodModelItem.Transform.Factor().Scale; + Point3D targetCenter = new Point3D( + (_referenceLineStart.X + _referenceLineEnd.X) / 2.0, + (_referenceLineStart.Y + _referenceLineEnd.Y) / 2.0, + (_referenceLineStart.Z + _referenceLineEnd.Z) / 2.0); - doc.Models.ResetPermanentTransform(modelItems); + Rotation3D rotation = CreateLineRotation(_referenceLineStart, _referenceLineEnd); + ModelItemTransformHelper.MoveItemToCenterAndRotation( + _referenceRodModelItem, + targetCenter, + rotation); - var originalBounds = _referenceRodModelItem.BoundingBox(); - var originalCenter = originalBounds.Center; - var targetCenter = new Point3D( - (startPoint.X + endPoint.X) / 2.0, - (startPoint.Y + endPoint.Y) / 2.0, - (startPoint.Z + endPoint.Z) / 2.0); - - Rotation3D rotation = CreateLineRotation(startPoint, endPoint); - - var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); - var components = identity.Factor(); - components.Scale = currentScale; - components.Rotation = rotation; - - var preTransform = components.Combine(); - var linear = preTransform.Linear; - var transformedCenter = new Point3D( - linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, - linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, - linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); - - components.Translation = new Vector3D( - targetCenter.X - transformedCenter.X, - targetCenter.Y - transformedCenter.Y, - targetCenter.Z - transformedCenter.Z); - - Transform3D transform = components.Combine(); - doc.Models.OverridePermanentTransform(modelItems, transform, false); + LogManager.Info( + $"[装配参考杆] 定位完成,中心=({targetCenter.X:F3}, {targetCenter.Y:F3}, {targetCenter.Z:F3}),起点=({_referenceLineStart.X:F3}, {_referenceLineStart.Y:F3}, {_referenceLineStart.Z:F3}),终点=({_referenceLineEnd.X:F3}, {_referenceLineEnd.Y:F3}, {_referenceLineEnd.Z:F3})"); } private static Rotation3D CreateLineRotation(Point3D startPoint, Point3D endPoint) @@ -277,28 +265,12 @@ namespace NavisworksTransport.Core endPoint.Y - startPoint.Y, endPoint.Z - startPoint.Z)); - var worldUp = new Vector3D(0, 0, 1); - var projectedNormal = new Vector3D( - worldUp.X - Dot(worldUp, tangent) * tangent.X, - worldUp.Y - Dot(worldUp, tangent) * tangent.Y, - worldUp.Z - Dot(worldUp, tangent) * tangent.Z); - - if (VectorLengthSquared(projectedNormal) < 1e-9) - { - var fallbackUp = new Vector3D(0, 1, 0); - projectedNormal = new Vector3D( - fallbackUp.X - Dot(fallbackUp, tangent) * tangent.X, - fallbackUp.Y - Dot(fallbackUp, tangent) * tangent.Y, - fallbackUp.Z - Dot(fallbackUp, tangent) * tangent.Z); - } - - var normal = Normalize(projectedNormal); - var lateral = Normalize(Cross(normal, tangent)); - + // 参考杆资源约定:几何中心在原点,长度轴沿局部 +X。 + // Navisworks API 文档中 Rotation3D(vector1, vector2) 的语义 + // 是“将 vector1 旋转到与 vector2 同方向”,这里直接将本地 X 轴对齐参考线方向。 return new Rotation3D( - new UnitVector3D(tangent), - new UnitVector3D(lateral), - new UnitVector3D(normal)); + new UnitVector3D(1, 0, 0), + new UnitVector3D(tangent)); } private static Point3D ProjectPointToSegment(Point3D point, Point3D segmentStart, Point3D segmentEnd) @@ -325,19 +297,6 @@ namespace NavisworksTransport.Core segmentStart.Z + projection * segmentZ); } - private static Vector3D Cross(Vector3D a, Vector3D b) - { - return new Vector3D( - a.Y * b.Z - a.Z * b.Y, - a.Z * b.X - a.X * b.Z, - a.X * b.Y - a.Y * b.X); - } - - private static double Dot(Vector3D a, Vector3D b) - { - return a.X * b.X + a.Y * b.Y + a.Z * b.Z; - } - private static Vector3D Normalize(Vector3D vector) { double lengthSquared = VectorLengthSquared(vector); @@ -355,30 +314,6 @@ namespace NavisworksTransport.Core return vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z; } - private static ModelItem FindFirstGeometryItem(ModelItem item) - { - if (item == null) - { - return null; - } - - if (item.HasGeometry) - { - return item; - } - - foreach (var child in item.Children) - { - var geometryItem = FindFirstGeometryItem(child); - if (geometryItem != null) - { - return geometryItem; - } - } - - return item; - } - private static string GetReferenceRodResourcePath(out string resourceName) { string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); diff --git a/src/Core/PathCurveEngine.cs b/src/Core/PathCurveEngine.cs index b895442..fac612c 100644 --- a/src/Core/PathCurveEngine.cs +++ b/src/Core/PathCurveEngine.cs @@ -439,7 +439,6 @@ namespace NavisworksTransport var edge = BuildStraightEdge(sortedPoints[0], sortedPoints[1], samplingStep); route.Edges.Add(edge); route.IsCurved = true; - RecalculateRouteLength(route); return; } @@ -493,7 +492,6 @@ namespace NavisworksTransport route.Edges.Add(lastEdge); route.IsCurved = true; - RecalculateRouteLength(route); } /// @@ -510,4 +508,4 @@ namespace NavisworksTransport // 长度会自动从 Edges 或 Points 计算 } } -} \ No newline at end of file +} diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index d3fd3ab..9355a2a 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -127,6 +127,11 @@ namespace NavisworksTransport /// RailBaseline, + /// + /// 直线装配基准向量样式(绿色) + /// + AssemblyGuideLine, + /// /// 吊装路径样式(紫色) /// @@ -1126,6 +1131,14 @@ namespace NavisworksTransport /// 空轨模型ID /// 基准路径点列表 public void RenderRailBaseline(string railModelId, List baselinePoints) + { + RenderRailBaseline(railModelId, baselinePoints, RenderStyleName.RailBaseline); + } + + /// + /// 按指定样式渲染基准路径。 + /// + public void RenderRailBaseline(string railModelId, List baselinePoints, RenderStyleName renderStyleName) { if (string.IsNullOrEmpty(railModelId) || baselinePoints == null || baselinePoints.Count < 2) { @@ -1146,7 +1159,7 @@ namespace NavisworksTransport LastUpdated = DateTime.Now }; - var renderStyle = GetRenderStyle(RenderStyleName.RailBaseline); + var renderStyle = GetRenderStyle(renderStyleName); // 创建连线标记 for (int i = 0; i < baselinePoints.Count - 1; i++) @@ -2328,6 +2341,9 @@ namespace NavisworksTransport case RenderStyleName.RailBaseline: return new RenderStyle(Color.FromByteRGB(255, 138, 128), 0.7); // 浅红色,30%透明 + case RenderStyleName.AssemblyGuideLine: + return new RenderStyle(Color.FromByteRGB(76, 175, 80), 0.85); // Material Green装配基准向量 + case RenderStyleName.HoistingLine: return new RenderStyle(Color.FromByteRGB(156, 39, 176), 0.8); // Material Purple吊装路径,20%透明 diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index db79b0b..37eb74a 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -121,7 +121,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels private ObservableCollection> _railMountModeOptions; private ObservableCollection> _railPayloadAnchorModeOptions; private const double DefaultAssemblyReferenceRodLengthInMeters = 20.0; - private const double DefaultAssemblyReferenceRodDiameterInMeters = 0.2; + private static readonly double DefaultAssemblyReferenceRodDiameterInMeters = CalculateDefaultAssemblyReferenceRodDiameterInMeters(); private string _assemblyTerminalObjectName = "未选择"; private string _assemblyTerminalObjectInfo = "请选择终点处已安装箱体"; private double _assemblyReferenceRodLengthInMeters = DefaultAssemblyReferenceRodLengthInMeters; @@ -134,6 +134,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels private ModelItem _assemblyTerminalObject; private Point3D _assemblyStartPoint; private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; + private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; + private const string AssemblyReferenceLinePathId = "assembly_reference_line"; // 自动路径起点和终点的路径对象引用(用于正确的ID管理) private PathRoute _autoPathStartPointRoute = null; @@ -1320,27 +1322,29 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("终点箱体未设置或已失效,请重新捕获终点箱体"); } - var forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform); - var endPoint = GetAssemblyTerminalAnchorPoint(); - double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); - var startPoint = new Point3D( - endPoint.X - forward.X * rodLength, - endPoint.Y - forward.Y * rodLength, - endPoint.Z - forward.Z * rodLength); + AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine(); AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( - startPoint, - endPoint, + referenceLine.StartPoint, + referenceLine.EndPoint, AssemblyReferenceRodDiameterInMeters); _assemblyStartPoint = Point3D.Origin; AssemblyStartPointText = "未选择"; - RefreshAssemblyTerminalObjectInfo(startPoint, endPoint); + RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint); RenderAssemblyAnchorMarker(); + RenderAssemblyCenterGuideLine(); + RenderAssemblyReferenceLine(referenceLine); + FocusOnAssemblyReferenceArea(referenceLine.StartPoint, referenceLine.EndPoint); - UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认位置"); + UpdateMainStatus("已生成直线装配参考杆,请在三维视图中确认终点锚点、方向、外端位置和杆体贴合情况"); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); - LogManager.Info($"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}"); + LogManager.Info( + $"[直线装配] 已生成参考杆: {AssemblyTerminalObjectName}, " + + $"终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " + + $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2}), " + + $"方向=({referenceLine.Direction.X:F3}, {referenceLine.Direction.Y:F3}, {referenceLine.Direction.Z:F3}), " + + $"资源={AssemblyReferencePathManager.Instance.ReferenceResourceName}"); }, "生成装配参考杆"); } @@ -1391,14 +1395,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels CleanupAssemblyReferenceSelection(); AssemblyReferencePathManager.Instance.HideReferenceRod(); ClearAssemblyAnchorMarker(); + ClearAssemblyCenterGuideLine(); + ClearAssemblyReferenceLine(); AssemblyStartPointText = "未选择"; OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); - UpdateMainStatus("已隐藏直线装配参考杆"); - LogManager.Info("[直线装配] 已隐藏参考杆"); + UpdateMainStatus("已隐藏直线装配参考线"); + LogManager.Info("[直线装配] 已隐藏参考线"); } catch (Exception ex) { - LogManager.Error($"[直线装配] 隐藏参考杆失败: {ex.Message}"); + LogManager.Error($"[直线装配] 隐藏参考线失败: {ex.Message}"); } } @@ -1494,6 +1500,39 @@ namespace NavisworksTransport.UI.WPF.ViewModels center.Z + up.Z * halfHeight * direction); } + private AssemblyReferenceLine BuildAssemblyReferenceLine() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,无法生成装配参考线"); + } + + BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox(); + Point3D centerPoint = bounds.Center; + Point3D endPoint = GetAssemblyTerminalAnchorPoint(); + Vector3D direction = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z); + double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z; + if (directionLengthSquared < 1e-9) + { + throw new InvalidOperationException("箱体中心与世界原点重合,无法生成装配参考线方向"); + } + + direction = direction.Normalize(); + double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); + Point3D startPoint = new Point3D( + endPoint.X + direction.X * rodLength, + endPoint.Y + direction.Y * rodLength, + endPoint.Z + direction.Z * rodLength); + + LogManager.Info( + $"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " + + $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " + + $"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " + + $"原点到箱体中心方向=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})"); + + return new AssemblyReferenceLine(startPoint, endPoint, direction); + } + private void RefreshAssemblyTerminalObjectInfo(Point3D referenceStartPoint = null, Point3D referenceEndPoint = null) { if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) @@ -1551,21 +1590,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels try { - Point3D endPoint = GetAssemblyTerminalAnchorPoint(); - Vector3D forward = ModelItemTransformHelper.GetForwardDirectionFromTransform(_assemblyTerminalObject.Transform); - double rodLength = UnitsConverter.ConvertFromMeters(AssemblyReferenceRodLengthInMeters); - Point3D startPoint = new Point3D( - endPoint.X - forward.X * rodLength, - endPoint.Y - forward.Y * rodLength, - endPoint.Z - forward.Z * rodLength); + AssemblyReferenceLine referenceLine = BuildAssemblyReferenceLine(); AssemblyReferencePathManager.Instance.CreateOrUpdateReferenceRod( - startPoint, - endPoint, + referenceLine.StartPoint, + referenceLine.EndPoint, AssemblyReferenceRodDiameterInMeters); - RefreshAssemblyTerminalObjectInfo(startPoint, endPoint); + RefreshAssemblyTerminalObjectInfo(referenceLine.StartPoint, referenceLine.EndPoint); RenderAssemblyAnchorMarker(); + RenderAssemblyCenterGuideLine(); + RenderAssemblyReferenceLine(referenceLine); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); } catch (Exception ex) @@ -1574,6 +1609,82 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private sealed class AssemblyReferenceLine + { + public AssemblyReferenceLine(Point3D startPoint, Point3D endPoint, Vector3D direction) + { + StartPoint = startPoint; + EndPoint = endPoint; + Direction = direction; + } + + public Point3D StartPoint { get; } + public Point3D EndPoint { get; } + public Vector3D Direction { get; } + } + + private static double CalculateDefaultAssemblyReferenceRodDiameterInMeters() + { + double standardRadiusInMeters = ConfigManager.Instance.Current.PathEditing.CellSizeMeters; + standardRadiusInMeters = Math.Max(0.1, Math.Min(0.5, standardRadiusInMeters)); + return standardRadiusInMeters * 0.8; + } + + private void FocusOnAssemblyReferenceArea(Point3D startPoint, Point3D endPoint) + { + try + { + double minX = Math.Min(startPoint.X, endPoint.X); + double minY = Math.Min(startPoint.Y, endPoint.Y); + double minZ = Math.Min(startPoint.Z, endPoint.Z); + double maxX = Math.Max(startPoint.X, endPoint.X); + double maxY = Math.Max(startPoint.Y, endPoint.Y); + double maxZ = Math.Max(startPoint.Z, endPoint.Z); + + if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + var terminalBounds = _assemblyTerminalObject.BoundingBox(); + minX = Math.Min(minX, terminalBounds.Min.X); + minY = Math.Min(minY, terminalBounds.Min.Y); + minZ = Math.Min(minZ, terminalBounds.Min.Z); + maxX = Math.Max(maxX, terminalBounds.Max.X); + maxY = Math.Max(maxY, terminalBounds.Max.Y); + maxZ = Math.Max(maxZ, terminalBounds.Max.Z); + } + + Point3D center = new Point3D( + (minX + maxX) / 2.0, + (minY + maxY) / 2.0, + (minZ + maxZ) / 2.0); + + double targetSize = Math.Max( + maxX - minX, + Math.Max(maxY - minY, maxZ - minZ)); + + targetSize = Math.Max(targetSize, UnitsConverter.ConvertFromMeters(1.0)); + + ViewpointHelper.FocusOnPosition(center, targetSize, 45.0, 0.35); + + var selection = new List(); + if (_assemblyTerminalObject != null && ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + selection.Add(_assemblyTerminalObject); + } + + var referenceRod = AssemblyReferencePathManager.Instance.CurrentReferenceRod; + if (referenceRod != null) + { + selection.Add(referenceRod); + } + + NavisworksSelectionHelper.SetModelSelection(selection); + } + catch (Exception ex) + { + LogManager.Warning($"[直线装配] 聚焦参考杆区域失败: {ex.Message}"); + } + } + private void RenderAssemblyAnchorMarker() { if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) @@ -1588,6 +1699,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels } Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); + LogManager.Info( + $"[直线装配] 已渲染终点锚点标记: ({anchorPoint.X:F2}, {anchorPoint.Y:F2}, {anchorPoint.Z:F2})"); var markerRoute = new PathRoute("装配对接点") { Id = AssemblyAnchorMarkerPathId, @@ -1597,6 +1710,47 @@ namespace NavisworksTransport.UI.WPF.ViewModels renderPlugin.RenderPointOnly(markerRoute); } + private void RenderAssemblyReferenceLine(AssemblyReferenceLine referenceLine) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null || referenceLine == null) + { + return; + } + + renderPlugin.RenderRailBaseline( + AssemblyReferenceLinePathId, + new List { referenceLine.EndPoint, referenceLine.StartPoint }); + + LogManager.Info( + $"[直线装配] 已渲染参考线: 终点锚点=({referenceLine.EndPoint.X:F2}, {referenceLine.EndPoint.Y:F2}, {referenceLine.EndPoint.Z:F2}), " + + $"参考线外端=({referenceLine.StartPoint.X:F2}, {referenceLine.StartPoint.Y:F2}, {referenceLine.StartPoint.Z:F2})"); + } + + private void RenderAssemblyCenterGuideLine() + { + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + return; + } + + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center; + renderPlugin.RenderRailBaseline( + AssemblyCenterGuideLinePathId, + new List { Point3D.Origin, centerPoint }, + RenderStyleName.AssemblyGuideLine); + + LogManager.Info( + $"[直线装配] 已渲染原点到箱体中心基准线: 原点=(0.00, 0.00, 0.00), " + + $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})"); + } + private void ClearAssemblyAnchorMarker() { var renderPlugin = PathPointRenderPlugin.Instance; @@ -1608,6 +1762,28 @@ namespace NavisworksTransport.UI.WPF.ViewModels renderPlugin.RemovePath(AssemblyAnchorMarkerPathId); } + private void ClearAssemblyReferenceLine() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.ClearRailBaseline(AssemblyReferenceLinePathId); + } + + private void ClearAssemblyCenterGuideLine() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId); + } + private void CleanupAssemblyReferenceSelection() { try @@ -5071,6 +5247,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Warning($"清理直线装配对接点标记时发生异常: {ex.Message}"); } + try + { + ClearAssemblyReferenceLine(); + } + catch (Exception ex) + { + LogManager.Warning($"清理直线装配参考线时发生异常: {ex.Message}"); + } + // 确保停止任何活动的点击工具 try { diff --git a/src/Utils/ModelItemTransformHelper.cs b/src/Utils/ModelItemTransformHelper.cs index 35af3a2..f878236 100644 --- a/src/Utils/ModelItemTransformHelper.cs +++ b/src/Utils/ModelItemTransformHelper.cs @@ -152,6 +152,50 @@ namespace NavisworksTransport.Utils } } + /// + /// 根据箱体局部尺寸和世界原点方向,推导“背离球心”的真实长轴方向。 + /// 先选局部最长轴,再用箱体中心相对世界原点的方向决定正负。 + /// + public static Vector3D GetLongestAxisDirectionAwayFromOrigin(BoundingBox3D bounds, Transform3D transform) + { + Vector3D localSize = EstimateLocalBoxSize(bounds, transform); + + int longestAxisIndex = 0; + double longestAxisLength = localSize.X; + if (localSize.Y > longestAxisLength) + { + longestAxisIndex = 1; + longestAxisLength = localSize.Y; + } + + if (localSize.Z > longestAxisLength) + { + longestAxisIndex = 2; + } + + Vector3D axisDirection = GetAxisDirectionFromTransform( + transform, + longestAxisIndex, + longestAxisIndex == 0 ? new Vector3D(1, 0, 0) : + longestAxisIndex == 1 ? new Vector3D(0, 1, 0) : + new Vector3D(0, 0, 1)); + + Vector3D awayFromOrigin = new Vector3D(bounds.Center.X, bounds.Center.Y, bounds.Center.Z); + double awayLengthSquared = awayFromOrigin.X * awayFromOrigin.X + awayFromOrigin.Y * awayFromOrigin.Y + awayFromOrigin.Z * awayFromOrigin.Z; + if (awayLengthSquared < 1e-9) + { + return axisDirection; + } + + double dot = axisDirection.X * awayFromOrigin.X + axisDirection.Y * awayFromOrigin.Y + axisDirection.Z * awayFromOrigin.Z; + if (dot < 0.0) + { + return new Vector3D(-axisDirection.X, -axisDirection.Y, -axisDirection.Z); + } + + return axisDirection; + } + private static Vector3D GetAxisDirectionFromTransform(Transform3D transform, int axisIndex, Vector3D fallback) { if (transform == null) @@ -317,6 +361,168 @@ namespace NavisworksTransport.Utils ApplyAbsoluteTransform(item, targetPosition, targetRotation, preserveCurrentScale: true); } + /// + /// 将物体移动到指定中心点和完整三维朝向,同时保留当前缩放。 + /// 适用于参考杆、辅助几何等以几何中心作为定位基准的场景。 + /// + public static void MoveItemToCenterAndRotationWithCurrentScale(ModelItem item, Point3D targetCenter, Rotation3D targetRotation) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + + var currentComponents = item.Transform.Factor(); + Vector3D currentScale = currentComponents.Scale; + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = item.BoundingBox(); + Point3D originalCenter = originalBounds.Center; + + var rotationTransform = new Transform3D(targetRotation); + var linear = rotationTransform.Linear; + + var rotatedCenter = new Point3D( + linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, + linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, + linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); + + var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); + var components = identity.Factor(); + components.Scale = currentScale; + components.Rotation = targetRotation; + components.Translation = new Vector3D( + targetCenter.X - rotatedCenter.X, + targetCenter.Y - rotatedCenter.Y, + targetCenter.Z - rotatedCenter.Z); + + doc.Models.OverridePermanentTransform(modelItems, components.Combine(), false); + } + + /// + /// 将物体移动到指定中心点和完整三维朝向。 + /// 仅应用位置和旋转,不保留当前缩放。 + /// 适用于缩放已经在 Model 层完成的参考杆等场景。 + /// + public static void MoveItemToCenterAndRotation(ModelItem item, Point3D targetCenter, Rotation3D targetRotation) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = item.BoundingBox(); + Point3D originalCenter = originalBounds.Center; + + var rotationTransform = new Transform3D(targetRotation); + var linear = rotationTransform.Linear; + + var rotatedCenter = new Point3D( + linear.Get(0, 0) * originalCenter.X + linear.Get(0, 1) * originalCenter.Y + linear.Get(0, 2) * originalCenter.Z, + linear.Get(1, 0) * originalCenter.X + linear.Get(1, 1) * originalCenter.Y + linear.Get(1, 2) * originalCenter.Z, + linear.Get(2, 0) * originalCenter.X + linear.Get(2, 1) * originalCenter.Y + linear.Get(2, 2) * originalCenter.Z); + + var translation = new Vector3D( + targetCenter.X - rotatedCenter.X, + targetCenter.Y - rotatedCenter.Y, + targetCenter.Z - rotatedCenter.Z); + + doc.Models.OverridePermanentTransform(modelItems, new Transform3D(targetRotation, translation), false); + } + + /// + /// 将物体的局部正 X 端面中心对齐到目标点,并保留当前缩放。 + /// 适用于单位立方体/单位圆柱体沿 +X 定义长度方向的参考杆资源。 + /// + public static void MoveItemPositiveXEndToPointAndRotationWithCurrentScale(ModelItem item, Point3D targetPoint, Rotation3D targetRotation) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + + var currentComponents = item.Transform.Factor(); + Vector3D currentScale = currentComponents.Scale; + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = item.BoundingBox(); + Point3D originalPositiveXEndCenter = new Point3D( + originalBounds.Max.X, + originalBounds.Center.Y, + originalBounds.Center.Z); + + var rotationTransform = new Transform3D(targetRotation); + var linear = rotationTransform.Linear; + + var rotatedPositiveXEndCenter = new Point3D( + linear.Get(0, 0) * originalPositiveXEndCenter.X + linear.Get(0, 1) * originalPositiveXEndCenter.Y + linear.Get(0, 2) * originalPositiveXEndCenter.Z, + linear.Get(1, 0) * originalPositiveXEndCenter.X + linear.Get(1, 1) * originalPositiveXEndCenter.Y + linear.Get(1, 2) * originalPositiveXEndCenter.Z, + linear.Get(2, 0) * originalPositiveXEndCenter.X + linear.Get(2, 1) * originalPositiveXEndCenter.Y + linear.Get(2, 2) * originalPositiveXEndCenter.Z); + + var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0)); + var components = identity.Factor(); + components.Scale = currentScale; + components.Rotation = targetRotation; + components.Translation = new Vector3D( + targetPoint.X - rotatedPositiveXEndCenter.X, + targetPoint.Y - rotatedPositiveXEndCenter.Y, + targetPoint.Z - rotatedPositiveXEndCenter.Z); + + doc.Models.OverridePermanentTransform(modelItems, components.Combine(), false); + } + + /// + /// 将物体的局部正 X 端面中心对齐到目标点。 + /// 仅应用位置和旋转,不保留当前缩放。 + /// 适用于缩放已经在 Model 层完成、Item 层只负责定位的场景。 + /// + public static void MoveItemPositiveXEndToPointAndRotation(ModelItem item, Point3D targetPoint, Rotation3D targetRotation) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var doc = Application.ActiveDocument; + var modelItems = new ModelItemCollection { item }; + + doc.Models.ResetPermanentTransform(modelItems); + + var originalBounds = item.BoundingBox(); + Point3D originalPositiveXEndCenter = new Point3D( + originalBounds.Max.X, + originalBounds.Center.Y, + originalBounds.Center.Z); + + var rotationTransform = new Transform3D(targetRotation); + var linear = rotationTransform.Linear; + + var rotatedPositiveXEndCenter = new Point3D( + linear.Get(0, 0) * originalPositiveXEndCenter.X + linear.Get(0, 1) * originalPositiveXEndCenter.Y + linear.Get(0, 2) * originalPositiveXEndCenter.Z, + linear.Get(1, 0) * originalPositiveXEndCenter.X + linear.Get(1, 1) * originalPositiveXEndCenter.Y + linear.Get(1, 2) * originalPositiveXEndCenter.Z, + linear.Get(2, 0) * originalPositiveXEndCenter.X + linear.Get(2, 1) * originalPositiveXEndCenter.Y + linear.Get(2, 2) * originalPositiveXEndCenter.Z); + + var translation = new Vector3D( + targetPoint.X - rotatedPositiveXEndCenter.X, + targetPoint.Y - rotatedPositiveXEndCenter.Y, + targetPoint.Z - rotatedPositiveXEndCenter.Z); + + doc.Models.OverridePermanentTransform(modelItems, new Transform3D(targetRotation, translation), false); + } + private static void ApplyAbsoluteTransform(ModelItem item, Point3D targetPosition, Rotation3D targetRotation, bool preserveCurrentScale) { var doc = Application.ActiveDocument;