实现了路径的剖面盒开关
This commit is contained in:
parent
507339d4f7
commit
3f0b42770d
@ -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>
|
||||
|
||||
@ -77,10 +77,10 @@
|
||||
1. [x] (功能)对路径上的各点进行坐标编辑
|
||||
2. [ ] (功能)记录并查看路径文件操作的历史记录
|
||||
3. [ ] (功能)自动隐藏或淡化非关键层,以便专注于物流路径相关的层级。
|
||||
4. [ ] (优化)优化路径时间标签功能
|
||||
4. [x] (优化)优化路径时间标签功能
|
||||
5. [ ] (测试)路径规划文件能导入DELMIA
|
||||
6. [ ] (优化)优化路径规划分析和分析报告
|
||||
7. [ ] (功能)增加物流属性自定义
|
||||
6. [x] (优化)优化路径规划分析和分析报告
|
||||
7. [x] (功能)增加物流属性自定义
|
||||
8. [x] (BUG) 动画时物流车在起点时应该朝向路径方向,切换虚拟物体和指定物体时,原有的要归位
|
||||
9. [x] (功能)物流车在路径点转弯时,设置转弯半径等参数,将路径变成曲线
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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}");
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- 右侧:原有按钮 -->
|
||||
|
||||
683
src/Utils/SectionClipHelper.cs
Normal file
683
src/Utils/SectionClipHelper.cs
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user