NavisworksTransport/src/Utils/CoordinateSystem/ObjectPassageProjectionOptimizer.cs
tian 7187bcf622 自动最优朝向: 黑盒实测模式
- 新增 ObjectPassageProjectionOptimizer(SO(3)粗搜索+Nelder-Mead)
- EditRotationWindow 加入"自动调整"按钮
- AnimationControlViewModel 实现黑盒实测评估(临时摆位→读BoundingBox→恢复)
- 设计文档和实现计划
2026-05-27 12:24:01 +08:00

382 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
namespace NavisworksTransport.Utils.CoordinateSystem
{
public sealed class ObjectPassageProjectionOptimizationRequest
{
public ObjectPassageProjectionOptimizationRequest(
double sizeX,
double sizeY,
double sizeZ,
Vector3 hostPathForward,
Vector3 hostUp,
double areaRelativeTieTolerance = 0.01,
Quaternion? baselineHostRotation = null)
{
SizeX = sizeX;
SizeY = sizeY;
SizeZ = sizeZ;
HostPathForward = hostPathForward;
HostUp = hostUp;
AreaRelativeTieTolerance = areaRelativeTieTolerance;
BaselineHostRotation = Quaternion.Normalize(baselineHostRotation ?? Quaternion.Identity);
}
public double SizeX { get; }
public double SizeY { get; }
public double SizeZ { get; }
public Vector3 HostPathForward { get; }
public Vector3 HostUp { get; }
public double AreaRelativeTieTolerance { get; }
public Quaternion BaselineHostRotation { get; }
}
public sealed class ObjectPassageProjectionOptimizationResult
{
private ObjectPassageProjectionOptimizationResult(
bool success,
LocalEulerRotationCorrection correction,
ObjectPassageProjectionScore score,
string errorMessage)
{
Success = success;
Correction = correction;
Score = score;
ErrorMessage = errorMessage;
}
public bool Success { get; }
public LocalEulerRotationCorrection Correction { get; }
public ObjectPassageProjectionScore Score { get; }
public string ErrorMessage { get; }
public static ObjectPassageProjectionOptimizationResult CreateSuccess(
LocalEulerRotationCorrection correction,
ObjectPassageProjectionScore score)
{
return new ObjectPassageProjectionOptimizationResult(true, correction, score, null);
}
public static ObjectPassageProjectionOptimizationResult CreateFailure(string errorMessage)
{
return new ObjectPassageProjectionOptimizationResult(
false,
LocalEulerRotationCorrection.Zero,
ObjectPassageProjectionScore.Invalid,
errorMessage);
}
}
public struct ObjectPassageProjectionScore
{
public static readonly ObjectPassageProjectionScore Invalid =
new ObjectPassageProjectionScore(double.PositiveInfinity, double.PositiveInfinity);
public ObjectPassageProjectionScore(double widthAcrossPath, double heightAlongHostUp)
{
WidthAcrossPath = widthAcrossPath;
HeightAlongHostUp = heightAlongHostUp;
Area = widthAcrossPath * heightAlongHostUp;
}
public double WidthAcrossPath { get; }
public double HeightAlongHostUp { get; }
public double Area { get; }
}
public static class ObjectPassageProjectionOptimizer
{
private const double Epsilon = 1e-9;
public static ObjectPassageProjectionOptimizationResult Optimize(
ObjectPassageProjectionOptimizationRequest request)
{
if (!TryValidateRequest(request, out string errorMessage))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure(errorMessage);
}
return OptimizeWithEvaluator(request, correction => Evaluate(request, correction));
}
public static ObjectPassageProjectionOptimizationResult OptimizeWithEvaluator(
ObjectPassageProjectionOptimizationRequest request,
Func<LocalEulerRotationCorrection, ObjectPassageProjectionScore> evaluator)
{
if (!TryValidateRequest(request, out string errorMessage))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure(errorMessage);
}
if (evaluator == null)
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("自动调整评分器为空。");
}
LocalEulerRotationCorrection bestCorrection = LocalEulerRotationCorrection.Zero;
ObjectPassageProjectionScore bestScore = ObjectPassageProjectionScore.Invalid;
foreach (double xDegrees in CandidateAngles())
{
foreach (double yDegrees in CandidateAngles())
{
foreach (double zDegrees in CandidateAngles())
{
var correction = new LocalEulerRotationCorrection(xDegrees, yDegrees, zDegrees);
ObjectPassageProjectionScore score = evaluator(correction);
if (IsBetter(score, bestScore, request.AreaRelativeTieTolerance))
{
bestScore = score;
bestCorrection = correction;
}
}
}
}
RefineByPatternSearch(request, evaluator, ref bestCorrection, ref bestScore);
if (double.IsInfinity(bestScore.Area))
{
return ObjectPassageProjectionOptimizationResult.CreateFailure("优化器未找到有效姿态。");
}
return ObjectPassageProjectionOptimizationResult.CreateSuccess(bestCorrection, bestScore);
}
public static ObjectPassageProjectionScore Evaluate(
ObjectPassageProjectionOptimizationRequest request,
LocalEulerRotationCorrection correction)
{
if (!TryCreateFrame(request, out Vector3 hostSide, out Vector3 hostUp))
{
throw new InvalidOperationException("路径方向退化,无法计算通行截面。");
}
Quaternion correctionRotation = correction.CreateQuaternion(Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);
Quaternion rotation = Quaternion.Normalize(correctionRotation * request.BaselineHostRotation);
Vector3 rotatedHostX = Vector3.Normalize(Vector3.Transform(Vector3.UnitX, rotation));
Vector3 rotatedHostY = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, rotation));
Vector3 rotatedHostZ = Vector3.Normalize(Vector3.Transform(Vector3.UnitZ, rotation));
double width = ProjectExtent(
request.SizeX,
request.SizeY,
request.SizeZ,
rotatedHostX,
rotatedHostY,
rotatedHostZ,
hostSide);
double height = ProjectExtent(
request.SizeX,
request.SizeY,
request.SizeZ,
rotatedHostX,
rotatedHostY,
rotatedHostZ,
hostUp);
return new ObjectPassageProjectionScore(width, height);
}
private static bool TryValidateRequest(
ObjectPassageProjectionOptimizationRequest request,
out string errorMessage)
{
if (request == null)
{
errorMessage = "自动调整请求为空。";
return false;
}
if (request.SizeX <= Epsilon || request.SizeY <= Epsilon || request.SizeZ <= Epsilon)
{
errorMessage = "物体尺寸无效。";
return false;
}
if (!TryCreateFrame(request, out _, out _))
{
errorMessage = "路径方向退化,无法计算通行截面。";
return false;
}
errorMessage = null;
return true;
}
private static bool TryCreateFrame(
ObjectPassageProjectionOptimizationRequest request,
out Vector3 hostSide,
out Vector3 hostUp)
{
hostSide = Vector3.Zero;
hostUp = Vector3.Zero;
if (request == null || request.HostUp.LengthSquared() < Epsilon)
{
return false;
}
hostUp = Vector3.Normalize(request.HostUp);
Vector3 hostPathForward = request.HostPathForward - hostUp * Vector3.Dot(request.HostPathForward, hostUp);
if (hostPathForward.LengthSquared() < Epsilon)
{
return false;
}
hostPathForward = Vector3.Normalize(hostPathForward);
hostSide = Vector3.Cross(hostPathForward, hostUp);
if (hostSide.LengthSquared() < Epsilon)
{
return false;
}
hostSide = Vector3.Normalize(hostSide);
return true;
}
private static bool IsBetter(
ObjectPassageProjectionScore candidate,
ObjectPassageProjectionScore currentBest,
double areaRelativeTieTolerance)
{
if (double.IsInfinity(currentBest.Area))
{
return true;
}
double tolerance = Math.Max(0.0, areaRelativeTieTolerance) * Math.Max(1.0, currentBest.Area);
if (candidate.Area < currentBest.Area - tolerance)
{
return true;
}
if (Math.Abs(candidate.Area - currentBest.Area) <= tolerance)
{
if (candidate.HeightAlongHostUp < currentBest.HeightAlongHostUp - Epsilon)
{
return true;
}
if (Math.Abs(candidate.HeightAlongHostUp - currentBest.HeightAlongHostUp) <= Epsilon &&
candidate.WidthAcrossPath < currentBest.WidthAcrossPath - Epsilon)
{
return true;
}
}
return false;
}
private static double ProjectExtent(
double sizeX,
double sizeY,
double sizeZ,
Vector3 rotatedHostX,
Vector3 rotatedHostY,
Vector3 rotatedHostZ,
Vector3 targetAxis)
{
return Math.Abs(Vector3.Dot(rotatedHostX, targetAxis)) * sizeX +
Math.Abs(Vector3.Dot(rotatedHostY, targetAxis)) * sizeY +
Math.Abs(Vector3.Dot(rotatedHostZ, targetAxis)) * sizeZ;
}
private static double[] CandidateAngles()
{
return new[]
{
0.0,
90.0,
180.0,
270.0,
45.0,
135.0,
225.0,
315.0
};
}
private static void RefineByPatternSearch(
ObjectPassageProjectionOptimizationRequest request,
Func<LocalEulerRotationCorrection, ObjectPassageProjectionScore> evaluator,
ref LocalEulerRotationCorrection bestCorrection,
ref ObjectPassageProjectionScore bestScore)
{
double stepDegrees = 22.5;
while (stepDegrees >= 1.0)
{
bool improved = false;
foreach (var delta in PatternSearchDeltas(stepDegrees))
{
var candidate = new LocalEulerRotationCorrection(
NormalizeDegrees(bestCorrection.XDegrees + delta.X),
NormalizeDegrees(bestCorrection.YDegrees + delta.Y),
NormalizeDegrees(bestCorrection.ZDegrees + delta.Z));
ObjectPassageProjectionScore score = evaluator(candidate);
if (IsBetter(score, bestScore, request.AreaRelativeTieTolerance))
{
bestCorrection = candidate;
bestScore = score;
improved = true;
}
}
if (!improved)
{
stepDegrees *= 0.5;
}
}
}
private static (double X, double Y, double Z)[] PatternSearchDeltas(double stepDegrees)
{
return new[]
{
( stepDegrees, 0.0, 0.0),
(-stepDegrees, 0.0, 0.0),
(0.0, stepDegrees, 0.0),
(0.0, -stepDegrees, 0.0),
(0.0, 0.0, stepDegrees),
(0.0, 0.0, -stepDegrees),
( stepDegrees, stepDegrees, 0.0),
( stepDegrees, -stepDegrees, 0.0),
(-stepDegrees, stepDegrees, 0.0),
(-stepDegrees, -stepDegrees, 0.0),
( stepDegrees, 0.0, stepDegrees),
( stepDegrees, 0.0, -stepDegrees),
(-stepDegrees, 0.0, stepDegrees),
(-stepDegrees, 0.0, -stepDegrees),
(0.0, stepDegrees, stepDegrees),
(0.0, stepDegrees, -stepDegrees),
(0.0, -stepDegrees, stepDegrees),
(0.0, -stepDegrees, -stepDegrees)
};
}
private static double NormalizeDegrees(double degrees)
{
double normalized = degrees % 360.0;
if (normalized < 0.0)
{
normalized += 360.0;
}
return Math.Abs(normalized) < Epsilon ? 0.0 : normalized;
}
}
}