615 lines
21 KiB
C#
615 lines
21 KiB
C#
using System;
|
||
using System.IO;
|
||
using Autodesk.Revit.DB;
|
||
using Autodesk.Revit.UI;
|
||
using RevitHttpControl.Models;
|
||
|
||
namespace RevitHttpControl.Services
|
||
{
|
||
/// <summary>
|
||
/// 文档操作服务
|
||
/// </summary>
|
||
public static class DocumentService
|
||
{
|
||
private const string ParkingFileName = "parking.rvt";
|
||
private const string AutoParkingFileName = "parking.auto.rvt";
|
||
private const string AutoParkingDirectoryName = "RevitHttpControl";
|
||
|
||
/// <summary>
|
||
/// 打开文档文件
|
||
/// </summary>
|
||
/// <param name="request">打开文件请求</param>
|
||
/// <returns>打开文件响应</returns>
|
||
public static OpenFileResponse OpenDocument(OpenFileRequest request)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request?.FilePath))
|
||
throw new ArgumentException("文件路径不能为空");
|
||
|
||
if (!File.Exists(request.FilePath))
|
||
throw new FileNotFoundException($"文件不存在: {request.FilePath}");
|
||
|
||
try
|
||
{
|
||
var fileName = Path.GetFileName(request.FilePath);
|
||
var completed = false;
|
||
Exception capturedException = null;
|
||
|
||
App.Instance.EnqueueCommand(uiApp =>
|
||
{
|
||
try
|
||
{
|
||
// 将字符串路径转换为ModelPath
|
||
var modelPath = ModelPathUtils.ConvertUserVisiblePathToModelPath(request.FilePath);
|
||
|
||
// 配置打开选项
|
||
var openOptions = new OpenOptions
|
||
{
|
||
DetachFromCentralOption = request.Detached
|
||
? DetachFromCentralOption.DetachAndPreserveWorksets
|
||
: DetachFromCentralOption.DetachAndDiscardWorksets
|
||
};
|
||
|
||
// 打开并激活文档
|
||
uiApp.OpenAndActivateDocument(modelPath, openOptions, false);
|
||
}
|
||
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 new OpenFileResponse
|
||
{
|
||
Result = "文件打开成功",
|
||
FileName = fileName,
|
||
FilePath = request.FilePath
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new InvalidOperationException($"打开文件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭当前活动文档
|
||
/// </summary>
|
||
/// <returns>关闭文件响应</returns>
|
||
public static CloseFileResponse CloseCurrentDocument()
|
||
{
|
||
try
|
||
{
|
||
string targetTitle = null;
|
||
string targetPath = null;
|
||
string fileName = null;
|
||
string filePath = null;
|
||
|
||
const int maxAttempts = 3;
|
||
Exception lastException = null;
|
||
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||
{
|
||
try
|
||
{
|
||
ExecuteRevitCommand(uiApp =>
|
||
{
|
||
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
||
if (activeDoc == null)
|
||
{
|
||
throw new InvalidOperationException("没有打开的文档");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(targetTitle) && string.IsNullOrWhiteSpace(targetPath))
|
||
{
|
||
targetTitle = activeDoc.Title;
|
||
targetPath = activeDoc.PathName;
|
||
fileName = string.IsNullOrEmpty(activeDoc.PathName)
|
||
? activeDoc.Title
|
||
: Path.GetFileName(activeDoc.PathName);
|
||
filePath = string.IsNullOrEmpty(activeDoc.PathName) ? null : activeDoc.PathName;
|
||
}
|
||
|
||
var target = ResolveDocumentForClose(uiApp, targetTitle, targetPath);
|
||
if (target == null)
|
||
{
|
||
throw new InvalidOperationException("目标文档未找到或已关闭");
|
||
}
|
||
|
||
if (!TrySwitchAwayFromDocument(uiApp, target))
|
||
{
|
||
throw new InvalidOperationException("无法切换到可关闭目标(请先打开第二个文档,或确保默认模板可用以自动创建 parking 文档)");
|
||
}
|
||
}, $"关闭文件操作超时(切换文档阶段,第{attempt}次)");
|
||
|
||
ExecuteRevitCommand(uiApp =>
|
||
{
|
||
var target = ResolveDocumentForClose(uiApp, targetTitle, targetPath);
|
||
if (target == null)
|
||
{
|
||
throw new InvalidOperationException("目标文档未找到或已关闭");
|
||
}
|
||
|
||
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
||
if (IsSameDocument(activeDoc, targetTitle, targetPath))
|
||
{
|
||
throw new InvalidOperationException("文档切换未生效,无法关闭当前激活文档");
|
||
}
|
||
|
||
target.Close(false);
|
||
TryFinalizeUiAfterClose(uiApp);
|
||
}, $"关闭文件操作超时(关闭文档阶段,第{attempt}次)");
|
||
|
||
lastException = null;
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
lastException = ex;
|
||
|
||
if (attempt >= maxAttempts)
|
||
{
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (lastException != null)
|
||
{
|
||
throw lastException;
|
||
}
|
||
|
||
return new CloseFileResponse
|
||
{
|
||
Result = "文件关闭成功",
|
||
FileName = fileName,
|
||
FilePath = filePath
|
||
};
|
||
}
|
||
catch (TimeoutException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (InvalidOperationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new InvalidOperationException($"关闭文件失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private static bool TrySwitchAwayFromDocument(UIApplication uiApp, Document sourceDoc)
|
||
{
|
||
var documents = uiApp.Application.Documents;
|
||
var sourceTitle = sourceDoc?.Title;
|
||
var sourcePath = sourceDoc?.PathName;
|
||
|
||
foreach (Document doc in documents)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(doc.PathName) || IsSameDocument(doc, sourceTitle, sourcePath))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
try
|
||
{
|
||
uiApp.OpenAndActivateDocument(doc.PathName);
|
||
var activeAfterSwitch = uiApp.ActiveUIDocument?.Document;
|
||
if (!IsSameDocument(activeAfterSwitch, sourceTitle, sourcePath))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 尝试下一个文档
|
||
}
|
||
}
|
||
|
||
var parkingPath = ResolveOrCreateParkingPath(uiApp, sourcePath);
|
||
if (string.IsNullOrWhiteSpace(parkingPath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
uiApp.OpenAndActivateDocument(parkingPath);
|
||
var activeAfterSwitch = uiApp.ActiveUIDocument?.Document;
|
||
return !IsSameDocument(activeAfterSwitch, sourceTitle, sourcePath);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string ResolveOrCreateParkingPath(UIApplication uiApp, string sourcePath)
|
||
{
|
||
var packagedParkingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ParkingFileName);
|
||
if (IsUsableParkingPath(packagedParkingPath, sourcePath))
|
||
{
|
||
return packagedParkingPath;
|
||
}
|
||
|
||
var autoParkingPath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
AutoParkingDirectoryName,
|
||
AutoParkingFileName);
|
||
|
||
if (IsSamePath(autoParkingPath, sourcePath))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (File.Exists(autoParkingPath))
|
||
{
|
||
return autoParkingPath;
|
||
}
|
||
|
||
return TryCreateAutoParkingDocument(uiApp, autoParkingPath) ? autoParkingPath : null;
|
||
}
|
||
|
||
private static bool IsUsableParkingPath(string parkingPath, string sourcePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(parkingPath) || !File.Exists(parkingPath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return !IsSamePath(parkingPath, sourcePath);
|
||
}
|
||
|
||
private static bool IsSamePath(string pathA, string pathB)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(pathA) || string.IsNullOrWhiteSpace(pathB))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
return string.Equals(
|
||
Path.GetFullPath(pathA),
|
||
Path.GetFullPath(pathB),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
catch
|
||
{
|
||
return string.Equals(pathA, pathB, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
}
|
||
|
||
private static bool TryCreateAutoParkingDocument(UIApplication uiApp, string parkingPath)
|
||
{
|
||
Document parkingDoc = null;
|
||
try
|
||
{
|
||
var revitApp = uiApp.Application;
|
||
var defaultTemplate = ResolveProjectTemplatePath(revitApp);
|
||
|
||
if (!string.IsNullOrWhiteSpace(defaultTemplate) && File.Exists(defaultTemplate))
|
||
{
|
||
parkingDoc = revitApp.NewProjectDocument(defaultTemplate);
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (parkingDoc == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var directory = Path.GetDirectoryName(parkingPath);
|
||
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
var saveAsOptions = new SaveAsOptions
|
||
{
|
||
OverwriteExistingFile = true
|
||
};
|
||
|
||
parkingDoc.SaveAs(parkingPath, saveAsOptions);
|
||
return File.Exists(parkingPath);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
finally
|
||
{
|
||
if (parkingDoc != null)
|
||
{
|
||
try
|
||
{
|
||
parkingDoc.Close(false);
|
||
}
|
||
catch
|
||
{
|
||
// parking 文档关闭失败不阻塞主流程
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static string ResolveProjectTemplatePath(Autodesk.Revit.ApplicationServices.Application revitApp)
|
||
{
|
||
var defaultTemplate = revitApp.DefaultProjectTemplate;
|
||
if (!string.IsNullOrWhiteSpace(defaultTemplate) && File.Exists(defaultTemplate))
|
||
{
|
||
return defaultTemplate;
|
||
}
|
||
|
||
var version = revitApp.VersionNumber;
|
||
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||
var candidates = new[]
|
||
{
|
||
Path.Combine(programData, "Autodesk", $"RVT {version}", "Templates"),
|
||
Path.Combine(programData, "Autodesk", "RVT", version, "Templates")
|
||
};
|
||
|
||
foreach (var dir in candidates)
|
||
{
|
||
if (!Directory.Exists(dir))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
try
|
||
{
|
||
var template = Directory.GetFiles(dir, "*.rte", SearchOption.AllDirectories);
|
||
if (template.Length > 0)
|
||
{
|
||
return template[0];
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore and try next candidate
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void TryFinalizeUiAfterClose(UIApplication uiApp)
|
||
{
|
||
try
|
||
{
|
||
var activeDoc = uiApp.ActiveUIDocument?.Document;
|
||
var documents = uiApp.Application.Documents;
|
||
if (activeDoc == null || documents == null || documents.Size != 1)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!IsParkingDocument(activeDoc.PathName))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var closeCmd = RevitCommandId.LookupPostableCommandId(PostableCommand.Close);
|
||
if (closeCmd != null && uiApp.CanPostCommand(closeCmd))
|
||
{
|
||
uiApp.PostCommand(closeCmd);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// UI收尾失败不影响主流程
|
||
}
|
||
}
|
||
|
||
private static bool IsParkingDocument(string path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var packagedParkingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ParkingFileName);
|
||
var autoParkingPath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
AutoParkingDirectoryName,
|
||
AutoParkingFileName);
|
||
|
||
return IsSamePath(path, packagedParkingPath) || IsSamePath(path, autoParkingPath);
|
||
}
|
||
|
||
private static Document ResolveDocumentForClose(UIApplication uiApp, string targetTitle, string targetPath)
|
||
{
|
||
foreach (Document doc in uiApp.Application.Documents)
|
||
{
|
||
if (IsSameDocument(doc, targetTitle, targetPath))
|
||
{
|
||
return doc;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static bool IsSameDocument(Document doc, string targetTitle, string targetPath)
|
||
{
|
||
if (doc == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(targetPath) && !string.IsNullOrWhiteSpace(doc.PathName))
|
||
{
|
||
try
|
||
{
|
||
return string.Equals(
|
||
Path.GetFullPath(doc.PathName),
|
||
Path.GetFullPath(targetPath),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
catch
|
||
{
|
||
return string.Equals(doc.PathName, targetPath, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
}
|
||
|
||
return string.Equals(doc.Title, targetTitle, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static void ExecuteRevitCommand(Action<UIApplication> command, string timeoutMessage)
|
||
{
|
||
var completed = false;
|
||
Exception capturedException = null;
|
||
|
||
App.Instance.EnqueueCommand(uiApp =>
|
||
{
|
||
try
|
||
{
|
||
command(uiApp);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
capturedException = ex;
|
||
}
|
||
finally
|
||
{
|
||
completed = true;
|
||
}
|
||
});
|
||
|
||
var timeout = DateTime.Now.AddSeconds(10);
|
||
while (!completed && DateTime.Now < timeout)
|
||
{
|
||
System.Threading.Thread.Sleep(100);
|
||
}
|
||
|
||
if (!completed)
|
||
{
|
||
throw new TimeoutException(timeoutMessage);
|
||
}
|
||
|
||
if (capturedException != null)
|
||
{
|
||
throw capturedException;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步打开文档文件
|
||
/// </summary>
|
||
/// <param name="request">打开文件请求</param>
|
||
/// <param name="taskId">任务ID</param>
|
||
/// <param name="taskManager">任务管理器</param>
|
||
public static void OpenDocumentAsync(OpenFileRequest request, Guid taskId, TaskManager taskManager)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request?.FilePath))
|
||
{
|
||
taskManager.FailTask(taskId, "文件路径不能为空");
|
||
return;
|
||
}
|
||
|
||
if (!File.Exists(request.FilePath))
|
||
{
|
||
taskManager.FailTask(taskId, $"文件不存在: {request.FilePath}");
|
||
return;
|
||
}
|
||
|
||
var fileName = Path.GetFileName(request.FilePath);
|
||
|
||
App.Instance.EnqueueCommand(uiApp =>
|
||
{
|
||
try
|
||
{
|
||
// 将字符串路径转换为ModelPath
|
||
var modelPath = ModelPathUtils.ConvertUserVisiblePathToModelPath(request.FilePath);
|
||
|
||
// 配置打开选项
|
||
var openOptions = new OpenOptions
|
||
{
|
||
DetachFromCentralOption = request.Detached
|
||
? DetachFromCentralOption.DetachAndPreserveWorksets
|
||
: DetachFromCentralOption.DetachAndDiscardWorksets
|
||
};
|
||
|
||
// 打开并激活文档
|
||
uiApp.OpenAndActivateDocument(modelPath, openOptions, false);
|
||
|
||
// 成功完成任务
|
||
var response = new OpenFileResponse
|
||
{
|
||
Result = "文件打开成功",
|
||
FileName = fileName,
|
||
FilePath = request.FilePath
|
||
};
|
||
|
||
taskManager.CompleteTask(taskId, response);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
taskManager.FailTask(taskId, $"打开文件失败: {ex.Message}");
|
||
}
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
taskManager.FailTask(taskId, $"打开文件失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证文件路径
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
/// <returns>是否有效</returns>
|
||
public static bool ValidateFilePath(string filePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(filePath))
|
||
return false;
|
||
|
||
try
|
||
{
|
||
var extension = Path.GetExtension(filePath).ToLowerInvariant();
|
||
return extension == ".rvt" && File.Exists(filePath);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取文件信息
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
/// <returns>文件信息</returns>
|
||
public static FileInfo GetFileInfo(string filePath)
|
||
{
|
||
if (!ValidateFilePath(filePath))
|
||
throw new ArgumentException($"无效的文件路径: {filePath}");
|
||
|
||
return new FileInfo(filePath);
|
||
}
|
||
}
|
||
}
|