From 9c484f15627748920467a3c981af70c09f2a4abf Mon Sep 17 00:00:00 2001 From: Aydimir95 Date: Tue, 16 Dec 2025 19:32:04 -0800 Subject: [PATCH] Add data extraction commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds three new data extraction commands to enable comprehensive model data analysis: 1. **export_room_data** - Extract all rooms with detailed properties including area, volume, perimeter, and parameters 2. **get_material_quantities** - Calculate material quantities and takeoffs with area and volume calculations per material 3. **analyze_model_statistics** - Analyze model complexity with element counts by category, type, family, and level ## Files Added: - Models/DataExtraction/ (3 model classes) - Services/DataExtraction/ (3 event handlers) - Commands/DataExtraction/ (3 command classes) - Updated command.json with new command registrations These commands follow the existing pattern (Model → Handler → Command) and support the MCP protocol for AI-driven BIM automation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- command.json | 15 ++ .../AnalyzeModelStatisticsCommand.cs | 45 +++++ .../DataExtraction/ExportRoomDataCommand.cs | 46 +++++ .../GetMaterialQuantitiesCommand.cs | 46 +++++ .../DataExtraction/MaterialQuantityModel.cs | 55 ++++++ .../DataExtraction/ModelStatisticsModel.cs | 91 +++++++++ .../Models/DataExtraction/RoomDataModel.cs | 70 +++++++ .../RevitMCPCommandSet.csproj | 4 +- .../AnalyzeModelStatisticsEventHandler.cs | 178 ++++++++++++++++++ .../ExportRoomDataEventHandler.cs | 109 +++++++++++ .../GetMaterialQuantitiesEventHandler.cs | 154 +++++++++++++++ 11 files changed, 811 insertions(+), 2 deletions(-) create mode 100644 revit-mcp-commandset/Commands/DataExtraction/AnalyzeModelStatisticsCommand.cs create mode 100644 revit-mcp-commandset/Commands/DataExtraction/ExportRoomDataCommand.cs create mode 100644 revit-mcp-commandset/Commands/DataExtraction/GetMaterialQuantitiesCommand.cs create mode 100644 revit-mcp-commandset/Models/DataExtraction/MaterialQuantityModel.cs create mode 100644 revit-mcp-commandset/Models/DataExtraction/ModelStatisticsModel.cs create mode 100644 revit-mcp-commandset/Models/DataExtraction/RoomDataModel.cs create mode 100644 revit-mcp-commandset/Services/DataExtraction/AnalyzeModelStatisticsEventHandler.cs create mode 100644 revit-mcp-commandset/Services/DataExtraction/ExportRoomDataEventHandler.cs create mode 100644 revit-mcp-commandset/Services/DataExtraction/GetMaterialQuantitiesEventHandler.cs diff --git a/command.json b/command.json index 159aaa1..8654ea6 100644 --- a/command.json +++ b/command.json @@ -47,6 +47,21 @@ "commandName": "create_line_based_element", "description": "Create line based element such as wall", "assemblyPath": "RevitMCPCommandSet.dll" + }, + { + "commandName": "export_room_data", + "description": "Extract all rooms with detailed properties including area, volume, perimeter, and parameters", + "assemblyPath": "RevitMCPCommandSet.dll" + }, + { + "commandName": "get_material_quantities", + "description": "Calculate material quantities and takeoffs with area and volume calculations per material", + "assemblyPath": "RevitMCPCommandSet.dll" + }, + { + "commandName": "analyze_model_statistics", + "description": "Analyze model complexity with element counts by category, type, family, and level", + "assemblyPath": "RevitMCPCommandSet.dll" } ] } \ No newline at end of file diff --git a/revit-mcp-commandset/Commands/DataExtraction/AnalyzeModelStatisticsCommand.cs b/revit-mcp-commandset/Commands/DataExtraction/AnalyzeModelStatisticsCommand.cs new file mode 100644 index 0000000..1714132 --- /dev/null +++ b/revit-mcp-commandset/Commands/DataExtraction/AnalyzeModelStatisticsCommand.cs @@ -0,0 +1,45 @@ +using Autodesk.Revit.UI; +using Newtonsoft.Json.Linq; +using RevitMCPCommandSet.Services.DataExtraction; +using RevitMCPSDK.API.Base; + +namespace RevitMCPCommandSet.Commands.DataExtraction +{ + public class AnalyzeModelStatisticsCommand : ExternalEventCommandBase + { + private AnalyzeModelStatisticsEventHandler _handler => (AnalyzeModelStatisticsEventHandler)Handler; + + public override string CommandName => "analyze_model_statistics"; + + public AnalyzeModelStatisticsCommand(UIApplication uiApp) + : base(new AnalyzeModelStatisticsEventHandler(), uiApp) + { + } + + public override object Execute(JObject parameters, string requestId) + { + try + { + // Parse parameters + bool includeDetailedTypes = parameters?["includeDetailedTypes"]?.Value() ?? true; + + // Set parameters + _handler.SetParameters(includeDetailedTypes); + + // Execute and wait + if (RaiseAndWaitForCompletion(120000)) // 120 second timeout for large models + { + return _handler.ResultInfo; + } + else + { + throw new TimeoutException("Model statistics analysis timed out"); + } + } + catch (Exception ex) + { + throw new Exception($"Failed to analyze model statistics: {ex.Message}"); + } + } + } +} diff --git a/revit-mcp-commandset/Commands/DataExtraction/ExportRoomDataCommand.cs b/revit-mcp-commandset/Commands/DataExtraction/ExportRoomDataCommand.cs new file mode 100644 index 0000000..c1282d9 --- /dev/null +++ b/revit-mcp-commandset/Commands/DataExtraction/ExportRoomDataCommand.cs @@ -0,0 +1,46 @@ +using Autodesk.Revit.UI; +using Newtonsoft.Json.Linq; +using RevitMCPCommandSet.Services.DataExtraction; +using RevitMCPSDK.API.Base; + +namespace RevitMCPCommandSet.Commands.DataExtraction +{ + public class ExportRoomDataCommand : ExternalEventCommandBase + { + private ExportRoomDataEventHandler _handler => (ExportRoomDataEventHandler)Handler; + + public override string CommandName => "export_room_data"; + + public ExportRoomDataCommand(UIApplication uiApp) + : base(new ExportRoomDataEventHandler(), uiApp) + { + } + + public override object Execute(JObject parameters, string requestId) + { + try + { + // Parse optional parameters + bool includeUnplacedRooms = parameters?["includeUnplacedRooms"]?.Value() ?? false; + bool includeNotEnclosedRooms = parameters?["includeNotEnclosedRooms"]?.Value() ?? false; + + // Set parameters + _handler.SetParameters(includeUnplacedRooms, includeNotEnclosedRooms); + + // Execute and wait + if (RaiseAndWaitForCompletion(60000)) // 60 second timeout + { + return _handler.ResultInfo; + } + else + { + throw new TimeoutException("Export room data operation timed out"); + } + } + catch (Exception ex) + { + throw new Exception($"Failed to export room data: {ex.Message}"); + } + } + } +} diff --git a/revit-mcp-commandset/Commands/DataExtraction/GetMaterialQuantitiesCommand.cs b/revit-mcp-commandset/Commands/DataExtraction/GetMaterialQuantitiesCommand.cs new file mode 100644 index 0000000..65a003a --- /dev/null +++ b/revit-mcp-commandset/Commands/DataExtraction/GetMaterialQuantitiesCommand.cs @@ -0,0 +1,46 @@ +using Autodesk.Revit.UI; +using Newtonsoft.Json.Linq; +using RevitMCPCommandSet.Services.DataExtraction; +using RevitMCPSDK.API.Base; + +namespace RevitMCPCommandSet.Commands.DataExtraction +{ + public class GetMaterialQuantitiesCommand : ExternalEventCommandBase + { + private GetMaterialQuantitiesEventHandler _handler => (GetMaterialQuantitiesEventHandler)Handler; + + public override string CommandName => "get_material_quantities"; + + public GetMaterialQuantitiesCommand(UIApplication uiApp) + : base(new GetMaterialQuantitiesEventHandler(), uiApp) + { + } + + public override object Execute(JObject parameters, string requestId) + { + try + { + // Parse parameters + List categoryFilters = parameters?["categoryFilters"]?.ToObject>(); + bool selectedElementsOnly = parameters?["selectedElementsOnly"]?.Value() ?? false; + + // Set parameters + _handler.SetParameters(categoryFilters, selectedElementsOnly); + + // Execute and wait + if (RaiseAndWaitForCompletion(120000)) // 120 second timeout for large projects + { + return _handler.ResultInfo; + } + else + { + throw new TimeoutException("Material quantities calculation timed out"); + } + } + catch (Exception ex) + { + throw new Exception($"Failed to get material quantities: {ex.Message}"); + } + } + } +} diff --git a/revit-mcp-commandset/Models/DataExtraction/MaterialQuantityModel.cs b/revit-mcp-commandset/Models/DataExtraction/MaterialQuantityModel.cs new file mode 100644 index 0000000..354cdfc --- /dev/null +++ b/revit-mcp-commandset/Models/DataExtraction/MaterialQuantityModel.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +namespace RevitMCPCommandSet.Models.DataExtraction +{ + /// + /// Model for material quantity data + /// + public class MaterialQuantityModel + { + [JsonProperty("materialId")] + public long MaterialId { get; set; } + + [JsonProperty("materialName")] + public string MaterialName { get; set; } + + [JsonProperty("materialClass")] + public string MaterialClass { get; set; } + + [JsonProperty("area")] + public double Area { get; set; } // Square feet + + [JsonProperty("volume")] + public double Volume { get; set; } // Cubic feet + + [JsonProperty("elementCount")] + public int ElementCount { get; set; } + + [JsonProperty("elementIds")] + public List ElementIds { get; set; } = new List(); + } + + /// + /// Result container for material quantities + /// + public class GetMaterialQuantitiesResult + { + [JsonProperty("totalMaterials")] + public int TotalMaterials { get; set; } + + [JsonProperty("totalArea")] + public double TotalArea { get; set; } + + [JsonProperty("totalVolume")] + public double TotalVolume { get; set; } + + [JsonProperty("materials")] + public List Materials { get; set; } = new List(); + + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/revit-mcp-commandset/Models/DataExtraction/ModelStatisticsModel.cs b/revit-mcp-commandset/Models/DataExtraction/ModelStatisticsModel.cs new file mode 100644 index 0000000..a665614 --- /dev/null +++ b/revit-mcp-commandset/Models/DataExtraction/ModelStatisticsModel.cs @@ -0,0 +1,91 @@ +using Newtonsoft.Json; + +namespace RevitMCPCommandSet.Models.DataExtraction +{ + /// + /// Statistics for a category + /// + public class CategoryStatistics + { + [JsonProperty("categoryName")] + public string CategoryName { get; set; } + + [JsonProperty("elementCount")] + public int ElementCount { get; set; } + + [JsonProperty("typeCount")] + public int TypeCount { get; set; } + + [JsonProperty("familyCount")] + public int FamilyCount { get; set; } + + [JsonProperty("types")] + public List Types { get; set; } = new List(); + } + + /// + /// Statistics for a type + /// + public class TypeStatistics + { + [JsonProperty("typeName")] + public string TypeName { get; set; } + + [JsonProperty("familyName")] + public string FamilyName { get; set; } + + [JsonProperty("instanceCount")] + public int InstanceCount { get; set; } + } + + /// + /// Statistics by level + /// + public class LevelStatistics + { + [JsonProperty("levelName")] + public string LevelName { get; set; } + + [JsonProperty("elevation")] + public double Elevation { get; set; } + + [JsonProperty("elementCount")] + public int ElementCount { get; set; } + } + + /// + /// Result container for model statistics + /// + public class AnalyzeModelStatisticsResult + { + [JsonProperty("projectName")] + public string ProjectName { get; set; } + + [JsonProperty("totalElements")] + public int TotalElements { get; set; } + + [JsonProperty("totalTypes")] + public int TotalTypes { get; set; } + + [JsonProperty("totalFamilies")] + public int TotalFamilies { get; set; } + + [JsonProperty("totalViews")] + public int TotalViews { get; set; } + + [JsonProperty("totalSheets")] + public int TotalSheets { get; set; } + + [JsonProperty("categories")] + public List Categories { get; set; } = new List(); + + [JsonProperty("levels")] + public List Levels { get; set; } = new List(); + + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/revit-mcp-commandset/Models/DataExtraction/RoomDataModel.cs b/revit-mcp-commandset/Models/DataExtraction/RoomDataModel.cs new file mode 100644 index 0000000..9a79d7d --- /dev/null +++ b/revit-mcp-commandset/Models/DataExtraction/RoomDataModel.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; + +namespace RevitMCPCommandSet.Models.DataExtraction +{ + /// + /// Model for room data extraction + /// + public class RoomDataModel + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("uniqueId")] + public string UniqueId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } + + [JsonProperty("level")] + public string Level { get; set; } + + [JsonProperty("area")] + public double Area { get; set; } // Square feet + + [JsonProperty("volume")] + public double Volume { get; set; } // Cubic feet + + [JsonProperty("perimeter")] + public double Perimeter { get; set; } // Feet + + [JsonProperty("unboundedHeight")] + public double UnboundedHeight { get; set; } // Feet + + [JsonProperty("department")] + public string Department { get; set; } + + [JsonProperty("comments")] + public string Comments { get; set; } + + [JsonProperty("phase")] + public string Phase { get; set; } + + [JsonProperty("occupancy")] + public string Occupancy { get; set; } + } + + /// + /// Result container for room data export + /// + public class ExportRoomDataResult + { + [JsonProperty("totalRooms")] + public int TotalRooms { get; set; } + + [JsonProperty("totalArea")] + public double TotalArea { get; set; } + + [JsonProperty("rooms")] + public List Rooms { get; set; } = new List(); + + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + } +} diff --git a/revit-mcp-commandset/RevitMCPCommandSet.csproj b/revit-mcp-commandset/RevitMCPCommandSet.csproj index 62c1fa2..048a979 100644 --- a/revit-mcp-commandset/RevitMCPCommandSet.csproj +++ b/revit-mcp-commandset/RevitMCPCommandSet.csproj @@ -56,7 +56,7 @@ - - + + \ No newline at end of file diff --git a/revit-mcp-commandset/Services/DataExtraction/AnalyzeModelStatisticsEventHandler.cs b/revit-mcp-commandset/Services/DataExtraction/AnalyzeModelStatisticsEventHandler.cs new file mode 100644 index 0000000..c2280cb --- /dev/null +++ b/revit-mcp-commandset/Services/DataExtraction/AnalyzeModelStatisticsEventHandler.cs @@ -0,0 +1,178 @@ +using Autodesk.Revit.DB; +using Autodesk.Revit.UI; +using RevitMCPCommandSet.Models.DataExtraction; +using RevitMCPSDK.API.Interfaces; + +namespace RevitMCPCommandSet.Services.DataExtraction +{ + public class AnalyzeModelStatisticsEventHandler : IExternalEventHandler, IWaitableExternalEventHandler + { + private bool _includeDetailedTypes; + + public AnalyzeModelStatisticsResult ResultInfo { get; private set; } + public bool TaskCompleted { get; private set; } + private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false); + + public void SetParameters(bool includeDetailedTypes = true) + { + _includeDetailedTypes = includeDetailedTypes; + TaskCompleted = false; + _resetEvent.Reset(); + } + + public bool WaitForCompletion(int timeoutMilliseconds = 10000) + { + return _resetEvent.WaitOne(timeoutMilliseconds); + } + + public void Execute(UIApplication app) + { + try + { + var doc = app.ActiveUIDocument.Document; + + // Get project name + string projectName = doc.Title; + + // Count total elements + int totalElements = new FilteredElementCollector(doc) + .WhereElementIsNotElementType() + .GetElementCount(); + + // Count total types + int totalTypes = new FilteredElementCollector(doc) + .WhereElementIsElementType() + .GetElementCount(); + + // Count views + int totalViews = new FilteredElementCollector(doc) + .OfClass(typeof(View)) + .Where(v => !(v as View).IsTemplate) + .Count(); + + // Count sheets + int totalSheets = new FilteredElementCollector(doc) + .OfClass(typeof(ViewSheet)) + .GetElementCount(); + + // Analyze by category + var categoryStats = new Dictionary(); + var familyNames = new HashSet(); + + var elements = new FilteredElementCollector(doc) + .WhereElementIsNotElementType() + .ToElements(); + + foreach (Element elem in elements) + { + if (elem.Category == null) continue; + + string catName = elem.Category.Name; + + if (!categoryStats.ContainsKey(catName)) + { + categoryStats[catName] = new CategoryStatistics + { + CategoryName = catName + }; + } + + categoryStats[catName].ElementCount++; + + // Track type information + if (elem is FamilyInstance fi) + { + string familyName = fi.Symbol?.Family?.Name; + string typeName = fi.Symbol?.Name; + + if (!string.IsNullOrEmpty(familyName)) + { + familyNames.Add(familyName); + } + + if (_includeDetailedTypes && !string.IsNullOrEmpty(typeName)) + { + var existingType = categoryStats[catName].Types + .FirstOrDefault(t => t.TypeName == typeName && t.FamilyName == familyName); + + if (existingType != null) + { + existingType.InstanceCount++; + } + else + { + categoryStats[catName].Types.Add(new TypeStatistics + { + TypeName = typeName, + FamilyName = familyName, + InstanceCount = 1 + }); + } + } + } + } + + // Calculate type and family counts per category + foreach (var stat in categoryStats.Values) + { + stat.TypeCount = stat.Types.Select(t => t.TypeName).Distinct().Count(); + stat.FamilyCount = stat.Types.Select(t => t.FamilyName).Distinct().Count(); + } + + // Analyze by level + var levelStats = new List(); + var levels = new FilteredElementCollector(doc) + .OfClass(typeof(Level)) + .Cast() + .OrderBy(l => l.Elevation); + + foreach (Level level in levels) + { + int elementCount = new FilteredElementCollector(doc) + .WhereElementIsNotElementType() + .Where(e => e.LevelId == level.Id) + .Count(); + + levelStats.Add(new LevelStatistics + { + LevelName = level.Name, + Elevation = level.Elevation, + ElementCount = elementCount + }); + } + + ResultInfo = new AnalyzeModelStatisticsResult + { + ProjectName = projectName, + TotalElements = totalElements, + TotalTypes = totalTypes, + TotalFamilies = familyNames.Count, + TotalViews = totalViews, + TotalSheets = totalSheets, + Categories = categoryStats.Values.OrderByDescending(c => c.ElementCount).ToList(), + Levels = levelStats, + Success = true, + Message = $"Successfully analyzed model with {totalElements} elements across {categoryStats.Count} categories" + }; + } + catch (Exception ex) + { + ResultInfo = new AnalyzeModelStatisticsResult + { + Success = false, + Message = $"Error analyzing model statistics: {ex.Message}" + }; + } + finally + { + TaskCompleted = true; + _resetEvent.Set(); + } + } + + public string GetName() + { + return "Analyze Model Statistics"; + } + } +} diff --git a/revit-mcp-commandset/Services/DataExtraction/ExportRoomDataEventHandler.cs b/revit-mcp-commandset/Services/DataExtraction/ExportRoomDataEventHandler.cs new file mode 100644 index 0000000..0be900c --- /dev/null +++ b/revit-mcp-commandset/Services/DataExtraction/ExportRoomDataEventHandler.cs @@ -0,0 +1,109 @@ +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.Architecture; +using Autodesk.Revit.UI; +using RevitMCPCommandSet.Models.DataExtraction; +using RevitMCPSDK.API.Interfaces; + +namespace RevitMCPCommandSet.Services.DataExtraction +{ + public class ExportRoomDataEventHandler : IExternalEventHandler, IWaitableExternalEventHandler + { + private bool _includeUnplacedRooms; + private bool _includeNotEnclosedRooms; + + public ExportRoomDataResult ResultInfo { get; private set; } + public bool TaskCompleted { get; private set; } + private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false); + + public void SetParameters(bool includeUnplacedRooms = false, bool includeNotEnclosedRooms = false) + { + _includeUnplacedRooms = includeUnplacedRooms; + _includeNotEnclosedRooms = includeNotEnclosedRooms; + TaskCompleted = false; + _resetEvent.Reset(); + } + + public bool WaitForCompletion(int timeoutMilliseconds = 10000) + { + return _resetEvent.WaitOne(timeoutMilliseconds); + } + + public void Execute(UIApplication app) + { + try + { + var doc = app.ActiveUIDocument.Document; + var rooms = new List(); + double totalArea = 0; + + // Collect all rooms in the project + var roomCollector = new FilteredElementCollector(doc) + .OfCategory(BuiltInCategory.OST_Rooms) + .WhereElementIsNotElementType() + .Cast(); + + foreach (Room room in roomCollector) + { + // Skip unplaced rooms if not included + if (!_includeUnplacedRooms && room.Area == 0) + continue; + + // Skip not enclosed rooms if not included + if (!_includeNotEnclosedRooms && room.Area == 0) + continue; + + var roomData = new RoomDataModel + { +#if REVIT2024_OR_GREATER + Id = room.Id.Value, +#else + Id = room.Id.IntegerValue, +#endif + UniqueId = room.UniqueId, + Name = room.get_Parameter(BuiltInParameter.ROOM_NAME)?.AsString() ?? "", + Number = room.Number ?? "", + Level = room.Level?.Name ?? "No Level", + Area = room.Area, // Already in square feet + Volume = room.Volume, // Already in cubic feet + Perimeter = room.Perimeter, // Already in feet + UnboundedHeight = room.UnboundedHeight, // Already in feet + Department = room.get_Parameter(BuiltInParameter.ROOM_DEPARTMENT)?.AsString() ?? "", + Comments = room.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS)?.AsString() ?? "", + Phase = doc.GetElement(room.get_Parameter(BuiltInParameter.ROOM_PHASE)?.AsElementId())?.Name ?? "", + Occupancy = room.get_Parameter(BuiltInParameter.ROOM_OCCUPANCY)?.AsString() ?? "" + }; + + rooms.Add(roomData); + totalArea += room.Area; + } + + ResultInfo = new ExportRoomDataResult + { + TotalRooms = rooms.Count, + TotalArea = totalArea, + Rooms = rooms, + Success = true, + Message = $"Successfully exported {rooms.Count} rooms" + }; + } + catch (Exception ex) + { + ResultInfo = new ExportRoomDataResult + { + Success = false, + Message = $"Error exporting room data: {ex.Message}" + }; + } + finally + { + TaskCompleted = true; + _resetEvent.Set(); + } + } + + public string GetName() + { + return "Export Room Data"; + } + } +} diff --git a/revit-mcp-commandset/Services/DataExtraction/GetMaterialQuantitiesEventHandler.cs b/revit-mcp-commandset/Services/DataExtraction/GetMaterialQuantitiesEventHandler.cs new file mode 100644 index 0000000..ee626d1 --- /dev/null +++ b/revit-mcp-commandset/Services/DataExtraction/GetMaterialQuantitiesEventHandler.cs @@ -0,0 +1,154 @@ +using Autodesk.Revit.DB; +using Autodesk.Revit.UI; +using RevitMCPCommandSet.Models.DataExtraction; +using RevitMCPSDK.API.Interfaces; + +namespace RevitMCPCommandSet.Services.DataExtraction +{ + public class GetMaterialQuantitiesEventHandler : IExternalEventHandler, IWaitableExternalEventHandler + { + private List _categoryFilters; + private bool _selectedElementsOnly; + + public GetMaterialQuantitiesResult ResultInfo { get; private set; } + public bool TaskCompleted { get; private set; } + private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false); + + public void SetParameters(List categoryFilters = null, bool selectedElementsOnly = false) + { + _categoryFilters = categoryFilters; + _selectedElementsOnly = selectedElementsOnly; + TaskCompleted = false; + _resetEvent.Reset(); + } + + public bool WaitForCompletion(int timeoutMilliseconds = 10000) + { + return _resetEvent.WaitOne(timeoutMilliseconds); + } + + public void Execute(UIApplication app) + { + try + { + var uiDoc = app.ActiveUIDocument; + var doc = uiDoc.Document; + + // Dictionary to accumulate material quantities + var materialData = new Dictionary(); + + // Get elements to analyze + ICollection elements; + if (_selectedElementsOnly) + { + var selectedIds = uiDoc.Selection.GetElementIds(); + elements = selectedIds.Select(id => doc.GetElement(id)).Where(e => e != null).ToList(); + } + else + { + var collector = new FilteredElementCollector(doc) + .WhereElementIsNotElementType(); + + // Apply category filters if specified + if (_categoryFilters != null && _categoryFilters.Count > 0) + { + var builtInCategories = new List(); + foreach (var catName in _categoryFilters) + { + if (Enum.TryParse(catName, out BuiltInCategory cat)) + { + builtInCategories.Add(cat); + } + } + if (builtInCategories.Count > 0) + { + var filter = new ElementMulticategoryFilter(builtInCategories); + collector = collector.WherePasses(filter); + } + } + + elements = collector.ToElements(); + } + + // Process each element + foreach (Element element in elements) + { + // Get all material ids in the element + var materialIds = element.GetMaterialIds(false); + + foreach (ElementId matId in materialIds) + { + Material material = doc.GetElement(matId) as Material; + if (material == null) continue; + + // Initialize material data if not exists + if (!materialData.ContainsKey(matId)) + { + materialData[matId] = new MaterialQuantityModel + { +#if REVIT2024_OR_GREATER + MaterialId = matId.Value, +#else + MaterialId = matId.IntegerValue, +#endif + MaterialName = material.Name, + MaterialClass = material.MaterialClass + }; + } + + // Get material area and volume for this element + double area = element.GetMaterialArea(matId, false); + double volume = element.GetMaterialVolume(matId); + + materialData[matId].Area += area; + materialData[matId].Volume += volume; + +#if REVIT2024_OR_GREATER + if (!materialData[matId].ElementIds.Contains(element.Id.Value)) + { + materialData[matId].ElementIds.Add(element.Id.Value); +#else + if (!materialData[matId].ElementIds.Contains(element.Id.IntegerValue)) + { + materialData[matId].ElementIds.Add(element.Id.IntegerValue); +#endif + materialData[matId].ElementCount++; + } + } + } + + var materials = materialData.Values.ToList(); + double totalArea = materials.Sum(m => m.Area); + double totalVolume = materials.Sum(m => m.Volume); + + ResultInfo = new GetMaterialQuantitiesResult + { + TotalMaterials = materials.Count, + TotalArea = totalArea, + TotalVolume = totalVolume, + Materials = materials, + Success = true, + Message = $"Successfully calculated quantities for {materials.Count} materials" + }; + } + catch (Exception ex) + { + ResultInfo = new GetMaterialQuantitiesResult + { + Success = false, + Message = $"Error calculating material quantities: {ex.Message}" + }; + } + finally + { + TaskCompleted = true; + _resetEvent.Set(); + } + } + + public string GetName() + { + return "Get Material Quantities"; + } + } +}