TellmeRevitPluging/Services/ShellOptimizer.cs

650 lines
23 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Autodesk.Revit.DB;
using RevitHttpControl.Models;
namespace RevitHttpControl.Services
{
/// <summary>
/// 薄壳优化执行服务
/// </summary>
public class ShellOptimizer
{
private readonly ShellAnalyzer _analyzer;
public ShellOptimizer()
{
_analyzer = new ShellAnalyzer();
}
/// <summary>
/// 执行薄壳优化
/// </summary>
public ShellOptimizeResult ExecuteOptimization(Document doc, ShellOptimizeMode mode, bool backupOriginal = true)
{
if (doc == null)
throw new ArgumentNullException(nameof(doc));
var stopwatch = Stopwatch.StartNew();
var result = new ShellOptimizeResult();
try
{
// 1. 获取原始文件大小
var originalSize = GetDocumentFileSize(doc);
result.OriginalSize = FormatFileSize(originalSize);
// 2. 分析模型获取删除列表
_analyzer.AnalyzeModel(doc, mode);
var elementsToDelete = GetElementsToDelete(doc, mode);
// 3. 创建备份(如果需要)
if (backupOriginal)
{
result.BackupPath = CreateBackup(doc);
}
// 4. 执行删除操作
var deletedCount = DeleteElements(doc, elementsToDelete, mode);
var purgedCount = PurgeUnusedFamilyData(doc);
result.RemovedCount = deletedCount + purgedCount;
// 5. 保存文档并计算优化后大小
SaveDocument(doc);
var optimizedSize = GetDocumentFileSize(doc);
result.OptimizedSize = FormatFileSize(optimizedSize);
// 6. 计算减少百分比
if (originalSize > 0)
{
var reduction = ((double)(originalSize - optimizedSize) / originalSize) * 100;
result.Reduction = $"{reduction:F1}%";
}
else
{
result.Reduction = "0%";
}
stopwatch.Stop();
result.ProcessingTimeSeconds = (int)Math.Round(stopwatch.Elapsed.TotalSeconds, MidpointRounding.AwayFromZero);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
result.ProcessingTimeSeconds = (int)Math.Round(stopwatch.Elapsed.TotalSeconds, MidpointRounding.AwayFromZero);
// 如果有备份且操作失败,可以考虑恢复备份
throw new InvalidOperationException($"薄壳优化执行失败: {ex.Message}", ex);
}
}
/// <summary>
/// 按类别执行自定义删除
/// </summary>
public ShellOptimizeResult ExecuteOptimizationByCategories(Document doc, IEnumerable<int> categoryIds, bool backupOriginal = true)
{
if (doc == null)
throw new ArgumentNullException(nameof(doc));
if (categoryIds == null)
throw new ArgumentNullException(nameof(categoryIds));
var categorySet = new HashSet<int>(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);
}
// Reuse Standard safety checks for custom category deletion.
var deletedCount = DeleteElements(doc, elementsToDelete, ShellOptimizeMode.Standard);
var purgedCount = PurgeUnusedFamilyData(doc);
result.RemovedCount = deletedCount + purgedCount;
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 = (int)Math.Round(stopwatch.Elapsed.TotalSeconds, MidpointRounding.AwayFromZero);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
result.ProcessingTimeSeconds = (int)Math.Round(stopwatch.Elapsed.TotalSeconds, MidpointRounding.AwayFromZero);
throw new InvalidOperationException($"自定义类别删除执行失败: {ex.Message}", ex);
}
}
/// <summary>
/// 根据分析结果获取需要删除的构件
/// </summary>
private List<ElementId> GetElementsToDelete(Document doc, ShellOptimizeMode mode)
{
var elementsToDelete = new List<ElementId>();
// 获取所有需要删除的构件
var allElements = new FilteredElementCollector(doc)
.WhereElementIsNotElementType()
.WhereElementIsViewIndependent()
.ToElements();
foreach (var element in allElements)
{
if (_analyzer.GetElementAction(element, mode) == ElementAction.Remove)
{
elementsToDelete.Add(element.Id);
}
}
return elementsToDelete;
}
/// <summary>
/// 按类别获取待删除构件
/// </summary>
private List<ElementId> GetElementsToDeleteByCategories(Document doc, HashSet<int> categoryIds)
{
var elementsToDelete = new List<ElementId>();
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;
}
/// <summary>
/// 批量删除构件
/// </summary>
private int DeleteElements(Document doc, List<ElementId> elementIds, ShellOptimizeMode mode)
{
if (elementIds == null || elementIds.Count == 0)
return 0;
int deletedCount = 0;
const int batchSize = 100; // 批量处理大小
// 分批删除以提高性能和稳定性
for (int i = 0; i < elementIds.Count; i += batchSize)
{
var batch = elementIds.Skip(i).Take(batchSize).ToList();
deletedCount += DeleteElementBatch(doc, batch, mode);
}
return deletedCount;
}
/// <summary>
/// 删除一批构件
/// </summary>
private int DeleteElementBatch(Document doc, List<ElementId> elementIds, ShellOptimizeMode mode)
{
using (var transaction = new Transaction(doc, "薄壳优化删除构件"))
{
transaction.Start();
ApplyWarningSuppression(transaction);
try
{
// 过滤出可以删除的构件
var deletableIds = new List<ElementId>();
foreach (var id in elementIds)
{
var element = doc.GetElement(id);
if (element != null && CanDeleteElement(element, mode))
{
deletableIds.Add(id);
}
}
// 执行删除
if (deletableIds.Count > 0)
{
var deletedIds = doc.Delete(deletableIds);
transaction.Commit();
return deletedIds.Count;
}
else
{
transaction.RollBack();
return 0;
}
}
catch (Exception ex)
{
transaction.RollBack();
// 记录错误但继续处理其他构件
System.Diagnostics.Debug.WriteLine($"删除构件批次失败: {ex.Message}");
return 0;
}
}
}
/// <summary>
/// 检查构件是否可以安全删除
/// </summary>
private bool CanDeleteElement(Element element, ShellOptimizeMode mode)
{
try
{
// 检查构件是否被锁定
if (element.Pinned)
return false;
// 检查构件是否在工作集中被其他用户编辑
var worksetId = element.WorksetId;
if (worksetId != WorksetId.InvalidWorksetId)
{
var doc = element.Document;
// 仅对工作共享文档执行“他人占用”校验,避免本地/样例文件误判。
if (doc.IsWorkshared)
{
var worksetTable = doc.GetWorksetTable();
var workset = worksetTable.GetWorkset(worksetId);
var owner = workset?.Owner;
var currentUser = doc.Application?.Username;
if (workset != null &&
workset.IsOpen &&
!string.IsNullOrWhiteSpace(owner) &&
!string.Equals(owner, currentUser, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
// 检查是否有依赖关系(如主体构件)
// EnvelopeOnly: 允许删除“宿主”类元素如内墙由Revit级联删除其托管构件
if (mode != ShellOptimizeMode.EnvelopeOnly && HasCriticalDependencies(element))
return false;
return true;
}
catch
{
// 如果检查过程出错,为安全起见不删除
return false;
}
}
/// <summary>
/// 检查构件是否有关键依赖关系
/// </summary>
private bool HasCriticalDependencies(Element element)
{
try
{
// 对于墙体,检查是否有门窗等构件托管在上面
if (element is Wall wall)
{
var doc = wall.Document;
var collector = new FilteredElementCollector(doc);
var familyInstances = collector.OfClass(typeof(FamilyInstance)).ToElements();
// 检查是否有门窗托管在这面墙上
foreach (Element e in familyInstances)
{
var familyInstance = e as FamilyInstance;
if (familyInstance?.Host?.Id == wall.Id)
{
return true; // 有构件托管在这面墙上,不能删除
}
}
}
return false;
}
catch
{
return true; // 无法确定时,为安全起见认为有依赖
}
}
/// <summary>
/// 创建文件备份
/// </summary>
private string CreateBackup(Document doc)
{
try
{
var originalPath = doc.PathName;
if (string.IsNullOrEmpty(originalPath))
{
// 如果文档未保存,先保存
throw new InvalidOperationException("文档必须先保存才能创建备份");
}
var fileName = Path.GetFileNameWithoutExtension(originalPath);
var extension = Path.GetExtension(originalPath);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupFileName = $"{fileName}_backup_{timestamp}{extension}";
// 先尝试写入原目录;若无权限则回退到用户本地目录。
var sourceDirectory = Path.GetDirectoryName(originalPath);
if (!string.IsNullOrWhiteSpace(sourceDirectory))
{
var backupPathInSource = Path.Combine(sourceDirectory, backupFileName);
try
{
File.Copy(originalPath, backupPathInSource, false);
return backupPathInSource;
}
catch (UnauthorizedAccessException)
{
// Ignore and fallback.
}
catch (IOException ex) when (File.Exists(backupPathInSource))
{
throw new InvalidOperationException($"备份文件已存在: {backupPathInSource}", ex);
}
}
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var fallbackDirectory = Path.Combine(localAppData, "RevitHttpControl", "Backups");
Directory.CreateDirectory(fallbackDirectory);
var backupPathInFallback = Path.Combine(fallbackDirectory, backupFileName);
File.Copy(originalPath, backupPathInFallback, false);
return backupPathInFallback;
}
catch (Exception ex)
{
throw new InvalidOperationException($"创建备份失败: {ex.Message}", ex);
}
}
/// <summary>
/// 保存文档
/// </summary>
private void SaveDocument(Document doc)
{
try
{
if (!doc.IsModified)
{
return;
}
// Prefer compact save for normal local documents to reclaim free space.
if (TryCompactSaveInPlace(doc))
{
return;
}
try
{
doc.Save();
return;
}
catch (Exception ex) when (IsReadOnlySaveError(ex))
{
SaveDocumentAsWritableCopy(doc);
return;
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"保存文档失败: {ex.Message}", ex);
}
}
private bool TryCompactSaveInPlace(Document doc)
{
if (doc == null)
return false;
if (doc.IsWorkshared)
return false;
var originalPath = doc.PathName;
if (string.IsNullOrWhiteSpace(originalPath))
return false;
if (!File.Exists(originalPath))
return false;
try
{
var options = new SaveAsOptions
{
OverwriteExistingFile = true,
Compact = true
};
doc.SaveAs(originalPath, options);
return true;
}
catch
{
return false;
}
}
private bool IsReadOnlySaveError(Exception ex)
{
if (ex == null) return false;
var message = ex.Message ?? string.Empty;
return message.IndexOf("read-only", StringComparison.OrdinalIgnoreCase) >= 0 ||
message.IndexOf("只读", StringComparison.OrdinalIgnoreCase) >= 0;
}
private void SaveDocumentAsWritableCopy(Document doc)
{
var originalPath = doc.PathName;
if (string.IsNullOrWhiteSpace(originalPath))
throw new InvalidOperationException("文档为只读且无法获取原始路径,不能自动另存为");
var fileName = Path.GetFileNameWithoutExtension(originalPath);
var extension = Path.GetExtension(originalPath);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var outputDirectory = Path.Combine(localAppData, "RevitHttpControl", "Optimized");
Directory.CreateDirectory(outputDirectory);
var outputPath = Path.Combine(outputDirectory, $"{fileName}_optimized_{timestamp}{extension}");
var saveAsOptions = new SaveAsOptions
{
OverwriteExistingFile = false,
Compact = true
};
doc.SaveAs(outputPath, saveAsOptions);
}
/// <summary>
/// Remove unused family symbols and empty families after instance deletion.
/// </summary>
private int PurgeUnusedFamilyData(Document doc)
{
if (doc == null)
return 0;
var usedSymbolIds = new HashSet<int>();
var instances = new FilteredElementCollector(doc)
.OfClass(typeof(FamilyInstance))
.WhereElementIsNotElementType()
.Cast<FamilyInstance>();
foreach (var instance in instances)
{
var symbolId = instance?.Symbol?.Id?.IntegerValue;
if (symbolId.HasValue)
{
usedSymbolIds.Add(symbolId.Value);
}
}
var unusedSymbolIds = new FilteredElementCollector(doc)
.OfClass(typeof(FamilySymbol))
.Cast<FamilySymbol>()
.Where(symbol => symbol != null && !usedSymbolIds.Contains(symbol.Id.IntegerValue))
.Select(symbol => symbol.Id)
.ToList();
var removedSymbolCount = DeleteElementIdsInBatches(doc, unusedSymbolIds, "清理未使用族类型");
var emptyFamilyIds = new FilteredElementCollector(doc)
.OfClass(typeof(Family))
.Cast<Family>()
.Where(family => family != null && !family.GetFamilySymbolIds().Any())
.Select(family => family.Id)
.ToList();
var removedFamilyCount = DeleteElementIdsInBatches(doc, emptyFamilyIds, "清理空族定义");
return removedSymbolCount + removedFamilyCount;
}
private int DeleteElementIdsInBatches(Document doc, List<ElementId> elementIds, string transactionName)
{
if (doc == null || elementIds == null || elementIds.Count == 0)
return 0;
const int batchSize = 100;
int totalRemoved = 0;
for (int i = 0; i < elementIds.Count; i += batchSize)
{
var batchIds = elementIds.Skip(i).Take(batchSize).ToList();
if (batchIds.Count == 0)
continue;
using (var transaction = new Transaction(doc, transactionName))
{
transaction.Start();
ApplyWarningSuppression(transaction);
try
{
var deletedIds = doc.Delete(batchIds);
transaction.Commit();
totalRemoved += deletedIds.Count;
}
catch
{
transaction.RollBack();
}
}
}
return totalRemoved;
}
private void ApplyWarningSuppression(Transaction transaction)
{
if (transaction == null)
return;
var options = transaction.GetFailureHandlingOptions();
options.SetFailuresPreprocessor(new WarningFailurePreprocessor());
options.SetClearAfterRollback(true);
transaction.SetFailureHandlingOptions(options);
}
private class WarningFailurePreprocessor : IFailuresPreprocessor
{
public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor)
{
if (failuresAccessor == null)
return FailureProcessingResult.Continue;
var failures = failuresAccessor.GetFailureMessages();
if (failures == null || failures.Count == 0)
return FailureProcessingResult.Continue;
foreach (var failure in failures)
{
if (failure != null && failure.GetSeverity() == FailureSeverity.Warning)
{
failuresAccessor.DeleteWarning(failure);
}
}
return FailureProcessingResult.Continue;
}
}
/// <summary>
/// 获取文档文件大小
/// </summary>
private long GetDocumentFileSize(Document doc)
{
try
{
var filePath = doc.PathName;
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
return 0;
var fileInfo = new FileInfo(filePath);
return fileInfo.Length;
}
catch
{
return 0;
}
}
/// <summary>
/// 格式化文件大小显示
/// </summary>
private string FormatFileSize(long bytes)
{
if (bytes == 0) return "0 字节";
string[] sizes = { "字节", "KB", "MB", "GB", "TB" };
int order = 0;
double size = bytes;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:F2} {sizes[order]}";
}
}
}