650 lines
23 KiB
C#
650 lines
23 KiB
C#
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]}";
|
||
}
|
||
}
|
||
}
|