Unify real-object rail pose interpretation

This commit is contained in:
tian 2026-03-25 00:11:07 +08:00
parent 6c74ea1319
commit 138eb43a67
12 changed files with 901 additions and 34 deletions

View File

@ -172,6 +172,20 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 可以视为直接生活在宿主坐标系里
- 角度调整对话框里的 `X/Y/Z`,对真实物体应按**宿主坐标系**消费
#### 真实物体参考姿态
- 真实物体不要再直接依赖 `ModelItem.Transform.Factor().Rotation`
- 对很多 Revit 导入件,这个值是单位旋转,不代表真实显示姿态
- 真实物体参考姿态应优先来自 fragment 代表姿态
- `Fragment默认Up` 的用途是:
- 把 fragment 参考框架解释成当前宿主坐标语义下的真实姿态
- 不是仅仅挑一个“竖直候选轴”
- 在真实物体链路里必须先完成“fragment 姿态解释”,再谈:
- 前进方向
- 路径对齐
- 角度调整
- `_trackedRotation` 对真实物体必须优先跟踪“解释后的真实参考姿态”,不能回退成 `Transform` 的单位旋转
#### 虚拟物体
- 有明确资产坐标系
@ -194,6 +208,15 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 不能像平面路径那样在宿主空间随意补旋转
- 必须并入 `canonical -> rail pose`
- `Rail 0°` 基线必须稳定,不能被角度修正逻辑污染
- `Rail` 真实物体不应再退回默认 `PositiveX / PositiveY`
- 三类路径都必须先复用同一个“对象姿态解释层”:
- `Ground / Hoisting`
- `forward = 路径方向`
- `up = 宿主 up`
- `Rail`
- `forward = rail 切向`
- `up = rail 法向 / preferred normal`
- 统一的是“对象参考姿态来源与解释方式”,不是把三类路径都强行改成同一个 `up`
### 4.4 通行空间与物体姿态的关系
@ -209,6 +232,10 @@ var rotation = new Rotation3D(qw, qx, qy, qz); // 错误
- 是否一条链用了“原始高度”
- 另一条链用了“旋转后法线尺寸”
- `Rail` 真实物体尤其要检查:
- 起点/逐帧中心偏移是否仍在用原始 `objectHeight`
- 通行空间是否仍在用旧 `RailAssetConvention`
- 通行空间、路径偏移、最终姿态是否共享同一套“最终姿态尺寸语义”
### 4.5 终端安装仿真

View File

@ -64,6 +64,8 @@
<Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectPlanarPoseSolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailAxisConventionResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\RealObjectRailExtentResolverTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyInstallationReferenceBuilderTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />

View File

@ -343,6 +343,8 @@
<Compile Include="src\Utils\CoordinateSystem\FragmentDefaultUpContext.cs" />
<Compile Include="src\Utils\CoordinateSystem\FragmentRepresentativePoseHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectPlanarPoseSolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectRailAxisConventionResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\RealObjectRailExtentResolver.cs" />
<Compile Include="src\Utils\CoordinateSystem\ObjectSpaceOrientationHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\RotatedObjectExtentHelper.cs" />
<Compile Include="src\Utils\CoordinateSystem\HostCoordinateAdapter.cs" />

View File

@ -305,6 +305,63 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostXAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(90.0, 0.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostYAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 90.0, 0.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
[TestMethod]
public void YUp_ComposeHostQuaternion_ForRailBaseline_ShouldRotateAxesAroundHostZAxis()
{
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Vector3 baselineX = new Vector3(-0.9900f, 0.1403f, -0.0163f);
Vector3 baselineY = new Vector3(0.1403f, 0.9901f, 0.0000f);
Vector3 baselineZ = new Vector3(0.0161f, -0.0023f, -0.9999f);
Quaternion baseline = CreateQuaternionFromAxes(baselineX, baselineY, baselineZ);
var correction = new LocalEulerRotationCorrection(0.0, 0.0, 90.0);
Quaternion composed = adapter.ComposeHostQuaternion(baseline, correction);
Quaternion hostCorrection = adapter.CreateHostRotationCorrection(correction);
Matrix4x4 linear = Matrix4x4.CreateFromQuaternion(composed);
AssertAxis(linear, 1, Vector3.Transform(baselineX, hostCorrection));
AssertAxis(linear, 2, Vector3.Transform(baselineY, hostCorrection));
AssertAxis(linear, 3, Vector3.Transform(baselineZ, hostCorrection));
}
private static void AssertPoint(Vector3 point, double x, double y, double z)
{
Assert.AreEqual(x, point.X, 1e-9);

View File

@ -0,0 +1,89 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectRailAxisConventionResolverTests
{
[TestMethod]
public void YUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveYForRail()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.YUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out Vector3 selectedForwardWorldAxis);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
AssertVector(selectedForwardWorldAxis, -1.0, 0.0, 0.0);
}
[TestMethod]
public void ZUp_InterpretedReferencePose_ShouldChoosePositiveXAndPositiveZForRail()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.9f, 0.3f, 0.0f));
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.ZUp,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out _);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, selectedForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveZ, convention.UpAxis);
}
[TestMethod]
public void YUp_ShouldNotSelectYAxisFamilyAsForwardCandidate()
{
Vector3 referenceAxisX = new Vector3(1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, 1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(0.0f, 1.0f, 0.01f));
bool ok = RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.YUp,
out _,
out LocalAxisDirection selectedForwardAxis,
out _);
Assert.IsTrue(ok);
Assert.AreNotEqual(LocalAxisDirection.PositiveY, selectedForwardAxis);
Assert.AreNotEqual(LocalAxisDirection.NegativeY, selectedForwardAxis);
}
private static void AssertVector(Vector3 actual, double x, double y, double z, double tolerance = 1e-6)
{
Assert.AreEqual(x, actual.X, tolerance);
Assert.AreEqual(y, actual.Y, tolerance);
Assert.AreEqual(z, actual.Z, tolerance);
}
}
}

View File

@ -0,0 +1,142 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NavisworksTransport.Utils.CoordinateSystem;
using System;
using System.Numerics;
namespace NavisworksTransport.UnitTests.CoordinateSystem
{
[TestClass]
public class RealObjectRailExtentResolverTests
{
[TestMethod]
public void YUp_ZeroCorrection_ShouldKeepResolvedRailUpExtent()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Identity;
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 2.0,
upSize: 4.0,
baseline,
baseline,
out ModelAxisConvention convention,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
Assert.AreEqual(6.0, extents.forwardExtent, 1e-6);
Assert.AreEqual(2.0, extents.sideExtent, 1e-6);
Assert.AreEqual(4.0, extents.upExtent, 1e-6);
}
[TestMethod]
public void YUp_HostY90_ShouldKeepResolvedRailUpExtentAndSwapForwardSide()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Identity;
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion final = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 90.0, 0.0));
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 2.0,
upSize: 4.0,
baseline,
final,
out _,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(2.0, extents.forwardExtent, 1e-6);
Assert.AreEqual(6.0, extents.sideExtent, 1e-6);
Assert.AreEqual(4.0, extents.upExtent, 1e-6);
}
[TestMethod]
public void YUp_DualAxisCorrection_WithRailBaseline_ShouldProjectAgainstFinalRailPose()
{
Vector3 referenceAxisX = new Vector3(-1.0f, 0.0f, 0.0f);
Vector3 referenceAxisY = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 referenceAxisZ = new Vector3(0.0f, 0.0f, -1.0f);
Vector3 desiredForward = Vector3.Normalize(new Vector3(-0.8987f, 0.0f, 0.4386f));
Quaternion baseline = Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
0.9900f, -0.1403f, -0.0161f, 0f,
0.1403f, 0.9901f, -0.0023f, 0f,
0.0163f, 0.0000f, 0.9999f, 0f,
0f, 0f, 0f, 1f)));
var adapter = new HostCoordinateAdapter(CoordinateSystemType.YUp);
Quaternion final = adapter.ComposeHostQuaternion(
baseline,
new LocalEulerRotationCorrection(0.0, 90.0, 90.0));
bool ok = RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
CoordinateSystemType.YUp,
forwardSize: 6.0,
sideSize: 2.0,
upSize: 4.0,
baseline,
final,
out ModelAxisConvention convention,
out var extents);
Assert.IsTrue(ok);
Assert.AreEqual(LocalAxisDirection.PositiveX, convention.ForwardAxis);
Assert.AreEqual(LocalAxisDirection.PositiveY, convention.UpAxis);
Vector3 localSize = convention.CreateScaleVector3(6.0, 2.0, 4.0);
Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, final));
Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, final));
Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, final));
Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baseline));
Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baseline));
Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
double expectedForward = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward);
double expectedSide = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide);
double expectedUp = ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp);
Assert.AreEqual(expectedForward, extents.forwardExtent, 1e-5);
Assert.AreEqual(expectedSide, extents.sideExtent, 1e-5);
Assert.AreEqual(expectedUp, extents.upExtent, 1e-5);
Assert.AreNotEqual(4.0, extents.upExtent, 1e-3, "双轴旋转后,法向尺寸不应仍停留在单轴结果。");
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
Vector3 rotatedLocalY,
Vector3 rotatedLocalZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
}
}
}

View File

@ -1,6 +1,6 @@
# 当前工程状态
更新时间2026-03-23
更新时间2026-03-25
## 1. 当前稳定状态
@ -46,6 +46,24 @@
## 4. 当前必须记住的根因与规则
### 4.0 真实物体参考姿态与 Fragment默认Up
- 真实物体不能再默认使用 `ModelItem.Transform` 或包围盒正交框架当“原始姿态”。
- 当前稳定做法是:
- 先从 fragment 统计得到代表姿态
- 再用当前文档级 `Fragment默认Up` 解释这组 fragment 参考轴
- 最终得到当前宿主语义下的真实参考姿态
- `Fragment默认Up` 的真正用途不是“找一个竖直候选轴”,而是:
- 解释 fragment 参考姿态的原始 `Y/Z` 语义
- 把 fragment 参考姿态转换成当前宿主坐标语义下可用的真实姿态
- 当前文档如果 `Fragment默认Up` 设错,会直接导致:
- 真实物体起点姿态错误
- Ground / Hoisting / Rail 的参考姿态解释错误
- 后续角度修正、通行空间、路径贴合全部建立在错误姿态上
- 当前规则:
- 先解释真实物体参考姿态,再做路径对齐
- 不能跳过这一步,直接拿 fragment 世界轴去猜业务姿态
### 4.1 Rotation / 矩阵语义
- `Rotation3D(a, b, c, d)` 的参数顺序是四元数 `x, y, z, w`
@ -162,6 +180,37 @@
2. 再验证修正后 `forward` 是否仍沿 rail 切向
3. 最后再看 Navisworks 应用层是否正确按三步增量姿态落位
### 4.8 Rail 真实物体必须复用统一姿态解释层
- `Rail` 不应再单独退回默认 `PositiveX / PositiveY` 轴约定。
- 当前稳定原则:
- `Ground / Hoisting / Rail` 三类路径的真实物体,必须先共用同一个“真实参考姿态解释层”
- 路径类型的差异只体现在目标路径框架:
- `Ground / Hoisting``up = 宿主 up`
- `Rail``up = rail 法向 / preferred normal`
- 当前 `Rail` 真实物体稳定链路:
1. fragment 代表姿态
2. `Fragment默认Up` 解释后的真实参考姿态
3. `Rail` 路径框架(切向 / 法向)
4. 宿主 `X/Y/Z` 角度修正
- 不要再让 `Rail` 真实物体单独维护一套与 `Ground / Hoisting` 不同的对象姿态来源。
### 4.9 Rail 真实物体的通行空间与路径偏移
- `Rail` 真实物体的通行空间、起点中心偏移、逐帧法向偏移,必须共享“最终姿态”的同一套尺寸语义。
- 当前稳定规则:
- 不能只根据“宿主轴角度修正”单独投影尺寸
- 必须同时吃到:
- `Rail` 基姿态
- 宿主 `X/Y/Z` 角度修正后的最终姿态
- 否则会出现典型错误:
- 物体本体姿态正确,但起点偏离路径
- 单轴旋转时通行空间看起来对,双轴旋转后只跟上一个角度
- `Rail` 真实物体通行空间与物体本体方向不一致
- 当前消费规则:
- `AnimationControlViewModel.UpdatePassageSpaceVisualization()` 中,真实物体的 `Rail``Hoisting` 必须分开消费
- `Rail` 真实物体不能再复用 `Hoisting` 的法向/分段参数语义
## 5. 当前保留的日志策略
- 保留:

View File

@ -707,19 +707,16 @@ namespace NavisworksTransport.Core.Animation
}
else if (_route.PathType == PathType.Rail)
{
double objectHeight = GetAnimatedObjectHeight();
Point3D previousPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
double objectHeight = GetAnimatedObjectRailNormalExtent(previousPoint, _pathPoints[0], nextPoint);
startPosition = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(_route, startPosition, previousPoint, nextPoint, objectHeight);
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,
if (TryCreateRailPathRotation(
previousPoint,
_pathPoints[0],
nextPoint,
GetCurrentRailModelAxisConvention(),
_objectRotationCorrection,
out var railRotation))
{
var railLinearTransform = new Transform3D(railRotation).Linear;
@ -964,7 +961,7 @@ namespace NavisworksTransport.Core.Animation
// 🔥 空中路径:根据路径类型和线段索引调整物体位置
double objectHeight = _route.PathType == PathType.Rail
? GetAnimatedObjectHeight()
? GetAnimatedObjectRailNormalExtent(p1, framePosition, p2)
: GetAnimatedObjectGroundContactHeight();
if (_route.PathType == PathType.Hoisting)
@ -1079,13 +1076,10 @@ namespace NavisworksTransport.Core.Animation
};
if (_route.PathType == PathType.Rail &&
RailPathPoseHelper.TryCreateRailRotation(
_route,
TryCreateRailPathRotation(
previousFramePoint,
framePosition,
nextFramePoint,
GetCurrentRailModelAxisConvention(),
_objectRotationCorrection,
out var railRotation))
{
frame.Rotation = railRotation;
@ -1941,7 +1935,7 @@ namespace NavisworksTransport.Core.Animation
Point3D expectedTrackedEndPoint;
if (_route.PathType == PathType.Rail)
{
double objectHeight = GetAnimatedObjectHeight();
double objectHeight = GetAnimatedObjectRailNormalExtent(previousAnchorPoint, terminalAnchorPoint, terminalAnchorPoint);
expectedTrackedEndPoint = RailPathPoseHelper.ResolveObjectSpaceCenterPosition(
_route,
terminalAnchorPoint,
@ -3510,19 +3504,227 @@ namespace NavisworksTransport.Core.Animation
return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
private ModelAxisConvention GetCurrentRailModelAxisConvention()
private bool TryGetCurrentRailModelAxisConvention(
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
out ModelAxisConvention convention)
{
convention = null;
if (IsVirtualObjectMode)
{
return ModelAxisConvention.CreateVirtualObjectAssetConvention();
convention = ModelAxisConvention.CreateVirtualObjectAssetConvention();
return true;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
var convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
if (IsRealObjectMode && _hasRealObjectReferenceRotation)
{
Vector3 desiredForward = new Vector3(
(float)(nextPoint.X - previousPoint.X),
(float)(nextPoint.Y - previousPoint.Y),
(float)(nextPoint.Z - previousPoint.Z));
if (desiredForward.LengthSquared() < 1e-6f)
{
desiredForward = new Vector3(
(float)(nextPoint.X - currentPoint.X),
(float)(nextPoint.Y - currentPoint.Y),
(float)(nextPoint.Z - currentPoint.Z));
}
if (RealObjectRailAxisConventionResolver.TryResolve(
_realObjectReferenceAxisX,
_realObjectReferenceAxisY,
_realObjectReferenceAxisZ,
desiredForward,
adapter.HostType,
out convention,
out var selectedForwardAxis,
out var selectedForwardWorldAxis))
{
LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"真实物体参考姿态生效, Forward={convention.ForwardAxis}, Up={convention.UpAxis}, " +
$"选中Forward轴={selectedForwardAxis}, 选中Forward世界轴=({selectedForwardWorldAxis.X:F4},{selectedForwardWorldAxis.Y:F4},{selectedForwardWorldAxis.Z:F4})");
return true;
}
}
convention = ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
LogManager.Info(
$"[Rail姿态修正] Host={adapter.HostType}, 虚拟物体={IsVirtualObjectMode}, " +
$"Forward={convention.ForwardAxis}, Up={convention.UpAxis}");
return convention;
$"Forward={convention.ForwardAxis}, Up={convention.UpAxis}, 来源=默认轴约定");
return true;
}
private bool TryCalculateCurrentRealObjectRailProjectedExtents(
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
out ModelAxisConvention convention,
out (double forwardExtent, double sideExtent, double upExtent) extents)
{
convention = null;
extents = (0.0, 0.0, 0.0);
if (!IsRealObjectMode ||
!_hasRealObjectReferenceRotation ||
_realObjectLength <= 0.0 ||
_realObjectWidth <= 0.0 ||
_realObjectHeight <= 0.0)
{
return false;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Vector3 desiredForward = new Vector3(
(float)(nextPoint.X - previousPoint.X),
(float)(nextPoint.Y - previousPoint.Y),
(float)(nextPoint.Z - previousPoint.Z));
if (desiredForward.LengthSquared() < 1e-6f)
{
desiredForward = new Vector3(
(float)(nextPoint.X - currentPoint.X),
(float)(nextPoint.Y - currentPoint.Y),
(float)(nextPoint.Z - currentPoint.Z));
}
if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
{
return false;
}
if (!RailPathPoseHelper.TryCreateRailRotation(
_route,
previousPoint,
currentPoint,
nextPoint,
railConvention,
LocalEulerRotationCorrection.Zero,
out var baselineRotation))
{
return false;
}
Quaternion baselineHostQuaternion = new Quaternion(
(float)baselineRotation.A,
(float)baselineRotation.B,
(float)baselineRotation.C,
(float)baselineRotation.D);
Quaternion finalHostQuaternion = adapter.ComposeHostQuaternion(
baselineHostQuaternion,
_objectRotationCorrection);
return RealObjectRailExtentResolver.TryResolveProjectedSemanticExtents(
_realObjectReferenceAxisX,
_realObjectReferenceAxisY,
_realObjectReferenceAxisZ,
desiredForward,
adapter.HostType,
_realObjectLength,
_realObjectWidth,
_realObjectHeight,
baselineHostQuaternion,
finalHostQuaternion,
out convention,
out extents);
}
public bool TryGetCurrentRouteRealObjectRailProjectedExtents(
out double forwardExtent,
out double sideExtent,
out double upExtent)
{
forwardExtent = 0.0;
sideExtent = 0.0;
upExtent = 0.0;
if (_route == null || _route.PathType != PathType.Rail || _pathPoints == null || _pathPoints.Count == 0)
{
return false;
}
Point3D previousPoint = _pathPoints[0];
Point3D currentPoint = _pathPoints[0];
Point3D nextPoint = _pathPoints.Count > 1 ? _pathPoints[1] : _pathPoints[0];
if (!TryCalculateCurrentRealObjectRailProjectedExtents(
previousPoint,
currentPoint,
nextPoint,
out _,
out var extents))
{
return false;
}
forwardExtent = extents.forwardExtent;
sideExtent = extents.sideExtent;
upExtent = extents.upExtent;
return true;
}
private bool TryCreateRailPathRotation(
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint,
out Rotation3D rotation)
{
rotation = Rotation3D.Identity;
if (!TryGetCurrentRailModelAxisConvention(previousPoint, currentPoint, nextPoint, out var railConvention))
{
return false;
}
if (IsRealObjectMode)
{
if (!RailPathPoseHelper.TryCreateRailRotation(
_route,
previousPoint,
currentPoint,
nextPoint,
railConvention,
LocalEulerRotationCorrection.Zero,
out var baselineRotation))
{
return false;
}
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
Quaternion baselineHostQuaternion = new Quaternion(
(float)baselineRotation.A,
(float)baselineRotation.B,
(float)baselineRotation.C,
(float)baselineRotation.D);
Quaternion composedHostQuaternion = adapter.ComposeHostQuaternion(
baselineHostQuaternion,
_objectRotationCorrection);
rotation = adapter.FromHostQuaternion(composedHostQuaternion);
Matrix4x4 baselineLinear = Matrix4x4.CreateFromQuaternion(baselineHostQuaternion);
Matrix4x4 composedLinear = Matrix4x4.CreateFromQuaternion(composedHostQuaternion);
LogManager.Info(
$"[Rail真实物体角度修正] BaselineX=({baselineLinear.M11:F4},{baselineLinear.M21:F4},{baselineLinear.M31:F4}), " +
$"BaselineY=({baselineLinear.M12:F4},{baselineLinear.M22:F4},{baselineLinear.M32:F4}), " +
$"BaselineZ=({baselineLinear.M13:F4},{baselineLinear.M23:F4},{baselineLinear.M33:F4}), " +
$"HostComposeX=({composedLinear.M11:F4},{composedLinear.M21:F4},{composedLinear.M31:F4}), " +
$"HostComposeY=({composedLinear.M12:F4},{composedLinear.M22:F4},{composedLinear.M32:F4}), " +
$"HostComposeZ=({composedLinear.M13:F4},{composedLinear.M23:F4},{composedLinear.M33:F4})");
return true;
}
return RailPathPoseHelper.TryCreateRailRotation(
_route,
previousPoint,
currentPoint,
nextPoint,
railConvention,
_objectRotationCorrection,
out rotation);
}
private ModelAxisConvention GetCurrentModelAxisConvention()
@ -3627,6 +3829,27 @@ namespace NavisworksTransport.Core.Animation
return true;
}
private double GetAnimatedObjectRailNormalExtent(
Point3D previousPoint,
Point3D currentPoint,
Point3D nextPoint)
{
if (TryCalculateCurrentRealObjectRailProjectedExtents(
previousPoint,
currentPoint,
nextPoint,
out var convention,
out var extents))
{
LogManager.Debug(
$"[Rail法向尺寸] 真实物体有效尺寸: Forward={extents.forwardExtent:F3}, Side={extents.sideExtent:F3}, Up={extents.upExtent:F3}, " +
$"ForwardAxis={convention.ForwardAxis}, UpAxis={convention.UpAxis}, 角度={_objectRotationCorrection}");
return extents.upExtent;
}
return GetAnimatedObjectHeight();
}
private bool TryCreatePlanarPathRotationFromHostForward(Vector3D hostForward, out Rotation3D rotation)
{
rotation = Rotation3D.Identity;

View File

@ -1713,9 +1713,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathAnimationManager?.SetRealObjectDimensions(objectLength, objectWidth, objectHeight);
LogManager.Debug($"[选择物体] 保存原始尺寸: 长度={_objectOriginalLength:F2}m, 宽度={_objectOriginalWidth:F2}m, 高度={_objectOriginalHeight:F2}m");
// 3. 设置新物体会触发UpdatePassageSpaceVisualization
SelectedAnimatedObject = newObject;
// 3. 先注册到动画管理器,再设置 SelectedAnimatedObject。
// SelectedAnimatedObject 的 setter 会立即触发 MoveAnimatedObjectToPathStart()
// 如果先移动、后 SetAnimatedObject会把“已摆到起点后的当前姿态”再次当成参考姿态缓存。
_pathAnimationManager?.SetAnimatedObject(newObject);
SelectedAnimatedObject = newObject;
LogManager.Info($"已选择移动物体: {SelectedAnimatedObject.DisplayName}");
// 选择实体物体意味着切换到实体模式,避免后续起点同步仍走虚拟物体分支。
@ -2160,16 +2162,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info("已清除PathAnimationManager中的动画数据并归位物体");
}
// 2. 重置角度修正值为0
// 2. 先清空选择,避免角度修正归零时又触发“移动到起点”链路。
SelectedAnimatedObject = null;
// 3. 重置角度修正值为0
ObjectRotationCorrection = LocalEulerRotationCorrection.Zero;
LogManager.Debug("[清除物体] 已重置角度修正值为0");
// 3. 重置属性
SelectedAnimatedObject = null;
// 4. 重置属性
UpdateAnimatedObjectInfo();
UpdateCanGenerateAnimation();
// 4. 清理高亮
// 5. 清理高亮
ModelHighlightHelper.ClearCollisionHighlights();
LogManager.Info("移动物体已完全清除并归位");
@ -4339,6 +4343,20 @@ namespace NavisworksTransport.UI.WPF.ViewModels
upSize = _objectOriginalHeight * metersToUnits;
var adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
if (CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail &&
_pathAnimationManager != null &&
_pathAnimationManager.TryGetCurrentRouteRealObjectRailProjectedExtents(
out double resolvedForward,
out double resolvedSide,
out double resolvedUp))
{
double unitsToMetersForRail = UnitsConverter.GetUnitsToMetersConversionFactor();
LogManager.Debug(
$"[角度修正] Rail真实物体复用路径姿态尺寸: Forward={resolvedForward * unitsToMetersForRail:F2}m, " +
$"Side={resolvedSide * unitsToMetersForRail:F2}m, Up={resolvedUp * unitsToMetersForRail:F2}m, 角度={_objectRotationCorrection}");
return (resolvedForward, resolvedSide, resolvedUp);
}
axisConvention = CurrentPathRoute?.PathType == NavisworksTransport.PathType.Rail
? ModelAxisConvention.CreateRailAssetConvention()
: ModelAxisConvention.CreateDefaultForHost(adapter.HostType);
@ -4442,9 +4460,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
else if (SelectedAnimatedObject != null)
{
// 根据路径类型确定通行空间的尺寸
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail || CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Hoisting)
{
// 空中路径(空轨或吊装)
// 吊装路径
// 高度上下都加间隙
passageAcrossPath = effectiveWidth + 2 * safetyMargin; // 旋转后宽度 + 2*间隙(垂直于路径方向)
passageNormalToPath = effectiveHeight + 2 * safetyMargin; // 局部up方向高度 + 2*间隙(法线方向)
@ -4467,7 +4485,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 地面路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else // Rail
else if (CurrentPathRoute.PathType == NavisworksTransport.PathType.Rail)
{
// 空轨路径(可能有坡度):
// 物体的长度方向X轴前进方向朝向路径方向
@ -4482,6 +4500,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
passageNormalToPathHorizontal = passageNormalToPath;
LogManager.Debug($"[通行空间可视化] 空轨路径使用物体尺寸 ({SelectedAnimatedObject.DisplayName}): 有效长度={effectiveLength / metersToUnitsPassage:F2}m, 有效宽度={effectiveWidth / metersToUnitsPassage:F2}m, 有效高度={effectiveHeight / metersToUnitsPassage:F2}m, 通行空间沿路径={passageAlongPath / metersToUnitsPassage:F2}m, 垂直路径={passageAcrossPath / metersToUnitsPassage:F2}m, 法线={passageNormalToPath / metersToUnitsPassage:F2}m");
}
else
{
passageAcrossPath = effectiveWidth + 2 * safetyMargin;
passageNormalToPath = effectiveHeight + safetyMargin;
passageAlongPath = effectiveLength;
passageNormalToPathVertical = passageNormalToPath;
passageNormalToPathHorizontal = passageNormalToPath;
}
}
else
{

View File

@ -28,6 +28,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private readonly UIStateManager _uiStateManager;
private readonly HashSet<string> _documentRotationHintShown = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _documentRotationHintPending = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// 日志管理字段
private ObservableCollection<string> _logLevels;
@ -506,7 +507,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void ShowRootModelRotationHintIfNeeded()
{
string documentKey = GetCurrentDocumentKey();
if (string.IsNullOrWhiteSpace(documentKey) || _documentRotationHintShown.Contains(documentKey))
if (string.IsNullOrWhiteSpace(documentKey) ||
_documentRotationHintShown.Contains(documentKey) ||
_documentRotationHintPending.Contains(documentKey))
{
return;
}
@ -522,13 +525,51 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return;
}
_documentRotationHintShown.Add(documentKey);
LogManager.Info($"[模型整体旋转提示] {hintMessage}");
System.Windows.MessageBox.Show(
hintMessage,
"坐标系提示",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
_documentRotationHintPending.Add(documentKey);
_ = Task.Run(async () =>
{
try
{
await Task.Delay(1500).ConfigureAwait(false);
_uiStateManager?.QueueUIUpdate(() =>
{
try
{
string currentDocumentKey = GetCurrentDocumentKey();
if (!string.Equals(documentKey, currentDocumentKey, StringComparison.OrdinalIgnoreCase))
{
_documentRotationHintPending.Remove(documentKey);
return;
}
if (_documentRotationHintShown.Contains(documentKey))
{
_documentRotationHintPending.Remove(documentKey);
return;
}
_documentRotationHintShown.Add(documentKey);
_documentRotationHintPending.Remove(documentKey);
LogManager.Info($"[模型整体旋转提示] {hintMessage}");
System.Windows.MessageBox.Show(
hintMessage,
"坐标系提示",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
}
catch (Exception ex)
{
_documentRotationHintPending.Remove(documentKey);
LogManager.Warning($"[模型整体旋转提示] 延后弹出失败: {ex.Message}");
}
});
}
catch (Exception ex)
{
_documentRotationHintPending.Remove(documentKey);
LogManager.Warning($"[模型整体旋转提示] 延后任务失败: {ex.Message}");
}
});
}
private static bool TryGetRootModelRotationHint(Document doc, out string hintMessage)

View File

@ -0,0 +1,132 @@
using System;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 根据“已解释为当前宿主语义”的真实物体参考姿态,解析 Rail 路径应使用的局部轴约定。
///
/// 语义:
/// - 真实物体没有资产坐标系,但 fragment 参考姿态经过 Fragment默认Up 解释后,
/// 可以得到“当前宿主语义下”的真实参考三轴。
/// - Rail 与 Ground/Hoisting 的统一点在于:都先解释对象姿态,再进入路径框架。
/// - Rail 的特殊性只在路径框架forward 来自 rail 切向up 来自 rail 法向。
/// - 本解析器只负责回答:对这个真实物体来说,哪个局部 forward 轴最接近当前路径 forward
/// up 轴始终采用当前宿主语义对应的轴族YUp => +YZUp => +Z
/// </summary>
public static class RealObjectRailAxisConventionResolver
{
private const float AxisEpsilon = 1e-6f;
public static bool TryResolve(
Vector3 referenceAxisX,
Vector3 referenceAxisY,
Vector3 referenceAxisZ,
Vector3 desiredForward,
CoordinateSystemType hostType,
out ModelAxisConvention convention,
out LocalAxisDirection selectedForwardAxis,
out Vector3 selectedForwardWorldAxis)
{
convention = null;
selectedForwardAxis = LocalAxisDirection.PositiveX;
selectedForwardWorldAxis = Vector3.Zero;
if (!TryNormalize(desiredForward, out Vector3 normalizedForward))
{
return false;
}
if (!TryNormalize(referenceAxisX, out Vector3 axisX) ||
!TryNormalize(referenceAxisY, out Vector3 axisY) ||
!TryNormalize(referenceAxisZ, out Vector3 axisZ))
{
return false;
}
LocalAxisDirection semanticUpAxis =
hostType == CoordinateSystemType.YUp
? LocalAxisDirection.PositiveY
: LocalAxisDirection.PositiveZ;
Candidate best = Candidate.Invalid;
foreach (Candidate candidate in EnumerateForwardCandidates(axisX, axisY, axisZ, semanticUpAxis))
{
float score = Vector3.Dot(candidate.WorldAxis, normalizedForward);
if (best.IsInvalid || score > best.Score)
{
best = new Candidate(candidate.Axis, candidate.WorldAxis, score);
}
}
if (best.IsInvalid)
{
return false;
}
convention = new ModelAxisConvention(best.Axis, semanticUpAxis);
selectedForwardAxis = best.Axis;
selectedForwardWorldAxis = best.WorldAxis;
return true;
}
private static Candidate[] EnumerateForwardCandidates(
Vector3 axisX,
Vector3 axisY,
Vector3 axisZ,
LocalAxisDirection semanticUpAxis)
{
switch (semanticUpAxis)
{
case LocalAxisDirection.PositiveY:
return new[]
{
new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
new Candidate(LocalAxisDirection.PositiveZ, axisZ, 0f),
new Candidate(LocalAxisDirection.NegativeZ, -axisZ, 0f)
};
case LocalAxisDirection.PositiveZ:
default:
return new[]
{
new Candidate(LocalAxisDirection.PositiveX, axisX, 0f),
new Candidate(LocalAxisDirection.NegativeX, -axisX, 0f),
new Candidate(LocalAxisDirection.PositiveY, axisY, 0f),
new Candidate(LocalAxisDirection.NegativeY, -axisY, 0f)
};
}
}
private static bool TryNormalize(Vector3 value, out Vector3 normalized)
{
if (value.LengthSquared() < AxisEpsilon)
{
normalized = Vector3.Zero;
return false;
}
normalized = Vector3.Normalize(value);
return true;
}
private readonly struct Candidate
{
public static Candidate Invalid => new Candidate(LocalAxisDirection.PositiveX, Vector3.Zero, float.MinValue);
public LocalAxisDirection Axis { get; }
public Vector3 WorldAxis { get; }
public float Score { get; }
public bool IsInvalid => Score == float.MinValue;
public Candidate(LocalAxisDirection axis, Vector3 worldAxis, float score)
{
Axis = axis;
WorldAxis = worldAxis;
Score = score;
}
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
/// <summary>
/// 真实物体在 Rail 路径下的尺寸语义解析。
/// 先根据解释后的真实参考姿态解析 rail 轴约定,再使用:
/// 1. rail 基姿态(零角度时对象与路径框架对齐后的宿主姿态)
/// 2. 最终姿态rail 基姿态再叠加宿主轴角度修正)
/// 共同计算沿 rail forward/side/normal 的有效尺寸。
///
/// 关键点:
/// - 这里不能只吃“角度修正”本身;否则双轴旋转时会漏掉 rail 基姿态语义。
/// - 目标语义轴来自 rail 基姿态,而不是宿主世界轴。
/// </summary>
public static class RealObjectRailExtentResolver
{
public static bool TryResolveProjectedSemanticExtents(
Vector3 referenceAxisX,
Vector3 referenceAxisY,
Vector3 referenceAxisZ,
Vector3 desiredForward,
CoordinateSystemType hostType,
double forwardSize,
double sideSize,
double upSize,
Quaternion baselineHostQuaternion,
Quaternion finalHostQuaternion,
out ModelAxisConvention convention,
out (double forwardExtent, double sideExtent, double upExtent) extents)
{
convention = null;
extents = (0.0, 0.0, 0.0);
if (!RealObjectRailAxisConventionResolver.TryResolve(
referenceAxisX,
referenceAxisY,
referenceAxisZ,
desiredForward,
hostType,
out convention,
out _,
out _))
{
return false;
}
Vector3 localSize = convention.CreateScaleVector3(forwardSize, sideSize, upSize);
Vector3 rotatedLocalX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, finalHostQuaternion));
Vector3 rotatedLocalY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, finalHostQuaternion));
Vector3 rotatedLocalZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, finalHostQuaternion));
Vector3 targetForward = Vector3.Normalize(Vector3.Transform(convention.ForwardUnitVector, baselineHostQuaternion));
Vector3 targetUp = Vector3.Normalize(Vector3.Transform(convention.UpUnitVector, baselineHostQuaternion));
Vector3 targetSide = Vector3.Normalize(Vector3.Cross(targetForward, targetUp));
extents = (
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetForward),
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetSide),
ProjectExtent(localSize, rotatedLocalX, rotatedLocalY, rotatedLocalZ, targetUp));
return true;
}
private static double ProjectExtent(
Vector3 localSize,
Vector3 rotatedLocalX,
Vector3 rotatedLocalY,
Vector3 rotatedLocalZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedLocalX, targetAxis)) * localSize.X +
Math.Abs(Vector3.Dot(rotatedLocalY, targetAxis)) * localSize.Y +
Math.Abs(Vector3.Dot(rotatedLocalZ, targetAxis)) * localSize.Z;
}
}
}