Files
ShrlAlgoToolkit/ShrlAlgoToolkit.RevitAddins/Checker/ModelCheckViewModel.cs
2026-03-01 10:42:42 +08:00

656 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Data;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Autodesk.Revit.DB.Electrical;
using Autodesk.Revit.DB.Mechanical;
using Autodesk.Revit.DB.Plumbing;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using MiniExcelLibs;
using MiniExcelLibs.OpenXml;
using Nice3point.Revit.Toolkit.External.Handlers;
using ShrlAlgoToolkit.RevitAddins.Common.Controls;
namespace ShrlAlgoToolkit.RevitAddins.ModelManager;
public partial class ModelCheckViewModel : ObservableObject
{
private readonly Standardizer.CorrectReferLevelExecutes correctReferLevelExecutes;
private readonly ActionEventHandler modifyModel = new();
private readonly ActionEventHandler showElementsSectionBox = new();
private readonly UIApplication uiapp;
public ModelCheckViewModel(UIApplication uiapp)
{
Items = [];
var cv = CollectionViewSource.GetDefaultView(Items);
cv.GroupDescriptions.Add(new PropertyGroupDescription("ErrorMessage"));
correctReferLevelExecutes = new Standardizer.CorrectReferLevelExecutes(uiapp);
this.uiapp = uiapp;
FindBasePoint();
}
private bool CanExport()
{
return Items.Any();
}
private static bool CanShowElement(object obj)
{
return obj is MessageModel { IsInstance: true };
}
[RelayCommand]
private /*async Task*/ void CheckModel(/*CancellationToken token*/)
{
//if (Items.Any())
//{
// Items = [];
//}
Items.Clear();
var uidoc = uiapp.ActiveUIDocument;
var doc = uidoc.Document;
//await Task.Delay(TimeSpan.FromSeconds(2), token);
//if (doc.ActiveView is not View3D view3d)
//{
// view3d = doc.OfClass<View3D>().FirstOrDefault(e => FilteredElementCollector.IsViewValidForElementIteration(doc, e.Id)) as View3D;
//}
var elements = doc.OfParentModelElements();
var typeInstancesGroups = elements.GroupBy(e => e.GetTypeId()).ToList();
if (IsCheckLevel)
{
var levels = doc.OfClass<Level>().OfType<Level>().OrderBy(l => l.Elevation);
var levelOutlines = Standardizer.CorrectReferLevelExecutes.GetRegions(levels);
foreach (var keyPair in levelOutlines)
{
//得到在标高范围内的元素
BoundingBoxIsInsideFilter insideFilter = new(keyPair.Value);
var level = keyPair.Key;
var insideCollector = new FilteredElementCollector(doc)
.WherePasses(insideFilter)
.Where(elem => elem.get_BoundingBox(doc.ActiveView) != null)
.ToList();
foreach (var elem in insideCollector)
{
var isCorrect = false;
var hasReferenceLevel = false;
foreach (Parameter param in elem.Parameters)
{
if (param.Definition.Name.Contains("标高") || param.Definition.Name.Contains("Level"))
{
hasReferenceLevel = param.StorageType == StorageType.ElementId;
if (hasReferenceLevel)
{
var value = param.AsElementId();
isCorrect = value == ElementId.InvalidElementId || param.AsElementId() == level.Id; //扶手等对象有宿主Host时为空
}
}
}
if (hasReferenceLevel && !isCorrect)
{
Items.Add(new MessageModel(elem, "参照标高有误"));
}
}
}
}
if (IsCheckName)
{
foreach (var item in typeInstancesGroups)
{
var id = item.Key;
var type = doc.GetElement(id);
var array = type.Name.Split('-');
if (array.Length < 3)
{
var errorItem = new MessageModel(type, "类型名称不符合三段式");
Items.Add(errorItem);
}
}
}
if (IsCheckProps)
{
}
if (IsCheckSlope)
{
var mepCurves = doc.OfClass<MEPCurve>().Where(e => e is not InsulationLiningBase and not FlexDuct and not FlexPipe);
foreach (var mepCurve in mepCurves)
{
var isError = false;
if (mepCurve is Pipe)
{
var param = mepCurve.get_Parameter(BuiltInParameter.RBS_PIPE_SLOPE);
isError = param.HasValue && ((param.AsDouble() < 0.25 && param.AsDouble() > 10E-6) || param.AsDouble() > Math.PI); //坡度过大或过小时,90度时AsDouble为0
}
else if (mepCurve is Duct)
{
var param = mepCurve.get_Parameter(BuiltInParameter.RBS_DUCT_SLOPE);
isError = param.HasValue && ((param.AsDouble() < 0.25 && param.AsDouble() > 10E-6) || param.AsDouble() > Math.PI); //坡度过大或过小时,90度时AsDouble为0
}
else if (mepCurve is Conduit or CableTray)
{
var p1 = mepCurve.get_Parameter(BuiltInParameter.RBS_START_OFFSET_PARAM).AsDouble();
var p2 = mepCurve.get_Parameter(BuiltInParameter.RBS_END_OFFSET_PARAM).AsDouble();
var l = mepCurve.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble();
var sin = Math.Abs(p1 - p2) / l;
var radian = Math.Asin(sin);
var slope = Math.Tan(radian);
isError = slope is (> 10E-6 and < 0.25) || (slope > Math.PI && Math.Abs(radian - Math.PI / 2) > 10E-6);
}
if (isError)
{
var errorItem = new MessageModel(mepCurve, "管线坡度有误差");
Items.Add(errorItem);
}
}
}
if (IsCheckLength)
{
var mepCurves = doc.OfClass<MEPCurve>().Where(e => e is not InsulationLiningBase);
foreach (var mepCurve in mepCurves)
{
var length = mepCurve.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble();
var connectors = mepCurve.GetConnectors(true);
if (length < 500 / 304.8 && connectors.Size == 2)
{
var errorItem = new MessageModel(mepCurve, "管道孤立或长度有误");
Items.Add(errorItem);
}
}
}
if (IsCheckEqual)
{
var mepCurves = doc.OfClass<MEPCurve>().Where(e => e is not InsulationLiningBase).ToList();
foreach (var c1 in mepCurves)
{
foreach (var c2 in mepCurves)
{
if (c1.Id != c2.Id)
{
var result = c1.GetCurve().Intersect(c2.GetCurve());
if (result == SetComparisonResult.Equal)
{
var errorItem = new MessageModel(c1, "管线存在重叠");
Items.Add(errorItem);
}
}
}
}
}
if (IsCheckSymbolGeometry)
{
foreach (var grouping in typeInstancesGroups)
{
var symbolId = grouping.Key;
if (doc.GetElement(symbolId) is not FamilySymbol symbol)
{
continue;
}
symbol.get_Geometry(new Options());
Options option = new() { ComputeReferences = true, DetailLevel = ViewDetailLevel.Fine };
var geometryElement = symbol.get_Geometry(option);
if (geometryElement == null)
{
continue;
}
var count = 0;
foreach (var geomObj in geometryElement)
{
if (geomObj is GeometryInstance geomInstance)
{
#if REVIT2018 || REVIT2020
if (geomInstance.Symbol is CADLinkType)
{
var errorItem = new MessageModel(symbol, "构件类型包含dwg模型");
Items.Add(errorItem);
}
#elif REVIT2025
if (doc.GetElement(geomInstance.GetSymbolGeometryId().SymbolId) is CADLinkType)
{
var errorItem = new MessageModel(symbol, "构件类型包含dwg模型");
Items.Add(errorItem);
}
#endif
}
if (geomObj is Solid)
{
count++;
}
}
if (count > 50)
{
var errorItem = new MessageModel(symbol, "构件类型几何需考虑简化超过50个实体");
Items.Add(errorItem);
}
}
}
ErrorCount = Items.Count;
ExportToExcelCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanExport))]
private void ExportToExcel()
{
var fileName = uiapp.ActiveUIDocument.Document.Title;
var dialog = new SaveFileDialog()
{
Title = "导出结果",
AddExtension = true,
DefaultExt = ".xlsx",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
Filter = "Excel表格(*.xlsx)|*.xlsx",
OverwritePrompt = true,
FileName = $"{fileName.Substring(0, fileName.Length - 4)}_检查结果"
};
if (dialog.ShowDialog() != true)
{
return;
}
//ExcelPackage.LicenseContext = OfficeOpenXml.LicenseContext.NonCommercial;
try
{
var config = new OpenXmlConfiguration()
{
TableStyles = TableStyles.Default,
AutoFilter = true,
};
Dictionary<string, MessageModel[]> sheets = Items.GroupBy(e => e.ErrorMessage).ToDictionary(g => g.Key, g => g.ToArray());
MiniExcel.SaveAs(dialog.FileName, sheets);
//EPPlusHelper.WriteExcel(
// dialog.FileName,
// m =>
// {
// IEnumerable<IGrouping<string, MessageModel>> groups = Items.GroupBy(e => e.ErrorMessage);
// foreach (var group in groups)
// {
// var sheet = m.Workbook.Worksheets.Add(group.Key);
// for (var i = -1; i < group.Count(); i++)
// {
// if (i == -1)
// {
// sheet.Cells[1, 1].Value = "元素ID";
// sheet.Cells[1, 2].Value = "族类别";
// sheet.Cells[1, 3].Value = "类型或实例名称";
// sheet.Cells[1, 4].Value = "参照标高";
// sheet.Cells[1, 5].Value = "系统";
// sheet.Cells[1, 6].Value = "主体";
// sheet.Cells[1, 7].Value = "房间";
// }
// else
// {
// var elem = group.ElementAt(i).Element;
// var type = elem.GetType();
// var host = type.GetProperty("Host");
// var level = type.GetProperty("ReferenceLevel");
// var mepSystem = type.GetProperty("MEPSystem");
// var room = type.GetProperty("Room");
// sheet.Cells[i + 2, 1].Value = elem.Id.ToString();
// sheet.Cells[i + 2, 2].Value = elem.Category.Name;
// sheet.Cells[i + 2, 3].Value = elem.Name;
// if (level != null)
// {
// sheet.Cells[i + 2, 4].Value = (level.GetValue(elem, null) as Level)?.Name;
// }
// if (mepSystem != null)
// {
// sheet.Cells[i + 2, 5].Value = (mepSystem.GetValue(elem, null) as MEPSystem)?.Name;
// }
// if (host != null)
// {
// sheet.Cells[i + 2, 6].Value = (host.GetValue(elem, null) as Element)?.Name;
// }
// if (room != null)
// {
// sheet.Cells[i + 2, 7].Value = (room.GetValue(room, null) as Room)?.Name;
// }
// }
// }
// EPPlusHelper.SetTitle(sheet, 1, 1, 1, 7);
// EPPlusHelper.SetStyle(sheet, 2, 1, group.Count() + 1, 7);
// var range = sheet.Cells[1, 1, group.Count() + 1, 7];
// range.AutoFitColumns();
// }
// }
//);
ExportErrorMessageToExcel(dialog.FileName);
Common.Assists.WinDialogAssist.OpenFolderAndSelectFile(dialog.FileName);
}
catch (Exception ex)
{
Common.Assists.LogAssist.ToLog(ex.Message);
}
}
private void ExportErrorMessageToExcel(string filePath)
{
// 1. 准备大字典,用于存放所有 Sheet 数据
var allSheetsData = new Dictionary<string, object>();
// 2. 分组逻辑保持不变
IEnumerable<IGrouping<string, MessageModel>> groups = Items.GroupBy(e => e.ErrorMessage);
foreach (var group in groups)
{
// 3. 构造当前 Sheet 的行数据列表
var sheetRows = new List<Dictionary<string, object>>();
foreach (var item in group)
{
var elem = item.Element;
if (elem == null) continue;
var type = elem.GetType();
// 创建一行数据
var row = new Dictionary<string, object>();
// 直接赋值固定属性
row["元素ID"] = elem.Id.ToString();
row["族类别"] = elem.Category?.Name ?? "";
row["类型或实例名称"] = elem.Name;
// 使用反射获取动态属性(保留你原有的逻辑)
var levelProp = type.GetProperty("ReferenceLevel");
var mepProp = type.GetProperty("MEPSystem");
var hostProp = type.GetProperty("Host");
var roomProp = type.GetProperty("Room");
row["参照标高"] = levelProp != null ? (levelProp.GetValue(elem, null) as Level)?.Name : "";
row["系统"] = mepProp != null ? (mepProp.GetValue(elem, null) as MEPSystem)?.Name : "";
row["主体"] = hostProp != null ? (hostProp.GetValue(elem, null) as Element)?.Name : "";
// 注意:原代码中 room.GetValue(room, null) 应该是 roomProp.GetValue(elem, null) 的笔误
row["房间"] = roomProp != null ? (roomProp.GetValue(elem, null) as Room)?.Name : "";
sheetRows.Add(row);
}
// 4. 处理 Sheet 名称Excel 限制 31 个字符且不能有特殊字符)
string sheetName = group.Key ?? "未命名";
char[] invalidChars = { '\\', '/', '?', '*', '[', ']', ':' };
foreach (var c in invalidChars) sheetName = sheetName.Replace(c, '_');
if (sheetName.Length > 31) sheetName = sheetName.Substring(0, 31);
// 防止同名 Sheet 冲突
if (allSheetsData.ContainsKey(sheetName))
sheetName = sheetName.Substring(0, Math.Min(sheetName.Length, 25)) + "_" + Guid.NewGuid().ToString().Substring(0, 4);
allSheetsData.Add(sheetName, sheetRows);
}
// 5. 保存文件
try
{
// MiniExcel 默认会将 Dictionary 的 Key 作为表头并加粗
MiniExcel.SaveAs(filePath, allSheetsData, overwriteFile: true);
}
catch (IOException)
{
// 提示文件被占用
MessageBox.Show("请先关闭已打开的 Excel 文件后再尝试保存。");
}
}
private void FindBasePoint()
{
var basePoints = uiapp.ActiveUIDocument.Document.OfClass<BasePoint>().OfType<BasePoint>();
foreach (var item in basePoints)
{
//南北
var ns = Math.Round(
item.get_Parameter(BuiltInParameter.BASEPOINT_NORTHSOUTH_PARAM).AsDouble() * 0.3048,
4,
MidpointRounding.AwayFromZero
);
//东西
var ew = Math.Round(item.get_Parameter(BuiltInParameter.BASEPOINT_EASTWEST_PARAM).AsDouble() * 0.3048, 4, MidpointRounding.AwayFromZero);
//高程
var elev = Math.Round(
item.get_Parameter(BuiltInParameter.BASEPOINT_ELEVATION_PARAM).AsDouble() * 0.3048,
4,
MidpointRounding.AwayFromZero
);
if (item.IsShared)//测量点
{
SharedBasePoint = $"南北:{ns}m东西{ew}m高程{elev}m";
continue;
}
if (item.Category.GetHashCode() == -2001271)//项目基点
{
//正北角度
var angle = item.get_Parameter(BuiltInParameter.BASEPOINT_ANGLETON_PARAM).AsValueString();
ProjectBasePoint = $"南北:{ns}m东西{ew}m高程{elev}m角度{angle}";
}
}
}
[RelayCommand]
private void ModifyModel()
{
var errorItems = new List<MessageModel>();
modifyModel.Raise(_ =>
{
var uidoc = uiapp.ActiveUIDocument;
var doc = uidoc.Document;
doc.Invoke(
_ =>
{
#region
var errorItems1 = correctReferLevelExecutes.SetWalls();
errorItems.AddRange(errorItems1);
var errorItems2 = correctReferLevelExecutes.SetInstances();
errorItems.AddRange(errorItems2);
var errorItems3 = correctReferLevelExecutes.SetMEPCurves();
errorItems.AddRange(errorItems3);
#endregion
#region
foreach (var error in Items)
{
if (error.ErrorMessage != "管线坡度有误差")
{
continue;
}
var elem = error.Element;
if (elem is MEPCurve mep)
{
var loc = mep.Location as LocationCurve;
var line = loc!.Curve as Line;
var endPoint = line!.GetEndPoint(0);
var endPoint1 = line.GetEndPoint(1);
Line unboundLine;
Line l;
var xoy1 = endPoint.Flatten();
var xoy2 = endPoint1.Flatten();
var tan = Math.Abs(endPoint.Z - endPoint1.Z) / xoy1.DistanceTo(xoy2);
var dir = -XYZ.BasisZ;
if (endPoint.Z > endPoint1.Z) //以高点作为基准,修改低点
{
if (tan > 1)
{
unboundLine = Line.CreateUnbound(endPoint, dir);
var startPoint = unboundLine.Project(endPoint1).XYZPoint;
l = Line.CreateBound(endPoint, startPoint);
}
else
{
dir = xoy2 - xoy1;
unboundLine = Line.CreateUnbound(endPoint, dir);
var startPoint = unboundLine.Project(endPoint1).XYZPoint;
l = Line.CreateBound(endPoint, startPoint);
}
}
else
{
if (tan > 1)
{
unboundLine = Line.CreateUnbound(endPoint1, dir);
var startPoint = unboundLine.Project(endPoint).XYZPoint;
l = Line.CreateBound(startPoint, endPoint1);
}
else
{
dir = xoy2 - xoy1;
unboundLine = Line.CreateUnbound(endPoint1, dir);
var startPoint = unboundLine.Project(endPoint).XYZPoint;
l = Line.CreateBound(startPoint, endPoint1);
}
}
loc.Curve = l;
}
}
#endregion
},
"模型调整"
);
if (errorItems.Count == 0)
{
return;
}
Common.Assists.WinDialogAssist.ShowOrActivate<MessageWin, MessageViewModel>(uiapp.ActiveUIDocument, errorItems, "未解决错误");
});
}
[RelayCommand(CanExecute = nameof(CanShowElement))]
private void ShowElement(object obj)
{
if (obj is MessageModel model)
{
var uidoc = uiapp.ActiveUIDocument;
var doc = uidoc.Document;
//if (UiDocument.ActiveView.IsTemporaryHideIsolateActive())
//{
// UiDocument.ActiveView.temporary
//}
if (model.Element.IsValidObject)
{
if (model.Element.IsHidden(uidoc.ActiveView))
{
return;
}
List<ElementId> ids = [model.Element.Id];
//UiDocument.ActiveView.IsolateElementTemporary(model.ElementToMove.ViewId);
showElementsSectionBox.Raise(_ =>
{
if (uidoc.ActiveView is not View3D view3d)
{
view3d =
doc.OfClass<View3D>().FirstOrDefault(e => FilteredElementCollector.IsViewValidForElementIteration(doc, e.Id)) as View3D;
}
uidoc.ActiveView = view3d;
doc.Invoke(_ =>
{
if (UseSectionBox)
{
doc.InvokeSub(_ => view3d.SectionBoxElements(ids));
}
view3d.ZoomElements(uidoc, ids);
uidoc.Selection.SetElementIds(ids);
});
});
}
else
{
Items.Remove(model);
}
}
}
[ObservableProperty]
public partial string Properties { get; set; }
[ObservableProperty]
public partial int ErrorCount { get; set; }
[ObservableProperty]
public partial bool IsCheckEqual { get; set; }
[ObservableProperty]
public partial bool IsCheckLength { get; set; }
[ObservableProperty]
public partial bool IsCheckLevel { get; set; }
[ObservableProperty]
public partial bool IsCheckName { get; set; }
[ObservableProperty]
public partial bool IsCheckProps { get; set; }
[ObservableProperty]
public partial bool IsCheckSlope { get; set; }
[ObservableProperty]
public partial bool IsCheckSymbolGeometry { get; set; }
[ObservableProperty]
public partial ObservableCollection<MessageModel> Items { get; set; }
[ObservableProperty]
public partial int MyProperty { get; set; }
public string ProjectBasePoint { get; set; }
public string SharedBasePoint { get; set; }
[ObservableProperty]
public partial bool UseSectionBox { get; set; }
}
public enum ModelCheckType
{
/// <summary>
/// 命名
/// </summary>
Name,
/// <summary>
/// 坡度问题
/// </summary>
Slope,
/// <summary>
/// 参照标高
/// </summary>
ReferenceLevel,
/// <summary>
/// 属性检查
/// </summary>
Property,
/// <summary>
/// 管线重叠
/// </summary>
Equal,
/// <summary>
/// 管线过短,或孤立管线
/// </summary>
Length
}