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().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().OfType().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().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().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().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 sheets = Items.GroupBy(e => e.ErrorMessage).ToDictionary(g => g.Key, g => g.ToArray()); MiniExcel.SaveAs(dialog.FileName, sheets); //EPPlusHelper.WriteExcel( // dialog.FileName, // m => // { // IEnumerable> 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(); // 2. 分组逻辑保持不变 IEnumerable> groups = Items.GroupBy(e => e.ErrorMessage); foreach (var group in groups) { // 3. 构造当前 Sheet 的行数据列表 var sheetRows = new List>(); foreach (var item in group) { var elem = item.Element; if (elem == null) continue; var type = elem.GetType(); // 创建一行数据 var row = new Dictionary(); // 直接赋值固定属性 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().OfType(); 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(); 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(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 ids = [model.Element.Id]; //UiDocument.ActiveView.IsolateElementTemporary(model.ElementToMove.ViewId); showElementsSectionBox.Raise(_ => { if (uidoc.ActiveView is not View3D view3d) { view3d = doc.OfClass().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 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 { /// /// 命名 /// Name, /// /// 坡度问题 /// Slope, /// /// 参照标高 /// ReferenceLevel, /// /// 属性检查 /// Property, /// /// 管线重叠 /// Equal, /// /// 管线过短,或孤立管线 /// Length }