From 660c3bcd6f99c85aab3717c1451631d7658a7559 Mon Sep 17 00:00:00 2001 From: sladro Date: Sun, 1 Mar 2026 08:04:54 +0800 Subject: [PATCH] feat: implement file closing and custom category deletion functionality --- Controllers/FileController.cs | 61 ++++++++++++++++++ Controllers/ShellController.cs | 83 +++++++++++++++++++++++++ Models/FileModels.cs | 21 +++++++ Models/ShellModels.cs | 36 +++++++++++ Services/DocumentService.cs | 75 +++++++++++++++++++++++ Services/RevitService.cs | 109 +++++++++++++++++++++++++++++++++ Services/ShellAnalyzer.cs | 37 +++++------ Services/ShellOptimizer.cs | 84 +++++++++++++++++++++++++ 8 files changed, 483 insertions(+), 23 deletions(-) diff --git a/Controllers/FileController.cs b/Controllers/FileController.cs index 33cc80d..0fbb0dd 100644 --- a/Controllers/FileController.cs +++ b/Controllers/FileController.cs @@ -188,5 +188,66 @@ namespace RevitHttpControl.Controllers return Request.CreateResponse(HttpStatusCode.InternalServerError, errorResponse); } } + + /// + /// 关闭当前打开文件 + /// + /// 关闭文件响应 + [HttpPost] + [Route("close")] + public HttpResponseMessage CloseFile() + { + try + { + var result = DocumentService.CloseCurrentDocument(); + + var successResponse = new ApiResponse + { + Success = true, + Code = 200, + Message = "文件关闭成功", + Data = result + }; + + return Request.CreateResponse(HttpStatusCode.OK, successResponse); + } + catch (InvalidOperationException ex) + { + var conflictResponse = new ApiResponse + { + Success = false, + Code = 409, + Message = ex.Message, + Data = new { error = "NO_DOCUMENT_OPEN" } + }; + return Request.CreateResponse(HttpStatusCode.Conflict, conflictResponse); + } + catch (TimeoutException ex) + { + var timeoutResponse = new ApiResponse + { + Success = false, + Code = 408, + Message = ex.Message, + Data = new { error = "OPERATION_TIMEOUT" } + }; + return Request.CreateResponse(HttpStatusCode.RequestTimeout, timeoutResponse); + } + catch (Exception ex) + { + var errorResponse = new ApiResponse + { + Success = false, + Code = 500, + Message = "关闭文件时发生错误", + Data = new + { + error = "INTERNAL_ERROR", + errorDescription = ex.Message + } + }; + return Request.CreateResponse(HttpStatusCode.InternalServerError, errorResponse); + } + } } } diff --git a/Controllers/ShellController.cs b/Controllers/ShellController.cs index 6b17506..5517102 100644 --- a/Controllers/ShellController.cs +++ b/Controllers/ShellController.cs @@ -254,6 +254,89 @@ namespace RevitHttpControl.Controllers } } + /// + /// 按类别自定义执行删除(同步) + /// + /// 自定义删除请求 + /// 执行结果 + [HttpPost] + [Route("execute/custom")] + public HttpResponseMessage ExecuteShellCustom([FromBody] ShellCustomExecuteRequest request) + { + try + { + if (request == null) + { + return this.CreateErrorResponse( + ErrorCodes.INVALID_REQUEST_PARAMETERS, + "请求参数不能为空", + HttpStatusCode.BadRequest + ); + } + + if (request.CategoryIds == null || request.CategoryIds.Count == 0) + { + return this.CreateErrorResponse( + ErrorCodes.INVALID_REQUEST_PARAMETERS, + "CategoryIds 不能为空,且至少包含一个类别ID", + HttpStatusCode.BadRequest + ); + } + + var result = RevitService.ExecuteShellOptimizationByCategories(request.CategoryIds, request.BackupOriginal); + return this.CreateSuccessResponse(result, "自定义类别删除执行完成"); + } + catch (InvalidOperationException ex) + { + return this.CreateErrorResponse( + ErrorCodes.NO_DOCUMENT_OPEN, + ex.Message, + HttpStatusCode.BadRequest + ); + } + catch (ArgumentException ex) + { + return this.CreateErrorResponse( + ErrorCodes.INVALID_REQUEST_PARAMETERS, + ex.Message, + HttpStatusCode.BadRequest + ); + } + catch (TimeoutException ex) + { + return this.CreateErrorResponse( + ErrorCodes.OPERATION_TIMEOUT, + ex.Message, + HttpStatusCode.RequestTimeout + ); + } + catch (UnauthorizedAccessException ex) + { + return this.CreateErrorResponse( + ErrorCodes.FILE_WRITE_PERMISSION_DENIED, + "文件写入权限不足:" + ex.Message, + HttpStatusCode.Forbidden + ); + } + catch (System.IO.IOException ex) + { + return this.CreateErrorResponse( + ErrorCodes.FILE_IO_ERROR, + "文件操作失败:" + ex.Message, + HttpStatusCode.InternalServerError + ); + } + catch (Exception ex) + { + return this.CreateErrorResponse( + ErrorCodes.REVIT_API_ERROR, + "自定义类别删除执行失败", + HttpStatusCode.InternalServerError, + ex.Message + ); + } + } + /// /// 获取薄壳优化预设配置 /// diff --git a/Models/FileModels.cs b/Models/FileModels.cs index b58479c..504b715 100644 --- a/Models/FileModels.cs +++ b/Models/FileModels.cs @@ -38,4 +38,25 @@ namespace RevitHttpControl.Models /// public string FilePath { get; set; } } + + /// + /// 关闭文件响应 + /// + public class CloseFileResponse + { + /// + /// 操作结果 + /// + public string Result { get; set; } + + /// + /// 关闭前的文件名 + /// + public string FileName { get; set; } + + /// + /// 关闭前的文件路径 + /// + public string FilePath { get; set; } + } } diff --git a/Models/ShellModels.cs b/Models/ShellModels.cs index 9865d11..4082ee5 100644 --- a/Models/ShellModels.cs +++ b/Models/ShellModels.cs @@ -49,6 +49,22 @@ namespace RevitHttpControl.Models public bool BackupOriginal { get; set; } = true; } + /// + /// 按类别自定义删除请求 + /// + public class ShellCustomExecuteRequest + { + /// + /// 要删除的类别ID列表(BuiltInCategory 的 int 值) + /// + public List CategoryIds { get; set; } = new List(); + + /// + /// 是否备份原文件 + /// + public bool BackupOriginal { get; set; } = true; + } + /// /// 薄壳执行响应 /// @@ -84,6 +100,16 @@ namespace RevitHttpControl.Models /// 预估文件大小减少百分比 /// public string EstimatedReduction { get; set; } + + /// + /// 当前文件大小(格式化显示) + /// + public string OriginalSize { get; set; } + + /// + /// 预估优化后文件大小(格式化显示) + /// + public string EstimatedOptimizedSize { get; set; } } /// @@ -91,6 +117,11 @@ namespace RevitHttpControl.Models /// public class CategoryStatistics { + /// + /// 分类ID(BuiltInCategory 的 int 值) + /// + public int CategoryId { get; set; } + /// /// 分类名称 /// @@ -205,6 +236,11 @@ namespace RevitHttpControl.Models /// public int ElementId { get; set; } + /// + /// 构件分类ID(BuiltInCategory 的 int 值) + /// + public int CategoryId { get; set; } + /// /// 构件分类名称 /// diff --git a/Services/DocumentService.cs b/Services/DocumentService.cs index ee4d29c..cf3e178 100644 --- a/Services/DocumentService.cs +++ b/Services/DocumentService.cs @@ -84,6 +84,81 @@ namespace RevitHttpControl.Services } } + /// + /// 关闭当前活动文档 + /// + /// 关闭文件响应 + public static CloseFileResponse CloseCurrentDocument() + { + try + { + var completed = false; + Exception capturedException = null; + CloseFileResponse response = null; + + App.Instance.EnqueueCommand(uiApp => + { + try + { + var activeDoc = uiApp.ActiveUIDocument?.Document; + if (activeDoc == null) + { + throw new InvalidOperationException("没有打开的文档"); + } + + var fileName = string.IsNullOrEmpty(activeDoc.PathName) + ? activeDoc.Title + : Path.GetFileName(activeDoc.PathName); + var filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : activeDoc.PathName; + + activeDoc.Close(false); + + response = new CloseFileResponse + { + Result = "文件关闭成功", + FileName = fileName, + FilePath = filePath + }; + } + catch (Exception ex) + { + capturedException = ex; + } + finally + { + completed = true; + } + }); + + // 等待命令执行完成(最多等待10秒) + var timeout = DateTime.Now.AddSeconds(10); + while (!completed && DateTime.Now < timeout) + { + System.Threading.Thread.Sleep(100); + } + + if (!completed) + throw new TimeoutException("关闭文件操作超时"); + + if (capturedException != null) + throw capturedException; + + return response; + } + catch (TimeoutException) + { + throw; + } + catch (InvalidOperationException) + { + throw; + } + catch (Exception ex) + { + throw new InvalidOperationException($"关闭文件失败: {ex.Message}", ex); + } + } + /// /// 异步打开文档文件 /// diff --git a/Services/RevitService.cs b/Services/RevitService.cs index 6b55235..e395894 100644 --- a/Services/RevitService.cs +++ b/Services/RevitService.cs @@ -609,6 +609,61 @@ namespace RevitHttpControl.Services } } + /// + /// 获取文档文件大小(字节) + /// + private static long GetDocumentFileSize(Document doc) + { + try + { + if (doc == null) + return 0; + + if (!string.IsNullOrEmpty(doc.PathName) && File.Exists(doc.PathName)) + return new FileInfo(doc.PathName).Length; + + if (doc.IsWorkshared) + { + var centralPath = doc.GetWorksharingCentralModelPath(); + var centralPathString = centralPath?.ToString(); + if (!string.IsNullOrEmpty(centralPathString) && File.Exists(centralPathString)) + return new FileInfo(centralPathString).Length; + } + } + catch + { + // 忽略文件大小获取异常,保持分析结果可用 + } + + return 0; + } + + /// + /// 基于真实文件大小和可删除比例填充动态估算信息 + /// + private static void ApplyDynamicSizeEstimate(Document doc, ShellAnalyzeResponse response) + { + if (doc == null || response?.Analysis == null) + return; + + var summary = response.Analysis; + var originalSize = GetDocumentFileSize(doc); + if (originalSize <= 0) + return; + + var removeRatio = summary.TotalElements > 0 + ? Math.Max(0.0, Math.Min(1.0, summary.RemoveElements / (double)summary.TotalElements)) + : 0.0; + + var estimatedOptimizedBytes = (long)Math.Round(originalSize * (1.0 - removeRatio)); + if (estimatedOptimizedBytes < 0) + estimatedOptimizedBytes = 0; + + summary.OriginalSize = FormatFileSize(originalSize); + summary.EstimatedOptimizedSize = FormatFileSize(estimatedOptimizedBytes); + summary.EstimatedReduction = $"{(removeRatio * 100.0):F1}%"; + } + /// /// 分析薄壳优化方案 /// @@ -632,6 +687,7 @@ namespace RevitHttpControl.Services var analyzer = new ShellAnalyzer(); result = analyzer.AnalyzeModel(doc, mode); + ApplyDynamicSizeEstimate(doc, result); } catch (Exception ex) { @@ -720,6 +776,59 @@ namespace RevitHttpControl.Services } } + /// + /// 按类别执行自定义删除(同步) + /// + public static ShellOptimizeResult ExecuteShellOptimizationByCategories(IEnumerable categoryIds, bool backupOriginal = true) + { + ShellOptimizeResult result = null; + Exception capturedException = null; + bool completed = false; + + try + { + App.Instance.EnqueueCommand(uiApp => + { + try + { + var doc = uiApp.ActiveUIDocument?.Document; + if (doc == null) + throw new InvalidOperationException("没有打开的Revit文档"); + + var optimizer = new ShellOptimizer(); + result = optimizer.ExecuteOptimizationByCategories(doc, categoryIds, backupOriginal); + } + catch (Exception ex) + { + capturedException = ex; + } + finally + { + completed = true; + } + }); + + // 等待命令执行完成(最多等待5分钟) + var timeout = DateTime.Now.AddMinutes(5); + while (!completed && DateTime.Now < timeout) + { + System.Threading.Thread.Sleep(200); + } + + if (capturedException != null) + throw capturedException; + + if (!completed) + throw new TimeoutException("自定义类别删除执行超时"); + + return result; + } + catch (Exception ex) + { + throw new InvalidOperationException($"自定义类别删除执行失败: {ex.Message}", ex); + } + } + /// /// 执行薄壳优化(异步) /// diff --git a/Services/ShellAnalyzer.cs b/Services/ShellAnalyzer.cs index 81553ac..bdb4932 100644 --- a/Services/ShellAnalyzer.cs +++ b/Services/ShellAnalyzer.cs @@ -40,7 +40,7 @@ namespace RevitHttpControl.Services } // 统计结果 - var summary = CreateAnalysisSummary(analysisResults, mode); + var summary = CreateAnalysisSummary(analysisResults); var categories = CreateCategoryStatistics(analysisResults); return new ShellAnalyzeResponse @@ -122,6 +122,7 @@ namespace RevitHttpControl.Services /// private ElementAnalysisResult AnalyzeElement(Element element, ShellOptimizeMode mode) { + var categoryId = element.Category?.Id?.IntegerValue ?? 0; var categoryName = element.Category?.Name ?? "未知类别"; var action = DetermineElementAction(element, mode); var reason = GetActionReason(element, action, mode); @@ -129,6 +130,7 @@ namespace RevitHttpControl.Services return new ElementAnalysisResult { ElementId = element.Id.IntegerValue, + CategoryId = categoryId, CategoryName = categoryName, Action = action, Reason = reason @@ -416,36 +418,24 @@ namespace RevitHttpControl.Services /// /// 创建分析摘要 /// - private ShellAnalysisSummary CreateAnalysisSummary(List results, ShellOptimizeMode mode) + private ShellAnalysisSummary CreateAnalysisSummary(List results) { var total = results.Count; var keep = results.Count(r => r.Action == ElementAction.Keep); var remove = results.Count(r => r.Action == ElementAction.Remove); - // 根据模式估算文件大小减少百分比 - string estimatedReduction; - switch (mode) - { - case ShellOptimizeMode.Conservative: - estimatedReduction = "30%"; - break; - case ShellOptimizeMode.Standard: - estimatedReduction = "65%"; - break; - case ShellOptimizeMode.Aggressive: - estimatedReduction = "80%"; - break; - default: - estimatedReduction = "50%"; - break; - } + var estimatedReduction = total > 0 + ? $"{(remove * 100.0 / total):F1}%" + : "0%"; return new ShellAnalysisSummary { TotalElements = total, KeepElements = keep, RemoveElements = remove, - EstimatedReduction = estimatedReduction + EstimatedReduction = estimatedReduction, + OriginalSize = "未知", + EstimatedOptimizedSize = "未知" }; } @@ -454,10 +444,10 @@ namespace RevitHttpControl.Services /// private List CreateCategoryStatistics(List results) { - var categoryGroups = results.GroupBy(r => r.CategoryName); + var categoryGroups = results.GroupBy(r => new { r.CategoryId, r.CategoryName }); var statistics = new List(); - foreach (var group in categoryGroups.OrderBy(g => g.Key)) + foreach (var group in categoryGroups.OrderBy(g => g.Key.CategoryName)) { var total = group.Count(); var keep = group.Count(r => r.Action == ElementAction.Keep); @@ -465,7 +455,8 @@ namespace RevitHttpControl.Services statistics.Add(new CategoryStatistics { - Name = group.Key, + CategoryId = group.Key.CategoryId, + Name = group.Key.CategoryName, Total = total, Keep = keep, Remove = remove diff --git a/Services/ShellOptimizer.cs b/Services/ShellOptimizer.cs index ff68f52..2fa12f6 100644 --- a/Services/ShellOptimizer.cs +++ b/Services/ShellOptimizer.cs @@ -82,6 +82,66 @@ namespace RevitHttpControl.Services } } + /// + /// 按类别执行自定义删除 + /// + public ShellOptimizeResult ExecuteOptimizationByCategories(Document doc, IEnumerable categoryIds, bool backupOriginal = true) + { + if (doc == null) + throw new ArgumentNullException(nameof(doc)); + if (categoryIds == null) + throw new ArgumentNullException(nameof(categoryIds)); + + var categorySet = new HashSet(categoryIds); + if (categorySet.Count == 0) + throw new ArgumentException("至少需要指定一个要删除的类别", nameof(categoryIds)); + + var stopwatch = Stopwatch.StartNew(); + var result = new ShellOptimizeResult(); + + try + { + var originalSize = GetDocumentFileSize(doc); + result.OriginalSize = FormatFileSize(originalSize); + + var elementsToDelete = GetElementsToDeleteByCategories(doc, categorySet); + + if (backupOriginal) + { + result.BackupPath = CreateBackup(doc); + } + + // 自定义删除沿用标准安全检查逻辑(非 EnvelopeOnly) + var deletedCount = DeleteElements(doc, elementsToDelete, ShellOptimizeMode.Standard); + result.RemovedCount = deletedCount; + + SaveDocument(doc); + var optimizedSize = GetDocumentFileSize(doc); + result.OptimizedSize = FormatFileSize(optimizedSize); + + if (originalSize > 0) + { + var reduction = ((double)(originalSize - optimizedSize) / originalSize) * 100; + result.Reduction = $"{reduction:F1}%"; + } + else + { + result.Reduction = "0%"; + } + + stopwatch.Stop(); + result.ProcessingTimeSeconds = stopwatch.Elapsed.TotalSeconds; + + return result; + } + catch (Exception ex) + { + stopwatch.Stop(); + result.ProcessingTimeSeconds = stopwatch.Elapsed.TotalSeconds; + throw new InvalidOperationException($"自定义类别删除执行失败: {ex.Message}", ex); + } + } + /// /// 根据分析结果获取需要删除的构件 /// @@ -106,6 +166,30 @@ namespace RevitHttpControl.Services return elementsToDelete; } + /// + /// 按类别获取待删除构件 + /// + private List GetElementsToDeleteByCategories(Document doc, HashSet categoryIds) + { + var elementsToDelete = new List(); + + var allElements = new FilteredElementCollector(doc) + .WhereElementIsNotElementType() + .WhereElementIsViewIndependent() + .ToElements(); + + foreach (var element in allElements) + { + var categoryId = element?.Category?.Id?.IntegerValue; + if (categoryId.HasValue && categoryIds.Contains(categoryId.Value)) + { + elementsToDelete.Add(element.Id); + } + } + + return elementsToDelete; + } + /// /// 批量删除构件 ///