实现了路径的剖面盒开关

This commit is contained in:
tian 2026-02-19 22:07:51 +08:00
parent 507339d4f7
commit 3f0b42770d
6 changed files with 828 additions and 4 deletions

View File

@ -333,6 +333,7 @@
<Compile Include="src\Utils\CachedTriangle3D.cs" />
<Compile Include="src\Utils\PathHelper.cs" />
<Compile Include="src\Utils\CollisionSceneHelper.cs" />
<Compile Include="src\Utils\SectionClipHelper.cs" />
<!-- Assembly Info -->
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

@ -77,10 +77,10 @@
1. [x] (功能)对路径上的各点进行坐标编辑
2. [ ] (功能)记录并查看路径文件操作的历史记录
3. [ ] (功能)自动隐藏或淡化非关键层,以便专注于物流路径相关的层级。
4. [ ] (优化)优化路径时间标签功能
4. [x] (优化)优化路径时间标签功能
5. [ ] 测试路径规划文件能导入DELMIA
6. [ ] (优化)优化路径规划分析和分析报告
7. [ ] (功能)增加物流属性自定义
6. [x] (优化)优化路径规划分析和分析报告
7. [x] (功能)增加物流属性自定义
8. [x] BUG 动画时物流车在起点时应该朝向路径方向,切换虚拟物体和指定物体时,原有的要归位
9. [x] (功能)物流车在路径点转弯时,设置转弯半径等参数,将路径变成曲线

View File

@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.Navisworks.Api;
using NavisApplication = Autodesk.Navisworks.Api.Application;
using NavisworksTransport.Core;
using NavisworksTransport.Core.Config;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@ -139,6 +143,31 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
private bool _isClipBoxEnabled;
/// <summary>
/// 是否启用剖面盒(聚焦当前路径)
/// </summary>
public bool IsClipBoxEnabled
{
get => _isClipBoxEnabled;
set
{
if (SetProperty(ref _isClipBoxEnabled, value))
{
if (value)
{
EnableClipBoxForCurrentRoute();
}
else
{
SectionClipHelper.ClearClipBox();
LogManager.Info("[剖面盒] 已关闭");
}
}
}
}
/// <summary>
/// 从 PathPointRenderPlugin 同步渲染状态
/// </summary>
@ -284,5 +313,96 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
#endregion
#region
/// <summary>
/// 启用剖面盒并聚焦到当前路径
/// </summary>
private void EnableClipBoxForCurrentRoute()
{
try
{
var currentRoute = _pathPlanningManager?.CurrentRoute;
if (currentRoute?.Points == null || currentRoute.Points.Count == 0)
{
LogManager.Warning("[剖面盒] 当前没有路径,无法设置剖面盒");
IsClipBoxEnabled = false;
return;
}
// 获取虚拟物体默认高度(米)
double objectHeightInMeters = ConfigManager.Instance.Current.PathEditing.ObjectHeightMeters;
// ========== 第1步处理路径计算包含虚拟物体高度的包围盒 ==========
// 转换路径点并根据路径类型调整Z值范围包含虚拟物体高度
var adjustedPoints = currentRoute.Points.Select(p => new Point3D(p.X, p.Y, p.Z)).ToList();
// 根据路径类型,添加包含虚拟物体高度的额外点
if (currentRoute.PathType == PathType.Ground)
{
// 地面路径:在路径点上方添加物体高度点
foreach (var p in currentRoute.Points)
{
adjustedPoints.Add(new Point3D(p.X, p.Y, p.Z + objectHeightInMeters));
}
}
else
{
// 空中路径:在路径点下方添加物体高度点
foreach (var p in currentRoute.Points)
{
adjustedPoints.Add(new Point3D(p.X, p.Y, p.Z - objectHeightInMeters));
}
}
// ========== 第2步统一扩展水平4米高度2米==========
bool success = SectionClipHelper.SetClipBoxByPath(adjustedPoints,
marginInMeters: 4.0, // 水平方向扩展4米
heightMarginInMeters: 2.0 // 高度方向上下各扩展2米
);
if (success)
{
LogManager.Info($"[剖面盒] 已启用,路径点数量: {currentRoute.Points.Count}, 各方向扩展: 2m");
}
else
{
LogManager.Warning("[剖面盒] 设置失败");
IsClipBoxEnabled = false;
}
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 启用失败: {ex.Message}");
IsClipBoxEnabled = false;
}
}
/// <summary>
/// 计算点列表的包围盒
/// </summary>
private (Point3D Center, double SizeX, double SizeY, double SizeZ) CalculatePointsBoundingBox(List<Point3D> points)
{
double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue;
foreach (var p in points)
{
minX = Math.Min(minX, p.X);
minY = Math.Min(minY, p.Y);
minZ = Math.Min(minZ, p.Z);
maxX = Math.Max(maxX, p.X);
maxY = Math.Max(maxY, p.Y);
maxZ = Math.Max(maxZ, p.Z);
}
var center = new Point3D((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2);
return (center, maxX - minX, maxY - minY, maxZ - minZ);
}
#endregion
}
}

View File

@ -1524,7 +1524,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
try
{
// 聚焦到模型(斜上方45度视角)
// 聚焦到模型(斜上方60度视角)
ViewpointHelper.FocusOnModelItem(logisticsModel.NavisworksItem, viewAngleDegrees: 60.0, targetViewRatio: 0.25);
UpdateMainStatus($"已聚焦到物流模型: {logisticsModel.Name}");

View File

@ -139,6 +139,26 @@ NavisworksTransport 主控制面板 - 采用与其他视图一致的Navisworks 2
<TextBlock Text="SP" FontSize="9" FontWeight="Bold"
Foreground="{Binding ShowObjectSpace, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Orange}"/>
</ToggleButton>
<!-- 分隔符 -->
<TextBlock Text="|"
VerticalAlignment="Center"
Margin="8,0,6,0"
Foreground="#FFCCCCCC"
FontSize="14"/>
<!-- 剖面盒开关 -->
<ToggleButton IsChecked="{Binding IsClipBoxEnabled}"
ToolTip="剖面盒(聚焦当前路径)"
Width="28" Height="24"
Margin="2,0">
<Path Data="M 2,6 L 14,6 L 14,18 L 2,18 Z M 2,6 L 6,2 L 18,2 L 14,6 M 14,18 L 18,14 L 18,2 L 14,6"
Stroke="{Binding IsClipBoxEnabled, Converter={StaticResource BoolToBrushConverter}, ConverterParameter=Red}"
StrokeThickness="1.5"
Fill="Transparent"
Stretch="Uniform"
Margin="2"/>
</ToggleButton>
</StackPanel>
<!-- 右侧:原有按钮 -->

View File

@ -0,0 +1,683 @@
using System;
using System.Collections.Generic;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils
{
/// <summary>
/// 剖面盒辅助类 - 用于优化碰撞检测性能
/// 通过设置视口剖面盒,只处理路径周围的对象
///
/// 注意Navisworks 的 ClipPlaneSet API 需要使用 JSON 字符串方式设置
/// 直接修改 ClipPlaneSet 对象的属性在手工打开剖面功能后会导致 "Object is Read-Only" 错误
/// </summary>
public static class SectionClipHelper
{
// 默认边距(米):路径周围保留的空间
private const double DEFAULT_MARGIN_METERS = 2.0;
// 默认高度范围(米):上下各延伸的高度
private const double DEFAULT_HEIGHT_MARGIN_METERS = 1.0;
/// <summary>
/// 根据路径点列表设置剖面盒
/// </summary>
/// <param name="pathPoints">路径点列表(模型单位)</param>
/// <param name="marginInMeters">水平边距(米)</param>
/// <param name="heightMarginInMeters">高度边距(米,上下各延伸)</param>
/// <returns>是否成功设置</returns>
public static bool SetClipBoxByPath(List<Point3D> pathPoints,
double marginInMeters = DEFAULT_MARGIN_METERS,
double heightMarginInMeters = DEFAULT_HEIGHT_MARGIN_METERS)
{
try
{
if (pathPoints == null || pathPoints.Count == 0)
{
LogManager.Warning("[剖面盒] 路径点为空,无法设置剖面盒");
return false;
}
// 米转换为模型单位
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
double margin = marginInMeters * metersToUnits;
double heightMargin = heightMarginInMeters * metersToUnits;
// 计算路径的包围盒
var pathBounds = CalculatePathBoundingBox(pathPoints);
// 扩展边距(使用模型单位)
var clipBox = ExpandBoundingBox(pathBounds, margin, heightMargin);
// 应用到视口
ApplyClipBox(clipBox);
LogManager.Info($"[剖面盒] 已设置 - 水平边距: {marginInMeters}m, 高度边距: {heightMarginInMeters}m, " +
$"范围: X[{clipBox.Min.X:F2}, {clipBox.Max.X:F2}], Y[{clipBox.Min.Y:F2}, {clipBox.Max.Y:F2}], Z[{clipBox.Min.Z:F2}, {clipBox.Max.Z:F2}]");
return true;
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 设置失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 根据单个点设置剖面盒(用于吊装路径等单点场景)
/// </summary>
/// <param name="centerPoint">中心点</param>
/// <param name="rangeMeters">范围(米)</param>
/// <param name="heightRangeMeters">高度范围(米)</param>
/// <returns>是否成功设置</returns>
public static bool SetClipBoxByPoint(Point3D centerPoint,
double rangeMeters = 10.0,
double heightRangeMeters = 5.0)
{
try
{
var min = new Point3D(
centerPoint.X - rangeMeters,
centerPoint.Y - rangeMeters,
centerPoint.Z - heightRangeMeters);
var max = new Point3D(
centerPoint.X + rangeMeters,
centerPoint.Y + rangeMeters,
centerPoint.Z + heightRangeMeters);
var clipBox = new BoundingBox3D(min, max);
ApplyClipBox(clipBox);
LogManager.Info($"[剖面盒] 已设置 - 中心: ({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " +
$"范围: {rangeMeters}m x {rangeMeters}m x {heightRangeMeters}m");
return true;
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 设置失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 根据楼层设置剖面盒
/// </summary>
/// <param name="floorItem">楼层模型项</param>
/// <param name="marginMeters">边距(米)</param>
/// <returns>是否成功设置</returns>
public static bool SetClipBoxByFloor(ModelItem floorItem, double marginMeters = 1.0)
{
try
{
if (floorItem == null)
{
LogManager.Warning("[剖面盒] 楼层对象为空");
return false;
}
BoundingBox3D floorBounds = floorItem.BoundingBox();
BoundingBox3D clipBox = ExpandBoundingBox(floorBounds, marginMeters, marginMeters);
ApplyClipBox(clipBox);
LogManager.Info($"[剖面盒] 已设置到楼层: {floorItem.DisplayName}");
return true;
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 设置失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 清除剖面盒(显示全部模型)
/// 使用 JSON 字符串方式设置 Enabled = false
/// </summary>
public static void ClearClipBox()
{
try
{
// 构建禁用剖面的 JSON
string json = BuildClipPlaneSetJson(
new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)),
false);
var view = Application.ActiveDocument.ActiveView;
view.TrySetClippingPlanes(json);
LogManager.Info("[剖面盒] 已清除");
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 清除失败: {ex.Message}");
}
}
/// <summary>
/// 检查剖面盒是否已启用
/// </summary>
public static bool IsClipBoxEnabled
{
get
{
try
{
return Application.ActiveDocument.CurrentViewpoint.Value.ClipPlanes.Enabled;
}
catch
{
return false;
}
}
}
/// <summary>
/// 获取当前剖面盒
/// </summary>
/// <param name="clipBox">输出剖面盒</param>
/// <returns>是否成功获取</returns>
public static bool TryGetCurrentClipBox(out BoundingBox3D clipBox)
{
clipBox = new BoundingBox3D();
try
{
var clipPlanes = Application.ActiveDocument.CurrentViewpoint.Value.ClipPlanes;
if (!clipPlanes.Enabled || clipPlanes.Mode != ClipPlaneSetMode.Box)
return false;
clipBox = clipPlanes.Box;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 测试点是否在剖面盒内
/// </summary>
public static bool IsPointInClipBox(Point3D point)
{
if (!TryGetCurrentClipBox(out var clipBox)) return true; // 无剖面盒时默认全部包含
return clipBox.Contains(point);
}
/// <summary>
/// 测试包围盒是否与剖面盒相交
/// </summary>
public static bool IntersectsClipBox(BoundingBox3D box)
{
if (!TryGetCurrentClipBox(out var clipBox)) return true; // 无剖面盒时默认全部相交
return clipBox.Intersects(box);
}
/// <summary>
/// 统计剖面盒内/外的对象数量(用于测试)
/// </summary>
/// <param name="totalCount">总对象数</param>
/// <param name="insideCount">在剖面盒内的对象数</param>
/// <param name="outsideCount">在剖面盒外的对象数</param>
public static void CountObjectsInClipBox(out int totalCount, out int insideCount, out int outsideCount)
{
totalCount = 0;
insideCount = 0;
outsideCount = 0;
try
{
if (!TryGetCurrentClipBox(out var clipBox))
{
LogManager.Warning("[剖面盒统计] 剖面盒未启用");
return;
}
var stack = new Stack<ModelItem>();
foreach (var model in Application.ActiveDocument.Models)
{
stack.Push(model.RootItem);
}
while (stack.Count > 0)
{
var item = stack.Pop();
totalCount++;
// 测试包围盒是否相交
BoundingBox3D itemBox = item.BoundingBox();
if (clipBox.Intersects(itemBox))
{
insideCount++;
}
else
{
outsideCount++;
continue; // 跳过子节点(剪枝)
}
// 继续遍历子节点
foreach (var child in item.Children)
{
stack.Push(child);
}
}
LogManager.Info($"[剖面盒统计] 总数: {totalCount}, 盒内: {insideCount}, 盒外: {outsideCount}, " +
$"过滤率: {(outsideCount * 100.0 / totalCount):F1}%");
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒统计] 失败: {ex.Message}");
}
}
/// <summary>
/// 使用采样点8个角点检测对象是否真正与剖面盒相交
/// 用于排除大包围盒但实际几何体远离剖面盒的对象(如地基)
/// </summary>
public static bool IntersectsByCornerPoints(BoundingBox3D itemBox, BoundingBox3D clipBox)
{
// 获取对象包围盒的8个角点
var corners = new Point3D[]
{
new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Min.Z),
new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Max.Z),
new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Min.Z),
new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Max.Z),
new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Min.Z),
new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Max.Z),
new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Min.Z),
new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Max.Z)
};
// 检查是否有任何角点在剖面盒内
foreach (var corner in corners)
{
if (clipBox.Contains(corner))
{
return true;
}
}
return false;
}
/// <summary>
/// 使用棱上自适应采样检测对象是否与剖面盒相交
/// 根据物体大小和剖面盒尺寸动态计算采样点数,确保长物体(如管道)不会漏检
/// 采样间距小于剖面盒最小边长,确保任何穿过剖面盒的物体都能被检测到
/// </summary>
/// <param name="itemBox">对象包围盒</param>
/// <param name="clipBox">剖面盒</param>
/// <param name="minSamplesPerEdge">每条棱的最小采样点数默认3个</param>
/// <returns>是否有采样点在剖面盒内</returns>
public static bool IntersectsByEdgeSampling(BoundingBox3D itemBox, BoundingBox3D clipBox, int minSamplesPerEdge = 3)
{
// 计算剖面盒的最小边长(作为采样间距上限)
double clipBoxMinSize = Math.Min(
Math.Min(clipBox.Max.X - clipBox.Min.X, clipBox.Max.Y - clipBox.Min.Y),
clipBox.Max.Z - clipBox.Min.Z
);
// 计算物体包围盒的三边长度
double itemSizeX = itemBox.Max.X - itemBox.Min.X;
double itemSizeY = itemBox.Max.Y - itemBox.Min.Y;
double itemSizeZ = itemBox.Max.Z - itemBox.Min.Z;
// 根据边长动态计算每条棱的采样点数
// 采样间距 = 边长 / (采样点数 - 1),确保间距 < 剖面盒最小边长
int GetSampleCount(double edgeLength)
{
if (edgeLength <= 0) return minSamplesPerEdge;
// 需要的采样点数 = 边长 / 剖面盒尺寸 + 1至少minSamplesPerEdge个
int count = (int)Math.Ceiling(edgeLength / clipBoxMinSize) + 1;
return Math.Max(count, minSamplesPerEdge);
}
int samplesX = GetSampleCount(itemSizeX);
int samplesY = GetSampleCount(itemSizeY);
int samplesZ = GetSampleCount(itemSizeZ);
// 12条棱的端点定义索引对应8个角点和采样点数
// 0: (0,0,0) 1: (0,0,1) 2: (0,1,0) 3: (0,1,1)
// 4: (1,0,0) 5: (1,0,1) 6: (1,1,0) 7: (1,1,1)
var edges = new (int start, int end, int samples)[]
{
// X方向棱4条用samplesX
(0, 4, samplesX), (1, 5, samplesX), (2, 6, samplesX), (3, 7, samplesX),
// Y方向棱4条用samplesY
(0, 2, samplesY), (1, 3, samplesY), (4, 6, samplesY), (5, 7, samplesY),
// Z方向棱4条用samplesZ
(0, 1, samplesZ), (2, 3, samplesZ), (4, 5, samplesZ), (6, 7, samplesZ)
};
// 8个角点的坐标
var cornerPoints = new Point3D[]
{
new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Min.Z),
new Point3D(itemBox.Min.X, itemBox.Min.Y, itemBox.Max.Z),
new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Min.Z),
new Point3D(itemBox.Min.X, itemBox.Max.Y, itemBox.Max.Z),
new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Min.Z),
new Point3D(itemBox.Max.X, itemBox.Min.Y, itemBox.Max.Z),
new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Min.Z),
new Point3D(itemBox.Max.X, itemBox.Max.Y, itemBox.Max.Z)
};
// 在每条棱上采样
foreach (var edge in edges)
{
var start = cornerPoints[edge.start];
var end = cornerPoints[edge.end];
int samples = edge.samples;
for (int i = 0; i < samples; i++)
{
double t = (double)i / (samples - 1); // 0.0 到 1.0
var samplePoint = new Point3D(
start.X + t * (end.X - start.X),
start.Y + t * (end.Y - start.Y),
start.Z + t * (end.Z - start.Z)
);
if (clipBox.Contains(samplePoint))
{
return true;
}
}
}
return false;
}
/// <summary>
/// 统计对象数量(使用采样点检测)
/// 与 CountObjectsInClipBox 的区别使用8个角点检测排除大包围盒假阳性
/// </summary>
public static void CountObjectsInClipBoxByCorners(out int totalCount, out int insideCount, out int outsideCount, out int largeBoxFiltered, List<string> debugDetails = null)
{
totalCount = 0;
insideCount = 0;
outsideCount = 0;
largeBoxFiltered = 0;
try
{
if (!TryGetCurrentClipBox(out var clipBox))
{
LogManager.Warning("[剖面盒角点统计] 剖面盒未启用");
return;
}
var stack = new Stack<ModelItem>();
foreach (var model in Application.ActiveDocument.Models)
{
stack.Push(model.RootItem);
}
while (stack.Count > 0)
{
var item = stack.Pop();
totalCount++;
BoundingBox3D itemBox = item.BoundingBox();
// 先检查包围盒是否相交(快速排除)
if (!clipBox.Intersects(itemBox))
{
outsideCount++;
continue; // 跳过子节点(剪枝)
}
// 相交:再用角点检测确认
bool hasCornerInside = IntersectsByCornerPoints(itemBox, clipBox);
if (hasCornerInside)
{
insideCount++;
}
else
{
outsideCount++;
largeBoxFiltered++;
// 记录被过滤的大对象
if (item.HasGeometry)
{
LogManager.Debug($"[剖面盒角点过滤] 大包围盒但无角点在盒内: {item.DisplayName}, 包围盒: X[{itemBox.Min.X:F2},{itemBox.Max.X:F2}], Y[{itemBox.Min.Y:F2},{itemBox.Max.Y:F2}], Z[{itemBox.Min.Z:F2},{itemBox.Max.Z:F2}]");
}
// 注意:这里继续遍历子节点,因为父对象可能只是外壳,子对象可能在盒内
}
// 继续遍历子节点
foreach (var child in item.Children)
{
stack.Push(child);
}
}
LogManager.Info($"[剖面盒角点统计] 总数: {totalCount}, 盒内(角点): {insideCount}, 盒外: {outsideCount}, 大包围盒过滤: {largeBoxFiltered}, " +
$"过滤率: {(outsideCount * 100.0 / totalCount):F1}%");
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒角点统计] 失败: {ex.Message}");
}
}
/// <summary>
/// 测试包围盒相交检测(用于验证)
/// </summary>
public static void TestIntersection()
{
try
{
if (!TryGetCurrentClipBox(out var clipBox))
{
LogManager.Warning("[剖面盒测试] 请先设置剖面盒");
return;
}
// 测试点
var center = clipBox.Center;
var testPointInside = new Point3D(center.X, center.Y, center.Z);
var testPointOutside = new Point3D(
center.X + 1000,
center.Y + 1000,
center.Z + 1000);
bool inside1 = IsPointInClipBox(testPointInside);
bool inside2 = IsPointInClipBox(testPointOutside);
LogManager.Info($"[剖面盒测试] 中心点: {inside1} (应为 True)");
LogManager.Info($"[剖面盒测试] 远点: {inside2} (应为 False)");
// 测试包围盒
var boxInside = new BoundingBox3D(
new Point3D(center.X - 1, center.Y - 1, center.Z - 1),
new Point3D(center.X + 1, center.Y + 1, center.Z + 1));
var boxOutside = new BoundingBox3D(
new Point3D(center.X + 100, center.Y + 100, center.Z + 100),
new Point3D(center.X + 200, center.Y + 200, center.Z + 200));
bool intersect1 = IntersectsClipBox(boxInside);
bool intersect2 = IntersectsClipBox(boxOutside);
LogManager.Info($"[剖面盒测试] 内部包围盒相交: {intersect1} (应为 True)");
LogManager.Info($"[剖面盒测试] 外部包围盒相交: {intersect2} (应为 False)");
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒测试] 失败: {ex.Message}");
}
}
#region
/// <summary>
/// 计算路径点的包围盒
/// </summary>
private static BoundingBox3D CalculatePathBoundingBox(List<Point3D> points)
{
if (points.Count == 1)
{
return new BoundingBox3D(points[0], points[0]);
}
double minX = double.MaxValue, minY = double.MaxValue, minZ = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue, maxZ = double.MinValue;
foreach (var p in points)
{
minX = Math.Min(minX, p.X);
minY = Math.Min(minY, p.Y);
minZ = Math.Min(minZ, p.Z);
maxX = Math.Max(maxX, p.X);
maxY = Math.Max(maxY, p.Y);
maxZ = Math.Max(maxZ, p.Z);
}
return new BoundingBox3D(
new Point3D(minX, minY, minZ),
new Point3D(maxX, maxY, maxZ));
}
/// <summary>
/// 扩展包围盒
/// </summary>
private static BoundingBox3D ExpandBoundingBox(BoundingBox3D box,
double horizontalMargin, double verticalMargin)
{
return ExpandBoundingBox(box, horizontalMargin, verticalMargin, verticalMargin);
}
/// <summary>
/// 扩展包围盒(支持分别设置上下边距)
/// </summary>
private static BoundingBox3D ExpandBoundingBox(BoundingBox3D box,
double horizontalMargin, double bottomMargin, double topMargin)
{
var min = new Point3D(
box.Min.X - horizontalMargin,
box.Min.Y - horizontalMargin,
box.Min.Z - bottomMargin);
var max = new Point3D(
box.Max.X + horizontalMargin,
box.Max.Y + horizontalMargin,
box.Max.Z + topMargin);
return new BoundingBox3D(min, max);
}
/// <summary>
/// 根据路径点列表设置剖面盒(支持分别设置上下边距)
/// </summary>
/// <param name="pathPoints">路径点列表(模型单位)</param>
/// <param name="marginInMeters">水平边距(米)</param>
/// <param name="heightMarginBottomInMeters">底部高度边距(米)</param>
/// <param name="heightMarginTopInMeters">顶部高度边距(米)</param>
public static bool SetClipBoxByPathWithMargins(List<Point3D> pathPoints,
double marginInMeters = DEFAULT_MARGIN_METERS,
double heightMarginBottomInMeters = DEFAULT_HEIGHT_MARGIN_METERS,
double heightMarginTopInMeters = DEFAULT_HEIGHT_MARGIN_METERS)
{
try
{
if (pathPoints == null || pathPoints.Count == 0)
{
LogManager.Warning("[剖面盒] 路径点为空,无法设置剖面盒");
return false;
}
// 米转换为模型单位
double metersToUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
double margin = marginInMeters * metersToUnits;
double bottomMargin = heightMarginBottomInMeters * metersToUnits;
double topMargin = heightMarginTopInMeters * metersToUnits;
// 计算路径的包围盒
var pathBounds = CalculatePathBoundingBox(pathPoints);
// 扩展边距(分别设置上下,使用模型单位)
var clipBox = ExpandBoundingBox(pathBounds, margin, bottomMargin, topMargin);
// 应用到视口
ApplyClipBox(clipBox);
LogManager.Info($"[剖面盒] 已设置 - 水平边距: {marginInMeters}m, 底部边距: {heightMarginBottomInMeters}m, 顶部边距: {heightMarginTopInMeters}m, " +
$"范围: X[{clipBox.Min.X:F2}, {clipBox.Max.X:F2}], Y[{clipBox.Min.Y:F2}, {clipBox.Max.Y:F2}], Z[{clipBox.Min.Z:F2}, {clipBox.Max.Z:F2}]");
return true;
}
catch (Exception ex)
{
LogManager.Error($"[剖面盒] 设置失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 应用剖面盒到视口
/// 使用 JSON 字符串方式设置,避免 "Object is Read-Only" 错误
/// </summary>
private static void ApplyClipBox(BoundingBox3D box)
{
// 构建 JSON 格式的 ClipPlaneSet
string json = BuildClipPlaneSetJson(box, true);
// 使用 View.SetClippingPlanes 方法设置
var view = Application.ActiveDocument.ActiveView;
// TrySetClippingPlanes 返回 bool 表示是否成功,不会抛出异常
bool success = view.TrySetClippingPlanes(json);
if (!success)
{
// 如果 Try 方法失败,尝试使用 SetClippingPlanes会抛出异常
view.SetClippingPlanes(json);
}
}
/// <summary>
/// 构建 ClipPlaneSet 的 JSON 字符串
/// </summary>
/// <param name="box">包围盒</param>
/// <param name="enabled">是否启用</param>
/// <returns>JSON 字符串</returns>
private static string BuildClipPlaneSetJson(BoundingBox3D box, bool enabled)
{
// Navisworks ClipPlaneSet JSON 格式:
// {
// "Type": "ClipPlaneSet",
// "Version": 1,
// "OrientedBox": {
// "Type": "OrientedBox3D",
// "Version": 1,
// "Box": [[minX, minY, minZ], [maxX, maxY, maxZ]],
// "Rotation": [0, 0, 0]
// },
// "Enabled": true/false
// }
return string.Format(
"{{\"Type\":\"ClipPlaneSet\",\"Version\":1," +
"\"OrientedBox\":{{\"Type\":\"OrientedBox3D\",\"Version\":1," +
"\"Box\":[[{0},{1},{2}],[{3},{4},{5}]]," +
"\"Rotation\":[0,0,0]}},\"Enabled\":{6}}}",
box.Min.X.ToString("G17"),
box.Min.Y.ToString("G17"),
box.Min.Z.ToString("G17"),
box.Max.X.ToString("G17"),
box.Max.Y.ToString("G17"),
box.Max.Z.ToString("G17"),
enabled.ToString().ToLowerInvariant());
}
#endregion
}
}