3020 lines
104 KiB
C#
3020 lines
104 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using TellmePdmsPluging.Models;
|
||
using Aveva.ApplicationFramework;
|
||
using Aveva.Pdms.Database;
|
||
using Aveva.ApplicationFramework.Presentation;
|
||
using System.Linq;
|
||
|
||
namespace TellmePdmsPluging.Core
|
||
{
|
||
public class PdmsManager
|
||
{
|
||
private static PdmsManager _instance;
|
||
private static readonly object _lock = new object();
|
||
private static readonly object _attributeCacheLock = new object();
|
||
private static readonly Dictionary<string, DbAttribute> _attributeCache = new Dictionary<string, DbAttribute>(StringComparer.OrdinalIgnoreCase);
|
||
private static string _lastExportErrorMessage;
|
||
private static readonly string[] LinkedAttributeCandidates = new[]
|
||
{
|
||
"ATCONN", "BRCON", "DEPREF", "ECRFA", "CONNE", "CONNS", "ATTC",
|
||
"CTYA", "CTYE", "CTYO", "CTYS", "ENDPOI", "CLNK", "CONN", "DCON", "DPCO"
|
||
};
|
||
|
||
public static PdmsManager Instance
|
||
{
|
||
get
|
||
{
|
||
if (_instance == null)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (_instance == null)
|
||
_instance = new PdmsManager();
|
||
}
|
||
}
|
||
return _instance;
|
||
}
|
||
}
|
||
|
||
private PdmsManager()
|
||
{
|
||
}
|
||
|
||
public SimplifyModelResult SimplifyModel(SimplifyModelRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new SimplifyModelRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new SimplifyModelResult
|
||
{
|
||
DryRun = effectiveRequest.DryRun,
|
||
SkipLinkedElements = effectiveRequest.SkipLinkedElements,
|
||
StartedAt = DateTime.Now
|
||
};
|
||
|
||
try
|
||
{
|
||
if (!IsPdmsConnected())
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "PDMS 未连接";
|
||
result.Errors.Add("PDMS 未连接");
|
||
return result;
|
||
}
|
||
|
||
var currentMdb = MDB.CurrentMDB;
|
||
var designDb = currentMdb?.GetFirstDB(DbType.Design);
|
||
if (designDb == null || designDb.World == null)
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "未找到有效的设计数据库";
|
||
result.Errors.Add("未找到有效的设计数据库");
|
||
return result;
|
||
}
|
||
|
||
if (!effectiveRequest.DryRun && IsDbReadOnly(designDb))
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "当前Design DB为只读,无法执行真实删除";
|
||
result.Errors.Add("当前Design DB为只读,无法执行真实删除");
|
||
return result;
|
||
}
|
||
|
||
var context = new SimplifyContext(effectiveRequest, result);
|
||
var sites = designDb.World.Members();
|
||
bool processed = false;
|
||
|
||
if (sites != null)
|
||
{
|
||
foreach (var site in sites)
|
||
{
|
||
if (!IsElementValid(site) || !string.Equals(GetElementTypeName(site), "SITE", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var zones = site.Members();
|
||
if (zones == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (var zone in zones)
|
||
{
|
||
if (!IsElementValid(zone) || !string.Equals(GetElementTypeName(zone), "ZONE", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!context.ShouldProcessZone(zone))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
processed = true;
|
||
context.EnterZone(zone);
|
||
context.ProcessChildren(zone);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!effectiveRequest.DryRun && processed && result.RemovedCount > 0)
|
||
{
|
||
string persistError;
|
||
if (!TryPersistCurrentMdbChanges("TellmePdms simplify model", out persistError))
|
||
{
|
||
result.Errors.Add("删除已执行,但保存更改失败: " + persistError);
|
||
}
|
||
}
|
||
|
||
result.Success = processed && result.Errors.Count == 0;
|
||
if (!processed)
|
||
{
|
||
result.Message = "未找到符合过滤条件的Zone";
|
||
}
|
||
else if (result.Success)
|
||
{
|
||
result.Message = result.DryRun ? "模型轻量化干跑完成" : "模型轻量化完成";
|
||
}
|
||
else
|
||
{
|
||
var topErrors = result.Errors.Take(3).ToArray();
|
||
var errorSummary = topErrors.Length == 0 ? "UNKNOWN_ERROR" : string.Join(" | ", topErrors);
|
||
result.Message = (result.DryRun ? "模型轻量化干跑完成,但存在错误: " : "模型轻量化完成,但存在错误: ") + errorSummary;
|
||
}
|
||
|
||
if (effectiveRequest.SkipLinkedElements && result.SkippedLinkedCount > 0)
|
||
{
|
||
result.Message += ";已跳过连接元素 " + result.SkippedLinkedCount + " 个";
|
||
}
|
||
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.Errors.Add(ex.Message);
|
||
result.Message = "模型轻量化失败";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public ShrinkwrapModelResult ShrinkwrapModel(ShrinkwrapModelRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new ShrinkwrapModelRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new ShrinkwrapModelResult
|
||
{
|
||
DryRun = effectiveRequest.DryRun,
|
||
SkipLinkedElements = effectiveRequest.SkipLinkedElements,
|
||
Padding = effectiveRequest.Padding,
|
||
DeletionMode = "conservative",
|
||
StartedAt = DateTime.Now
|
||
};
|
||
|
||
try
|
||
{
|
||
if (!IsPdmsConnected())
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "PDMS 未连接";
|
||
result.Errors.Add("PDMS 未连接");
|
||
return result;
|
||
}
|
||
|
||
var currentMdb = MDB.CurrentMDB;
|
||
var designDb = currentMdb?.GetFirstDB(DbType.Design);
|
||
if (designDb == null || designDb.World == null)
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "未找到有效的设计数据库";
|
||
result.Errors.Add("未找到有效的设计数据库");
|
||
return result;
|
||
}
|
||
|
||
if (!effectiveRequest.DryRun && IsDbReadOnly(designDb))
|
||
{
|
||
result.Success = false;
|
||
result.CompletedAt = DateTime.Now;
|
||
result.Message = "当前Design DB为只读,无法执行真实删除";
|
||
result.Errors.Add("当前Design DB为只读,无法执行真实删除");
|
||
return result;
|
||
}
|
||
|
||
var context = new ShrinkwrapContext(effectiveRequest, result);
|
||
var sites = designDb.World.Members();
|
||
bool processed = false;
|
||
|
||
if (sites != null)
|
||
{
|
||
foreach (var site in sites)
|
||
{
|
||
if (!IsElementValid(site) || !string.Equals(GetElementTypeName(site), "SITE", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var zones = site.Members();
|
||
if (zones == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (var zone in zones)
|
||
{
|
||
if (!IsElementValid(zone) || !string.Equals(GetElementTypeName(zone), "ZONE", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!context.ShouldProcessZone(zone))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
processed = true;
|
||
context.EnterZone(zone);
|
||
context.ProcessZone(zone);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!effectiveRequest.DryRun && processed && result.RemovedCount > 0)
|
||
{
|
||
string persistError;
|
||
if (!TryPersistCurrentMdbChanges("TellmePdms shrinkwrap model", context.GetTouchedWritableDbs(), result.ProcessedDbNames, out persistError))
|
||
{
|
||
result.FatalErrors.Add("删除已执行,但保存更改失败: " + persistError);
|
||
}
|
||
}
|
||
|
||
result.Success = processed && result.Errors.Count == 0 && result.FatalErrors.Count == 0;
|
||
if (!processed)
|
||
{
|
||
result.Message = "未找到符合过滤条件的Zone";
|
||
}
|
||
else if (result.Success)
|
||
{
|
||
result.Message = result.DryRun ? "外壳保留干跑完成" : "外壳保留完成";
|
||
}
|
||
else
|
||
{
|
||
var mergedErrors = result.FatalErrors.Concat(result.Errors).Take(3).ToArray();
|
||
var topErrors = mergedErrors;
|
||
var errorSummary = topErrors.Length == 0 ? "UNKNOWN_ERROR" : string.Join(" | ", topErrors);
|
||
result.Message = (result.DryRun ? "外壳保留干跑完成,但存在错误: " : "外壳保留完成,但存在错误: ") + errorSummary;
|
||
}
|
||
|
||
if (effectiveRequest.SkipLinkedElements && result.SkippedLinkedCount > 0)
|
||
{
|
||
result.Message += ";已跳过连接元素 " + result.SkippedLinkedCount + " 个";
|
||
}
|
||
|
||
if (result.SkippedUnsafeCount > 0)
|
||
{
|
||
result.Message += ";已跳过不安全元素 " + result.SkippedUnsafeCount + " 个";
|
||
}
|
||
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.FatalErrors.Add(ex.Message);
|
||
result.Message = "外壳保留失败";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public ModelStatusResponse GetModelStatus()
|
||
{
|
||
try
|
||
{
|
||
// 检查PDMS是否连接
|
||
if (!IsPdmsConnected())
|
||
{
|
||
return new ModelStatusResponse
|
||
{
|
||
ModelLoaded = false
|
||
};
|
||
}
|
||
|
||
return new ModelStatusResponse
|
||
{
|
||
ModelLoaded = true,
|
||
ProjectInfo = GetProjectInfo(),
|
||
ModelStatistics = GetModelStatistics(),
|
||
SessionInfo = GetSessionInfo(),
|
||
ExportDiagnostics = GetExportDiagnostics(null)
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录错误日志
|
||
System.Diagnostics.Debug.WriteLine($"获取PDMS模型状态失败: {ex.Message}");
|
||
return new ModelStatusResponse
|
||
{
|
||
ModelLoaded = false,
|
||
ExportDiagnostics = GetExportDiagnostics(null)
|
||
};
|
||
}
|
||
}
|
||
|
||
public OpenProjectResult OpenProject(OpenProjectRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new OpenProjectRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new OpenProjectResult
|
||
{
|
||
ProjectName = effectiveRequest.ProjectName,
|
||
FileSize = -1,
|
||
PolygonCount = -1,
|
||
FeatureCount = -1,
|
||
CompletedAt = DateTime.Now
|
||
};
|
||
|
||
try
|
||
{
|
||
var currentProject = Project.CurrentProject;
|
||
if (currentProject == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "Project.CurrentProject 为空,无法打开项目";
|
||
return result;
|
||
}
|
||
|
||
bool alreadyOpen = false;
|
||
try
|
||
{
|
||
alreadyOpen = currentProject.IsOpen();
|
||
}
|
||
catch
|
||
{
|
||
alreadyOpen = false;
|
||
}
|
||
|
||
result.WasAlreadyOpen = alreadyOpen;
|
||
|
||
// 如果已经打开且未指定目标项目名,则直接返回成功
|
||
if (alreadyOpen && string.IsNullOrEmpty(effectiveRequest.ProjectName))
|
||
{
|
||
result.Success = true;
|
||
result.Message = "项目已打开";
|
||
PopulateOpenProjectMetrics(result);
|
||
return result;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(effectiveRequest.ProjectName))
|
||
{
|
||
result.Success = false;
|
||
result.Message = "ProjectName 不能为空";
|
||
return result;
|
||
}
|
||
|
||
bool opened = currentProject.Open(
|
||
effectiveRequest.ProjectName,
|
||
effectiveRequest.UserName ?? string.Empty,
|
||
effectiveRequest.Password ?? string.Empty);
|
||
|
||
result.Success = opened;
|
||
result.Message = opened ? "项目打开成功" : "项目打开失败(Open 返回 false)";
|
||
if (opened)
|
||
{
|
||
PopulateOpenProjectMetrics(result);
|
||
}
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "项目打开异常: " + ex.Message;
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public CloseProjectResult CloseProject(CloseProjectRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new CloseProjectRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new CloseProjectResult
|
||
{
|
||
CompletedAt = DateTime.Now
|
||
};
|
||
|
||
try
|
||
{
|
||
var currentProject = Project.CurrentProject;
|
||
if (currentProject == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "Project.CurrentProject 为空,无法关闭项目";
|
||
return result;
|
||
}
|
||
|
||
bool isOpen = false;
|
||
try
|
||
{
|
||
isOpen = currentProject.IsOpen();
|
||
}
|
||
catch
|
||
{
|
||
isOpen = false;
|
||
}
|
||
|
||
result.WasOpen = isOpen;
|
||
result.ProjectName = GetProjectNameSafe(currentProject);
|
||
|
||
if (!isOpen)
|
||
{
|
||
result.Success = true;
|
||
result.Message = "当前没有已打开项目";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
|
||
var projectType = currentProject.GetType();
|
||
|
||
var closeWithForce = projectType.GetMethod("Close", new[] { typeof(bool) });
|
||
if (closeWithForce != null)
|
||
{
|
||
closeWithForce.Invoke(currentProject, new object[] { effectiveRequest.Force });
|
||
}
|
||
else
|
||
{
|
||
var closeNoArg = projectType.GetMethod("Close", Type.EmptyTypes);
|
||
if (closeNoArg == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "未找到可用的 Project.Close 方法";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
|
||
closeNoArg.Invoke(currentProject, null);
|
||
}
|
||
|
||
result.Success = true;
|
||
result.Message = "项目关闭成功";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "关闭项目异常: " + ex.Message;
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public ExportIfcResult ExportIfc(ExportIfcRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new ExportIfcRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new ExportIfcResult
|
||
{
|
||
StartedAt = DateTime.Now,
|
||
ExportPath = effectiveRequest.ExportPath,
|
||
FileName = effectiveRequest.FileName,
|
||
FullPath = effectiveRequest.GetFullPath(),
|
||
RequestedMdbName = effectiveRequest.MdbName,
|
||
DriverMode = "macro-export"
|
||
};
|
||
|
||
try
|
||
{
|
||
if (!IsPdmsConnected())
|
||
{
|
||
result.Success = false;
|
||
result.Message = "PDMS 未连接";
|
||
result.FailureCategory = "pdms-connection";
|
||
result.RawError = result.Message;
|
||
SetLastExportError(result.Message);
|
||
result.CompletedAt = DateTime.Now;
|
||
result.DurationSeconds = (result.CompletedAt - result.StartedAt).TotalSeconds;
|
||
return result;
|
||
}
|
||
|
||
string mdbValidationError;
|
||
var currentMdb = ResolveExportMdb(effectiveRequest, result, out mdbValidationError);
|
||
if (currentMdb == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = mdbValidationError ?? "未找到可用于导出的MDB";
|
||
result.FailureCategory = "mdb-validation";
|
||
result.RawError = result.Message;
|
||
SetLastExportError(result.Message);
|
||
result.CompletedAt = DateTime.Now;
|
||
result.DurationSeconds = (result.CompletedAt - result.StartedAt).TotalSeconds;
|
||
return result;
|
||
}
|
||
|
||
result.ActiveMdbName = GetMdbNameSafe(currentMdb);
|
||
result.BaseChecksPassed = true;
|
||
|
||
var designDb = currentMdb?.GetFirstDB(DbType.Design);
|
||
if (designDb == null || designDb.World == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "未找到有效的设计数据库";
|
||
result.FailureCategory = "design-db";
|
||
result.RawError = result.Message;
|
||
SetLastExportError(result.Message);
|
||
result.CompletedAt = DateTime.Now;
|
||
result.DurationSeconds = (result.CompletedAt - result.StartedAt).TotalSeconds;
|
||
return result;
|
||
}
|
||
|
||
// 确保导出目录存在
|
||
if (!System.IO.Directory.Exists(effectiveRequest.ExportPath))
|
||
{
|
||
System.IO.Directory.CreateDirectory(effectiveRequest.ExportPath);
|
||
}
|
||
|
||
// 执行PML命令 - 通过MacroCommand类型反射构造并执行
|
||
var macroType = CommandManager.Instance.MacroCommand;
|
||
if (macroType == null)
|
||
throw new InvalidOperationException("MacroCommand类型未注册");
|
||
var ctor = macroType.GetConstructor(
|
||
System.Reflection.BindingFlags.Public |
|
||
System.Reflection.BindingFlags.NonPublic |
|
||
System.Reflection.BindingFlags.Instance,
|
||
null,
|
||
new[] { typeof(string), typeof(string), typeof(string) },
|
||
null);
|
||
if (ctor == null)
|
||
{
|
||
var ctors = macroType.GetConstructors(
|
||
System.Reflection.BindingFlags.Public |
|
||
System.Reflection.BindingFlags.NonPublic |
|
||
System.Reflection.BindingFlags.Instance);
|
||
var ctorInfo = new System.Text.StringBuilder();
|
||
foreach (var c in ctors)
|
||
{
|
||
ctorInfo.Append(c.ToString()).Append(" | ");
|
||
}
|
||
|
||
throw new InvalidOperationException(
|
||
"MacroCommand未找到(string,string,string)构造函数,类型: " + macroType.FullName + " 构造函数: " + ctorInfo);
|
||
}
|
||
|
||
var exportCommands = BuildExportCommands(effectiveRequest);
|
||
for (int i = 0; i < exportCommands.Length; i++)
|
||
{
|
||
result.ExecutedCommands.Add(exportCommands[i]);
|
||
var macroCommand = ctor.Invoke(new object[]
|
||
{
|
||
"TellmePdmsPluging.ExportIfc." + (i + 1),
|
||
exportCommands[i],
|
||
string.Empty
|
||
}) as Command;
|
||
|
||
if (macroCommand == null)
|
||
{
|
||
throw new InvalidOperationException("MacroCommand实例创建失败,类型: " + macroType.FullName);
|
||
}
|
||
|
||
macroCommand.Execute();
|
||
}
|
||
|
||
// 检查文件是否生成
|
||
if (System.IO.File.Exists(result.FullPath))
|
||
{
|
||
var fileInfo = new System.IO.FileInfo(result.FullPath);
|
||
result.FileSizeBytes = fileInfo.Length;
|
||
result.Success = true;
|
||
result.Message = "RVM导出成功";
|
||
result.FailureCategory = null;
|
||
result.RawError = null;
|
||
SetLastExportError(null);
|
||
}
|
||
else
|
||
{
|
||
result.Success = false;
|
||
result.Message = "RVM导出失败,未生成文件";
|
||
result.FailureCategory = "output-path";
|
||
result.RawError = result.Message;
|
||
result.Diagnostics.Add("导出命令已执行,但目标文件未生成");
|
||
SetLastExportError(result.Message);
|
||
}
|
||
|
||
result.CompletedAt = DateTime.Now;
|
||
result.DurationSeconds = (result.CompletedAt - result.StartedAt).TotalSeconds;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.RawError = ex.Message;
|
||
result.FailureCategory = ClassifyExportFailure(ex.Message);
|
||
result.Message = TranslateExportException(ex, result.ActiveMdbName, effectiveRequest.MdbName);
|
||
result.Diagnostics.Add("导出命令执行失败,分类=" + result.FailureCategory);
|
||
SetLastExportError(result.Message);
|
||
result.CompletedAt = DateTime.Now;
|
||
result.DurationSeconds = (result.CompletedAt - result.StartedAt).TotalSeconds;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
private ExportDiagnostics GetExportDiagnostics(string requestedMdbName)
|
||
{
|
||
var diagnostics = new ExportDiagnostics
|
||
{
|
||
RequestedMdbName = NormalizeMdbName(requestedMdbName),
|
||
LastExportError = _lastExportErrorMessage
|
||
};
|
||
|
||
try
|
||
{
|
||
var currentMdb = MDB.CurrentMDB;
|
||
diagnostics.ActiveMdbName = GetMdbNameSafe(currentMdb);
|
||
diagnostics.CurrentMdbMatchesRequested = string.IsNullOrEmpty(diagnostics.RequestedMdbName) ||
|
||
IsSameMdbName(diagnostics.ActiveMdbName, diagnostics.RequestedMdbName);
|
||
|
||
var designDb = currentMdb == null ? null : currentMdb.GetFirstDB(DbType.Design);
|
||
diagnostics.DesignDbAvailable = designDb != null && designDb.World != null;
|
||
diagnostics.DefinitionsLikelyMissing = IsDefinitionsError(_lastExportErrorMessage);
|
||
|
||
if (currentMdb == null)
|
||
{
|
||
diagnostics.ExportPrecheckPassed = false;
|
||
diagnostics.Message = "当前没有活动MDB";
|
||
return diagnostics;
|
||
}
|
||
|
||
if (!diagnostics.CurrentMdbMatchesRequested)
|
||
{
|
||
diagnostics.ExportPrecheckPassed = false;
|
||
diagnostics.Message = "当前活动MDB与请求的MDB不一致";
|
||
return diagnostics;
|
||
}
|
||
|
||
if (!diagnostics.DesignDbAvailable)
|
||
{
|
||
diagnostics.ExportPrecheckPassed = false;
|
||
diagnostics.Message = "当前MDB未挂接有效的Design DB";
|
||
return diagnostics;
|
||
}
|
||
|
||
if (diagnostics.DefinitionsLikelyMissing)
|
||
{
|
||
diagnostics.ExportPrecheckPassed = false;
|
||
diagnostics.Message = "当前MDB疑似缺少导出所需的UDET/UDA definitions";
|
||
return diagnostics;
|
||
}
|
||
|
||
diagnostics.ExportPrecheckPassed = true;
|
||
diagnostics.Message = "导出前置检查通过";
|
||
return diagnostics;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
diagnostics.ExportPrecheckPassed = false;
|
||
diagnostics.Message = "导出检查异常: " + ex.Message;
|
||
return diagnostics;
|
||
}
|
||
}
|
||
|
||
private MDB ResolveExportMdb(ExportIfcRequest request, ExportIfcResult result, out string error)
|
||
{
|
||
error = null;
|
||
|
||
try
|
||
{
|
||
var currentMdb = MDB.CurrentMDB;
|
||
var requestedMdbName = NormalizeMdbName(request.MdbName);
|
||
request.MdbName = requestedMdbName;
|
||
result.RequestedMdbName = requestedMdbName;
|
||
result.ActiveMdbName = GetMdbNameSafe(currentMdb);
|
||
|
||
if (string.IsNullOrEmpty(requestedMdbName))
|
||
{
|
||
result.UsedRequestedMdb = false;
|
||
result.Diagnostics.Add("未提供mdbName,直接使用当前活动MDB");
|
||
return currentMdb;
|
||
}
|
||
|
||
if (currentMdb != null && IsSameMdbName(currentMdb.Name, requestedMdbName))
|
||
{
|
||
result.UsedRequestedMdb = true;
|
||
result.Diagnostics.Add("请求的MDB与当前活动MDB一致,直接使用当前MDB");
|
||
return currentMdb;
|
||
}
|
||
|
||
result.UsedRequestedMdb = false;
|
||
result.Diagnostics.Add("请求的MDB与当前活动MDB不一致,插件不会自动切换MDB");
|
||
error = "当前活动MDB " + (GetMdbNameSafe(currentMdb) ?? "NULL") + " 与请求的MDB " + requestedMdbName + " 不一致,请先在PDMS中手动切换后重试";
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
error = "校验导出MDB异常: " + ex.Message;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static string GetMdbNameSafe(MDB mdb)
|
||
{
|
||
if (mdb == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
return mdb.Name;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static string NormalizeMdbName(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var trimmed = value.Trim();
|
||
if (trimmed.Length == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (!trimmed.StartsWith("/", StringComparison.Ordinal))
|
||
{
|
||
trimmed = "/" + trimmed.TrimStart('/');
|
||
}
|
||
|
||
return trimmed;
|
||
}
|
||
|
||
private static bool IsSameMdbName(string left, string right)
|
||
{
|
||
var normalizedLeft = NormalizeMdbName(left);
|
||
var normalizedRight = NormalizeMdbName(right);
|
||
|
||
if (string.IsNullOrEmpty(normalizedLeft) || string.IsNullOrEmpty(normalizedRight))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static string TranslateExportException(Exception ex, string activeMdbName, string requestedMdbName)
|
||
{
|
||
var rawMessage = ex == null ? string.Empty : ex.Message ?? string.Empty;
|
||
var mdbName = !string.IsNullOrEmpty(activeMdbName) ? activeMdbName : requestedMdbName;
|
||
|
||
if (IsDefinitionsError(rawMessage))
|
||
{
|
||
if (!string.IsNullOrEmpty(mdbName))
|
||
{
|
||
return "RVM导出失败: 当前MDB " + mdbName + " 缺少导出所需的UDET/UDA定义,请切换到包含定义库的MDB后重试";
|
||
}
|
||
|
||
return "RVM导出失败: 当前MDB缺少导出所需的UDET/UDA定义,请切换到包含定义库的MDB后重试";
|
||
}
|
||
|
||
if (rawMessage.IndexOf("找不到文件", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
rawMessage.IndexOf("could not find file", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
return "RVM导出失败: Review driver/template 路径或输入文件无效";
|
||
}
|
||
|
||
return "RVM导出异常: " + rawMessage;
|
||
}
|
||
|
||
private static string ClassifyExportFailure(string rawMessage)
|
||
{
|
||
if (string.IsNullOrEmpty(rawMessage))
|
||
{
|
||
return "command";
|
||
}
|
||
|
||
if (IsDefinitionsError(rawMessage))
|
||
{
|
||
return "definitions";
|
||
}
|
||
|
||
if (rawMessage.IndexOf("找不到文件", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
rawMessage.IndexOf("could not find file", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
return "driver-template";
|
||
}
|
||
|
||
if (rawMessage.IndexOf("应用程序中的错误", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
return "mdb-switch";
|
||
}
|
||
|
||
return "command";
|
||
}
|
||
|
||
private static bool IsDefinitionsError(string message)
|
||
{
|
||
if (string.IsNullOrEmpty(message))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return message.IndexOf("Undefined UDA or UDET name", StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
|
||
private static void SetLastExportError(string message)
|
||
{
|
||
_lastExportErrorMessage = message;
|
||
}
|
||
|
||
private string[] BuildExportCommands(ExportIfcRequest request)
|
||
{
|
||
// 按文档流程构建命令:
|
||
// EXPORT SYSTEM <driver> (可选)
|
||
// EXPORT FILE <name> OVERWRITE
|
||
// EXPORT <selection>
|
||
// EXPORT FINISH
|
||
string fullPath = request.GetFullPath().Replace("\\", "/");
|
||
var commands = new List<string>();
|
||
|
||
if (!string.IsNullOrEmpty(request.ExportSystem))
|
||
{
|
||
commands.Add("EXPORT SYSTEM " + request.ExportSystem);
|
||
}
|
||
|
||
commands.Add(string.Format("EXPORT FILE {0} {1}", fullPath, request.Overwrite.GetValueOrDefault(true) ? "OVERWRITE" : "READ"));
|
||
|
||
var selections = request.GetEffectiveSelections();
|
||
foreach (var selectionCommand in selections)
|
||
{
|
||
commands.Add(selectionCommand);
|
||
}
|
||
|
||
commands.Add("EXPORT FINISH");
|
||
|
||
return commands.ToArray();
|
||
}
|
||
|
||
public OpenMdbResult OpenMdb(OpenMdbRequest request)
|
||
{
|
||
var effectiveRequest = request ?? new OpenMdbRequest();
|
||
effectiveRequest.ApplyDefaults();
|
||
|
||
var result = new OpenMdbResult
|
||
{
|
||
MdbName = effectiveRequest.MdbName,
|
||
ReadOnly = effectiveRequest.ReadOnly,
|
||
DefaultType = effectiveRequest.DefaultType,
|
||
FileSize = -1,
|
||
PolygonCount = -1,
|
||
FeatureCount = -1,
|
||
CompletedAt = DateTime.Now
|
||
};
|
||
|
||
try
|
||
{
|
||
if (string.IsNullOrEmpty(effectiveRequest.MdbName))
|
||
{
|
||
result.Success = false;
|
||
result.Message = "MdbName 不能为空";
|
||
return result;
|
||
}
|
||
|
||
// 如果当前MDB已经是目标,则直接返回成功
|
||
try
|
||
{
|
||
var current = MDB.CurrentMDB;
|
||
if (current != null && string.Equals(current.Name, effectiveRequest.MdbName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
result.Success = true;
|
||
result.WasAlreadyOpen = true;
|
||
result.Message = "MDB 已经打开";
|
||
PopulateOpenMdbMetrics(result);
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore, continue to open
|
||
}
|
||
|
||
var setup = MDBSetup.CreateMDBSetup(effectiveRequest.MdbName);
|
||
if (setup == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "创建 MDBSetup 失败";
|
||
return result;
|
||
}
|
||
|
||
setup.ReadOnly = effectiveRequest.ReadOnly;
|
||
|
||
if (effectiveRequest.Subtype.HasValue)
|
||
{
|
||
setup.Subtype = effectiveRequest.Subtype.Value;
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(effectiveRequest.DefaultType))
|
||
{
|
||
DbType defaultType;
|
||
if (!TryParseDbType(effectiveRequest.DefaultType, out defaultType))
|
||
{
|
||
result.Success = false;
|
||
result.Message = "DefaultType 无效: " + effectiveRequest.DefaultType;
|
||
return result;
|
||
}
|
||
setup.DefaultType = defaultType;
|
||
}
|
||
|
||
if (effectiveRequest.ReadTypes != null && effectiveRequest.ReadTypes.Count > 0)
|
||
{
|
||
DbType[] readTypes;
|
||
string error;
|
||
if (!TryParseDbTypes(effectiveRequest.ReadTypes, out readTypes, out error))
|
||
{
|
||
result.Success = false;
|
||
result.Message = "ReadTypes 无效: " + error;
|
||
return result;
|
||
}
|
||
setup.ReadTypes = readTypes;
|
||
}
|
||
|
||
if (effectiveRequest.WriteTypes != null && effectiveRequest.WriteTypes.Count > 0)
|
||
{
|
||
DbType[] writeTypes;
|
||
string error;
|
||
if (!TryParseDbTypes(effectiveRequest.WriteTypes, out writeTypes, out error))
|
||
{
|
||
result.Success = false;
|
||
result.Message = "WriteTypes 无效: " + error;
|
||
return result;
|
||
}
|
||
setup.WriteTypes = writeTypes;
|
||
}
|
||
|
||
var openedMdb = Project.OpenMDB(setup);
|
||
if (openedMdb == null)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "打开 MDB 失败(OpenMDB 返回 null)";
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
|
||
result.Success = true;
|
||
result.WasAlreadyOpen = false;
|
||
result.MdbName = openedMdb.Name;
|
||
result.Message = "MDB 打开成功";
|
||
PopulateOpenMdbMetrics(result);
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.Message = "打开 MDB 异常: " + ex.Message;
|
||
result.CompletedAt = DateTime.Now;
|
||
return result;
|
||
}
|
||
}
|
||
|
||
private static bool TryParseDbTypes(IEnumerable<string> values, out DbType[] types, out string error)
|
||
{
|
||
var list = new List<DbType>();
|
||
var invalid = new List<string>();
|
||
|
||
foreach (var raw in values)
|
||
{
|
||
if (string.IsNullOrEmpty(raw))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
DbType parsed;
|
||
if (TryParseDbType(raw, out parsed))
|
||
{
|
||
if (!list.Contains(parsed))
|
||
{
|
||
list.Add(parsed);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
invalid.Add(raw);
|
||
}
|
||
}
|
||
|
||
if (invalid.Count > 0)
|
||
{
|
||
types = null;
|
||
error = string.Join(",", invalid.ToArray());
|
||
return false;
|
||
}
|
||
|
||
types = list.ToArray();
|
||
error = null;
|
||
return true;
|
||
}
|
||
|
||
private static bool TryParseDbType(string value, out DbType dbType)
|
||
{
|
||
dbType = DbType.Design;
|
||
if (string.IsNullOrEmpty(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
dbType = (DbType)Enum.Parse(typeof(DbType), value.Trim(), true);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string GetProjectNameSafe(Project project)
|
||
{
|
||
if (project == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
var nameProperty = project.GetType().GetProperty("Name");
|
||
if (nameProperty != null)
|
||
{
|
||
var value = nameProperty.GetValue(project, null) as string;
|
||
if (!string.IsNullOrEmpty(value))
|
||
{
|
||
return value;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore and fallback
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static bool IsDbReadOnly(Db db)
|
||
{
|
||
if (db == null)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
try
|
||
{
|
||
var property = db.GetType().GetProperty("IsReadOnly");
|
||
if (property == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var value = property.GetValue(db, null);
|
||
if (value is bool)
|
||
{
|
||
return (bool)value;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore and treat as writable
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool TryPersistCurrentMdbChanges(string comment, out string error)
|
||
{
|
||
error = null;
|
||
try
|
||
{
|
||
var mdb = MDB.CurrentMDB;
|
||
if (mdb == null)
|
||
{
|
||
error = "当前MDB为空";
|
||
return false;
|
||
}
|
||
|
||
bool saveResult;
|
||
if (TryInvokeSaveWorkByComment(mdb, comment, out saveResult))
|
||
{
|
||
if (saveResult)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
error = "SaveWork(string) 返回 false";
|
||
return false;
|
||
}
|
||
|
||
var designDb = mdb.GetFirstDB(DbType.Design);
|
||
if (designDb == null)
|
||
{
|
||
error = "未找到Design DB";
|
||
return false;
|
||
}
|
||
|
||
if (IsDbReadOnly(designDb))
|
||
{
|
||
error = "Design DB为只读";
|
||
return false;
|
||
}
|
||
|
||
if (TryInvokeSaveWorkByDbs(mdb, new[] { designDb }, comment, out saveResult))
|
||
{
|
||
if (saveResult)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
error = "SaveWork(Db[], string) 返回 false";
|
||
return false;
|
||
}
|
||
|
||
error = "未找到可用的SaveWork重载";
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
error = ex.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool TryPersistCurrentMdbChanges(string comment, IList<Db> touchedDbs, IList<string> processedDbNames, out string error)
|
||
{
|
||
error = null;
|
||
|
||
if (processedDbNames != null)
|
||
{
|
||
processedDbNames.Clear();
|
||
}
|
||
|
||
try
|
||
{
|
||
var mdb = MDB.CurrentMDB;
|
||
if (mdb == null)
|
||
{
|
||
error = "当前MDB为空";
|
||
return false;
|
||
}
|
||
|
||
var writableDbs = new List<Db>();
|
||
if (touchedDbs != null)
|
||
{
|
||
for (int i = 0; i < touchedDbs.Count; i++)
|
||
{
|
||
var db = touchedDbs[i];
|
||
if (db == null || IsDbReadOnly(db))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!ContainsDb(writableDbs, db))
|
||
{
|
||
writableDbs.Add(db);
|
||
if (processedDbNames != null)
|
||
{
|
||
processedDbNames.Add(GetDbNameSafe(db));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (writableDbs.Count > 0)
|
||
{
|
||
bool saveResult;
|
||
if (TryInvokeSaveWorkByDbs(mdb, writableDbs.ToArray(), comment, out saveResult))
|
||
{
|
||
if (saveResult)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
error = "SaveWork(Db[], string) 返回 false";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return TryPersistCurrentMdbChanges(comment, out error);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
error = ex.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool TryInvokeSaveWorkByComment(MDB mdb, string comment, out bool saveResult)
|
||
{
|
||
saveResult = false;
|
||
try
|
||
{
|
||
var method = mdb.GetType().GetMethod("SaveWork", new[] { typeof(string) });
|
||
if (method == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var result = method.Invoke(mdb, new object[] { comment ?? string.Empty });
|
||
saveResult = !(result is bool) || (bool)result;
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool TryInvokeSaveWorkByDbs(MDB mdb, Db[] dbs, string comment, out bool saveResult)
|
||
{
|
||
saveResult = false;
|
||
try
|
||
{
|
||
var method = mdb.GetType().GetMethod("SaveWork", new[] { typeof(Db[]), typeof(string) });
|
||
if (method == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var result = method.Invoke(mdb, new object[] { dbs, comment ?? string.Empty });
|
||
saveResult = !(result is bool) || (bool)result;
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool ContainsDb(IList<Db> dbs, Db candidate)
|
||
{
|
||
if (dbs == null || candidate == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (int i = 0; i < dbs.Count; i++)
|
||
{
|
||
if (object.ReferenceEquals(dbs[i], candidate))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private bool IsPdmsConnected()
|
||
{
|
||
try
|
||
{
|
||
// 使用MDB.CurrentMDB检查PDMS连接状态
|
||
var currentMdb = MDB.CurrentMDB;
|
||
return currentMdb != null;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private ProjectInfo GetProjectInfo()
|
||
{
|
||
try
|
||
{
|
||
// 使用MDB.CurrentMDB和Project.CurrentProject获取真实项目信息
|
||
var currentMdb = MDB.CurrentMDB;
|
||
var currentProject = Project.CurrentProject;
|
||
|
||
if (currentMdb != null)
|
||
{
|
||
var designDb = currentMdb.GetFirstDB(DbType.Design);
|
||
if (designDb != null)
|
||
{
|
||
return new ProjectInfo
|
||
{
|
||
ProjectName = designDb.Name ?? "Unknown",
|
||
MdsName = currentMdb.Name ?? "Unknown",
|
||
PdmsVersion = "12.1.SP4"
|
||
};
|
||
}
|
||
}
|
||
|
||
return new ProjectInfo
|
||
{
|
||
ProjectName = "Unknown",
|
||
MdsName = "Unknown",
|
||
PdmsVersion = "12.1.SP4"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"获取项目信息失败: {ex.Message}");
|
||
return new ProjectInfo
|
||
{
|
||
ProjectName = "Unknown",
|
||
MdsName = "Unknown",
|
||
PdmsVersion = "12.1.SP4"
|
||
};
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private ModelStatistics GetModelStatistics()
|
||
{
|
||
try
|
||
{
|
||
// 使用MDB.CurrentMDB获取真实模型统计信息
|
||
var currentMdb = MDB.CurrentMDB;
|
||
if (currentMdb != null)
|
||
{
|
||
var designDb = currentMdb.GetFirstDB(DbType.Design);
|
||
if (designDb?.World != null)
|
||
{
|
||
// 初始化统计计数器
|
||
var elementCounts = new Dictionary<string, int>
|
||
{
|
||
{"SITE", 0}, {"ZONE", 0}, {"PIPE", 0}, {"EQUI", 0},
|
||
{"STRU", 0}, {"VALVE", 0}, {"FITT", 0}, {"NOZZ", 0}
|
||
};
|
||
|
||
int totalElements = 0;
|
||
var activeZones = new List<string>();
|
||
int zoneCount = 0;
|
||
|
||
// 递归统计所有元素
|
||
CountElementsByType(designDb.World, elementCounts, ref totalElements);
|
||
|
||
// 专门统计Zone信息
|
||
CountZones(designDb.World, ref zoneCount, activeZones);
|
||
|
||
return new ModelStatistics
|
||
{
|
||
TotalElements = totalElements,
|
||
ElementCounts = elementCounts,
|
||
ZoneCount = zoneCount,
|
||
ActiveZones = activeZones,
|
||
FileSize = TryGetCurrentModelFileSize(),
|
||
PolygonCount = -1,
|
||
FeatureCount = totalElements
|
||
};
|
||
}
|
||
}
|
||
|
||
return new ModelStatistics
|
||
{
|
||
TotalElements = 0,
|
||
ElementCounts = new Dictionary<string, int>(),
|
||
ZoneCount = 0,
|
||
ActiveZones = new List<string>(),
|
||
FileSize = -1,
|
||
PolygonCount = -1,
|
||
FeatureCount = 0
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"获取模型统计信息失败: {ex.Message}");
|
||
return new ModelStatistics
|
||
{
|
||
TotalElements = 0,
|
||
ElementCounts = new Dictionary<string, int>(),
|
||
ZoneCount = 0,
|
||
ActiveZones = new List<string>(),
|
||
FileSize = -1,
|
||
PolygonCount = -1,
|
||
FeatureCount = 0
|
||
};
|
||
}
|
||
}
|
||
|
||
private void PopulateOpenProjectMetrics(OpenProjectResult result)
|
||
{
|
||
var statistics = GetModelStatistics();
|
||
if (statistics == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
result.FileSize = statistics.FileSize;
|
||
result.PolygonCount = statistics.PolygonCount;
|
||
result.FeatureCount = statistics.FeatureCount;
|
||
}
|
||
|
||
private void PopulateOpenMdbMetrics(OpenMdbResult result)
|
||
{
|
||
var statistics = GetModelStatistics();
|
||
if (statistics == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
result.FileSize = statistics.FileSize;
|
||
result.PolygonCount = statistics.PolygonCount;
|
||
result.FeatureCount = statistics.FeatureCount;
|
||
}
|
||
|
||
private long TryGetCurrentModelFileSize()
|
||
{
|
||
try
|
||
{
|
||
var currentMdb = MDB.CurrentMDB;
|
||
if (currentMdb == null)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
var designDb = currentMdb.GetFirstDB(DbType.Design);
|
||
string path = TryGetCandidatePath(designDb) ?? TryGetCandidatePath(currentMdb);
|
||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
return new System.IO.FileInfo(path).Length;
|
||
}
|
||
catch
|
||
{
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
private static string TryGetCandidatePath(object source)
|
||
{
|
||
if (source == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var type = source.GetType();
|
||
var names = new[] { "FileName", "Filename", "FullName", "Path" };
|
||
foreach (var name in names)
|
||
{
|
||
var prop = type.GetProperty(name);
|
||
if (prop != null && prop.PropertyType == typeof(string))
|
||
{
|
||
var value = prop.GetValue(source, null) as string;
|
||
if (LooksLikeFilePath(value))
|
||
{
|
||
return value;
|
||
}
|
||
}
|
||
|
||
var field = type.GetField(name);
|
||
if (field != null && field.FieldType == typeof(string))
|
||
{
|
||
var value = field.GetValue(source) as string;
|
||
if (LooksLikeFilePath(value))
|
||
{
|
||
return value;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static bool LooksLikeFilePath(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var trimmed = value.Trim();
|
||
if (trimmed.Length < 3)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return trimmed.IndexOf('\\') >= 0 || trimmed.IndexOf('/') >= 0;
|
||
}
|
||
|
||
private void CountElementsByType(DbElement parentElement, Dictionary<string, int> elementCounts, ref int totalElements)
|
||
{
|
||
try
|
||
{
|
||
if (parentElement == null || parentElement.IsNull || !parentElement.IsValid)
|
||
return;
|
||
|
||
// 获取所有子元素
|
||
var members = parentElement.Members();
|
||
if (members != null)
|
||
{
|
||
foreach (var element in members)
|
||
{
|
||
if (element != null && !element.IsNull && element.IsValid)
|
||
{
|
||
totalElements++;
|
||
|
||
// 获取元素的实际类型
|
||
var elementType = element.GetActualType();
|
||
if (elementType != null)
|
||
{
|
||
string typeName = elementType.Name;
|
||
if (elementCounts.ContainsKey(typeName))
|
||
{
|
||
elementCounts[typeName]++;
|
||
}
|
||
}
|
||
|
||
// 递归统计子元素
|
||
CountElementsByType(element, elementCounts, ref totalElements);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"统计元素类型时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void CountZones(DbElement worldElement, ref int zoneCount, List<string> activeZones)
|
||
{
|
||
try
|
||
{
|
||
if (worldElement == null || worldElement.IsNull || !worldElement.IsValid)
|
||
return;
|
||
|
||
// 查找SITE元素
|
||
var sites = worldElement.Members();
|
||
if (sites != null)
|
||
{
|
||
foreach (var site in sites)
|
||
{
|
||
if (site != null && !site.IsNull && site.IsValid)
|
||
{
|
||
var siteType = site.GetActualType();
|
||
if (siteType != null && siteType.Name == "SITE")
|
||
{
|
||
// 在SITE下查找ZONE元素
|
||
var zones = site.Members();
|
||
if (zones != null)
|
||
{
|
||
foreach (var zone in zones)
|
||
{
|
||
if (zone != null && !zone.IsNull && zone.IsValid)
|
||
{
|
||
var zoneType = zone.GetActualType();
|
||
if (zoneType != null && zoneType.Name == "ZONE")
|
||
{
|
||
zoneCount++;
|
||
|
||
// 获取Zone名称
|
||
try
|
||
{
|
||
string zoneName = "";
|
||
if (zone.GetValidString(DbAttributeInstance.NAME, ref zoneName))
|
||
{
|
||
if (!string.IsNullOrEmpty(zoneName))
|
||
{
|
||
activeZones.Add(zoneName);
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 如果无法获取名称,跳过
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"统计Zone信息时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private SessionInfo GetSessionInfo()
|
||
{
|
||
try
|
||
{
|
||
// 使用MDB.CurrentMDB获取真实会话信息
|
||
var currentMdb = MDB.CurrentMDB;
|
||
if (currentMdb != null)
|
||
{
|
||
var designDb = currentMdb.GetFirstDB(DbType.Design);
|
||
if (designDb?.CurrentSession != null)
|
||
{
|
||
// 从真实的数据库会话获取信息
|
||
var session = designDb.CurrentSession;
|
||
return new SessionInfo
|
||
{
|
||
UserName = session.User ?? Environment.UserName, // 从DbSession获取用户名
|
||
StartTime = session.Date, // 从DbSession获取会话创建时间
|
||
DurationMinutes = (int)(DateTime.Now - session.Date).TotalMinutes // 计算会话持续时间
|
||
};
|
||
}
|
||
}
|
||
|
||
return new SessionInfo
|
||
{
|
||
UserName = Environment.UserName,
|
||
StartTime = DateTime.Now,
|
||
DurationMinutes = 0
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"获取会话信息失败: {ex.Message}");
|
||
return new SessionInfo
|
||
{
|
||
UserName = Environment.UserName,
|
||
StartTime = DateTime.Now,
|
||
DurationMinutes = 0
|
||
};
|
||
}
|
||
}
|
||
private static bool IsElementValid(DbElement element)
|
||
{
|
||
return element != null && !element.IsNull && element.IsValid;
|
||
}
|
||
|
||
private static string GetElementTypeName(DbElement element)
|
||
{
|
||
try
|
||
{
|
||
var type = element?.GetActualType();
|
||
return type?.Name ?? string.Empty;
|
||
}
|
||
catch
|
||
{
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
private static string BuildElementPath(DbElement element)
|
||
{
|
||
var segments = new List<string>();
|
||
var current = element;
|
||
int guard = 0;
|
||
|
||
while (IsElementValid(current) && guard < 128)
|
||
{
|
||
string name = string.Empty;
|
||
try
|
||
{
|
||
if (!current.GetValidString(DbAttributeInstance.NAME, ref name) || string.IsNullOrEmpty(name))
|
||
{
|
||
name = current.ToString();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
name = current.ToString();
|
||
}
|
||
|
||
segments.Add(name.Trim());
|
||
current = current.Owner;
|
||
guard++;
|
||
}
|
||
|
||
segments.Reverse();
|
||
return "/" + string.Join("/", segments.ToArray());
|
||
}
|
||
|
||
private static bool TryGetElementDb(DbElement element, out Db db)
|
||
{
|
||
db = null;
|
||
if (!IsElementValid(element))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var names = new[] { "DB", "Db", "Database", "OwningDB", "OwnerDB" };
|
||
var type = element.GetType();
|
||
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
var prop = type.GetProperty(names[i]);
|
||
if (prop != null && typeof(Db).IsAssignableFrom(prop.PropertyType))
|
||
{
|
||
try
|
||
{
|
||
db = prop.GetValue(element, null) as Db;
|
||
if (db != null)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static string GetDbNameSafe(Db db)
|
||
{
|
||
if (db == null)
|
||
{
|
||
return "UNKNOWN_DB";
|
||
}
|
||
|
||
try
|
||
{
|
||
return db.Name ?? db.ToString();
|
||
}
|
||
catch
|
||
{
|
||
return "UNKNOWN_DB";
|
||
}
|
||
}
|
||
|
||
private static bool IsDesignDb(Db db)
|
||
{
|
||
if (db == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
var property = db.GetType().GetProperty("Type");
|
||
if (property == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var value = property.GetValue(db, null);
|
||
if (value is DbType)
|
||
{
|
||
return (DbType)value == DbType.Design;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsElementUnderOwner(DbElement element, DbElement expectedOwner)
|
||
{
|
||
if (!IsElementValid(element) || !IsElementValid(expectedOwner))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var current = element;
|
||
int guard = 0;
|
||
while (IsElementValid(current) && guard < 512)
|
||
{
|
||
if (object.ReferenceEquals(current, expectedOwner))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
current = current.Owner;
|
||
guard++;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool ShouldSkipLinkedElement(DbElement element, bool skipLinkedElements, out string reason)
|
||
{
|
||
reason = null;
|
||
|
||
if (!skipLinkedElements || !IsElementValid(element))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!element.IsDeleteable)
|
||
{
|
||
reason = "IsDeleteable=false";
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
string attributeName;
|
||
string attributeValue;
|
||
if (TryFindLinkedAttributeValue(element, out attributeName, out attributeValue))
|
||
{
|
||
reason = string.IsNullOrEmpty(attributeValue)
|
||
? attributeName
|
||
: (attributeName + "=" + attributeValue);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool TryFindLinkedAttributeValue(DbElement element, out string attributeName, out string attributeValue)
|
||
{
|
||
attributeName = null;
|
||
attributeValue = null;
|
||
|
||
if (!IsElementValid(element))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
foreach (var candidate in LinkedAttributeCandidates)
|
||
{
|
||
DbAttribute attribute;
|
||
if (!TryGetDbAttributeByName(candidate, out attribute) || attribute == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool isValid;
|
||
try
|
||
{
|
||
isValid = element.IsAttributeValid(attribute);
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!isValid)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
string textValue = string.Empty;
|
||
try
|
||
{
|
||
if (element.GetValidAsString(attribute, ref textValue) && HasMeaningfulLinkedValue(textValue))
|
||
{
|
||
attributeName = candidate;
|
||
attributeValue = SummarizeValue(textValue);
|
||
return true;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore and fallback to array check
|
||
}
|
||
|
||
try
|
||
{
|
||
var refs = element.GetElementArray(attribute);
|
||
if (refs != null)
|
||
{
|
||
for (int i = 0; i < refs.Length; i++)
|
||
{
|
||
if (IsElementValid(refs[i]))
|
||
{
|
||
attributeName = candidate;
|
||
attributeValue = "REFS>0";
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool TryGetDbAttributeByName(string name, out DbAttribute attribute)
|
||
{
|
||
attribute = null;
|
||
if (string.IsNullOrEmpty(name))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
lock (_attributeCacheLock)
|
||
{
|
||
if (_attributeCache.TryGetValue(name, out attribute))
|
||
{
|
||
return attribute != null;
|
||
}
|
||
}
|
||
|
||
DbAttribute resolved = null;
|
||
try
|
||
{
|
||
var flags = System.Reflection.BindingFlags.Public |
|
||
System.Reflection.BindingFlags.Static |
|
||
System.Reflection.BindingFlags.IgnoreCase;
|
||
|
||
var field = typeof(DbAttributeInstance).GetField(name, flags);
|
||
if (field != null)
|
||
{
|
||
resolved = field.GetValue(null) as DbAttribute;
|
||
}
|
||
|
||
if (resolved == null)
|
||
{
|
||
var property = typeof(DbAttributeInstance).GetProperty(name, flags);
|
||
if (property != null)
|
||
{
|
||
resolved = property.GetValue(null, null) as DbAttribute;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
resolved = null;
|
||
}
|
||
|
||
lock (_attributeCacheLock)
|
||
{
|
||
_attributeCache[name] = resolved;
|
||
}
|
||
|
||
attribute = resolved;
|
||
return attribute != null;
|
||
}
|
||
|
||
private static bool HasMeaningfulLinkedValue(string value)
|
||
{
|
||
if (IsNullOrWhiteSpace(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var normalized = value.Trim().ToUpperInvariant();
|
||
switch (normalized)
|
||
{
|
||
case "0":
|
||
case "FALSE":
|
||
case "NONE":
|
||
case "NO":
|
||
case "NULREF":
|
||
case "NULL":
|
||
case "UNSET":
|
||
case "UNDEFINED":
|
||
case "[]":
|
||
case "{}":
|
||
return false;
|
||
}
|
||
|
||
if (normalized.IndexOf("NULREF", StringComparison.Ordinal) >= 0 ||
|
||
normalized.IndexOf("UNSET", StringComparison.Ordinal) >= 0 ||
|
||
normalized.IndexOf("UNDEFINED", StringComparison.Ordinal) >= 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool hasLetter = false;
|
||
bool hasSlash = false;
|
||
for (int i = 0; i < normalized.Length; i++)
|
||
{
|
||
char ch = normalized[i];
|
||
if (char.IsLetter(ch))
|
||
{
|
||
hasLetter = true;
|
||
break;
|
||
}
|
||
if (ch == '/')
|
||
{
|
||
hasSlash = true;
|
||
}
|
||
}
|
||
|
||
if (!hasLetter && !hasSlash)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private static string SummarizeValue(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
{
|
||
return value;
|
||
}
|
||
|
||
var trimmed = value.Trim();
|
||
const int maxLength = 64;
|
||
if (trimmed.Length <= maxLength)
|
||
{
|
||
return trimmed;
|
||
}
|
||
|
||
return trimmed.Substring(0, maxLength) + "...";
|
||
}
|
||
|
||
private static bool IsNullOrWhiteSpace(string value)
|
||
{
|
||
if (value == null)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
for (int i = 0; i < value.Length; i++)
|
||
{
|
||
if (!char.IsWhiteSpace(value[i]))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private class SimplifyContext
|
||
{
|
||
private const int MaxRemovedSnapshots = 200;
|
||
private const int MaxSkippedSnapshots = 200;
|
||
|
||
public SimplifyContext(SimplifyModelRequest request, SimplifyModelResult result)
|
||
{
|
||
Request = request;
|
||
Result = result;
|
||
_keepTypes = new HashSet<string>(request.KeepTypes ?? new List<string>());
|
||
_removeTypes = new HashSet<string>(request.RemoveTypes ?? new List<string>());
|
||
}
|
||
|
||
public SimplifyModelRequest Request { get; }
|
||
public SimplifyModelResult Result { get; }
|
||
public string CurrentZoneName { get; private set; }
|
||
|
||
private readonly HashSet<string> _keepTypes;
|
||
private readonly HashSet<string> _removeTypes;
|
||
|
||
public bool ShouldProcessZone(DbElement zone)
|
||
{
|
||
if (Request.ZoneFilters == null || Request.ZoneFilters.Count == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var zonePath = BuildElementPath(zone).ToUpperInvariant();
|
||
return Request.ZoneFilters.Any(filter => zonePath.Contains(filter));
|
||
}
|
||
|
||
public void EnterZone(DbElement zone)
|
||
{
|
||
CurrentZoneName = BuildElementPath(zone);
|
||
if (!Result.ZoneSummaries.Contains(CurrentZoneName))
|
||
{
|
||
Result.ZoneSummaries.Add(CurrentZoneName);
|
||
}
|
||
}
|
||
|
||
public void ProcessChildren(DbElement parent)
|
||
{
|
||
var members = parent.Members();
|
||
if (members == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var child in members)
|
||
{
|
||
ProcessElement(child);
|
||
}
|
||
}
|
||
|
||
private void ProcessElement(DbElement element)
|
||
{
|
||
if (!IsElementValid(element))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Result.TotalVisited++;
|
||
var typeName = GetElementTypeName(element);
|
||
var normalizedType = string.IsNullOrEmpty(typeName) ? string.Empty : typeName.ToUpperInvariant();
|
||
|
||
bool keep = _keepTypes.Contains(normalizedType);
|
||
bool remove = _removeTypes.Contains(normalizedType) && !keep;
|
||
|
||
if (remove)
|
||
{
|
||
RemoveElement(element, normalizedType);
|
||
return;
|
||
}
|
||
|
||
Result.KeptCount++;
|
||
ProcessChildren(element);
|
||
}
|
||
|
||
private void RemoveElement(DbElement element, string typeName)
|
||
{
|
||
var elementPath = BuildElementPath(element);
|
||
|
||
try
|
||
{
|
||
string linkedReason;
|
||
if (ShouldSkipLinkedElement(element, Request.SkipLinkedElements, out linkedReason))
|
||
{
|
||
Result.KeptCount++;
|
||
SnapshotSkipped(elementPath, typeName, linkedReason);
|
||
return;
|
||
}
|
||
|
||
if (Request.DryRun)
|
||
{
|
||
SnapshotRemoval(elementPath, typeName, true);
|
||
return;
|
||
}
|
||
|
||
element.Delete();
|
||
SnapshotRemoval(elementPath, typeName, false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Result.Errors.Add($"删除元素 {elementPath} 失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void SnapshotRemoval(string elementPath, string typeName, bool dryRun)
|
||
{
|
||
Result.RemovedCount++;
|
||
|
||
if (Result.RemovedElements.Count < MaxRemovedSnapshots)
|
||
{
|
||
Result.RemovedElements.Add((dryRun ? "[DRYRUN-DEL] " : "[DEL] ") + elementPath + " (" + typeName + ")");
|
||
}
|
||
}
|
||
|
||
private void SnapshotSkipped(string elementPath, string typeName, string reason)
|
||
{
|
||
Result.SkippedLinkedCount++;
|
||
|
||
if (Result.SkippedLinkedElements.Count < MaxSkippedSnapshots)
|
||
{
|
||
var suffix = string.IsNullOrEmpty(reason) ? string.Empty : (" [" + reason + "]");
|
||
Result.SkippedLinkedElements.Add("[SKIP-LINKED] " + elementPath + " (" + typeName + ")" + suffix);
|
||
}
|
||
}
|
||
}
|
||
|
||
private class ShrinkwrapContext
|
||
{
|
||
private const int MaxRemovedSnapshots = 200;
|
||
private const int MaxSkippedSnapshots = 200;
|
||
private const float MinBoxEdge = 0.001f;
|
||
private readonly List<Db> _touchedWritableDbs;
|
||
|
||
public ShrinkwrapContext(ShrinkwrapModelRequest request, ShrinkwrapModelResult result)
|
||
{
|
||
Request = request;
|
||
Result = result;
|
||
_keepTypes = new HashSet<string>(request.KeepTypes ?? new List<string>());
|
||
_shellBoxes = new List<float[]>();
|
||
_touchedWritableDbs = new List<Db>();
|
||
}
|
||
|
||
public ShrinkwrapModelRequest Request { get; }
|
||
public ShrinkwrapModelResult Result { get; }
|
||
public string CurrentZoneName { get; private set; }
|
||
|
||
private readonly HashSet<string> _keepTypes;
|
||
private float[] _zoneBox;
|
||
private float[] _innerBox;
|
||
private float[] _innerDeleteBox;
|
||
private List<float[]> _shellBoxes;
|
||
|
||
public bool ShouldProcessZone(DbElement zone)
|
||
{
|
||
if (Request.ZoneFilters == null || Request.ZoneFilters.Count == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var zonePath = BuildElementPath(zone).ToUpperInvariant();
|
||
return Request.ZoneFilters.Any(filter => zonePath.Contains(filter));
|
||
}
|
||
|
||
public void EnterZone(DbElement zone)
|
||
{
|
||
CurrentZoneName = BuildElementPath(zone);
|
||
if (!Result.ZoneSummaries.Contains(CurrentZoneName))
|
||
{
|
||
Result.ZoneSummaries.Add(CurrentZoneName);
|
||
}
|
||
}
|
||
|
||
public void ProcessZone(DbElement zone)
|
||
{
|
||
if (!TryGetZoneBounds(zone, out _zoneBox))
|
||
{
|
||
SnapshotUnsafe(BuildElementPath(zone), "ZONE", "Zone及其子元素均无法计算包围盒");
|
||
return;
|
||
}
|
||
|
||
_innerBox = BuildInnerBox(_zoneBox, (float)Math.Max(0d, Request.Padding));
|
||
_innerDeleteBox = BuildDeleteCoreBox(_innerBox, (float)Math.Max(0d, Request.TouchTolerance));
|
||
_shellBoxes = BuildShellBoxes(_zoneBox, _innerDeleteBox);
|
||
|
||
if (_innerDeleteBox == null || _shellBoxes == null || _shellBoxes.Count == 0)
|
||
{
|
||
SnapshotUnsafe(BuildElementPath(zone), "ZONE", "Zone几何边界不足以构建保壳删除区域");
|
||
return;
|
||
}
|
||
|
||
if (!ProcessZoneWithExactSpatial(zone))
|
||
{
|
||
Result.UsedExactSpatial = false;
|
||
ProcessFallbackCandidates(zone);
|
||
}
|
||
}
|
||
|
||
public IList<Db> GetTouchedWritableDbs()
|
||
{
|
||
return _touchedWritableDbs;
|
||
}
|
||
|
||
private bool TryGetZoneBounds(DbElement zone, out float[] box)
|
||
{
|
||
box = null;
|
||
if (!IsElementValid(zone))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (TryGetLimitsBox(zone, out box))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var aggregate = new List<float[]>();
|
||
CollectDescendantBoxes(zone, aggregate);
|
||
if (aggregate.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
box = MergeBoxes(aggregate);
|
||
return box != null;
|
||
}
|
||
|
||
private void CollectDescendantBoxes(DbElement parent, List<float[]> boxes)
|
||
{
|
||
var members = SnapshotMembers(parent);
|
||
for (int i = 0; i < members.Count; i++)
|
||
{
|
||
var child = members[i];
|
||
if (!IsElementValid(child))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
float[] childBox;
|
||
if (TryGetLimitsBox(child, out childBox))
|
||
{
|
||
boxes.Add(childBox);
|
||
}
|
||
|
||
CollectDescendantBoxes(child, boxes);
|
||
}
|
||
}
|
||
|
||
private bool ProcessZoneWithExactSpatial(DbElement zone)
|
||
{
|
||
if (_shellBoxes == null || _shellBoxes.Count == 0 || _innerDeleteBox == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var spatial = Spatial.Instance;
|
||
if (spatial == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
DbElement[] candidates;
|
||
try
|
||
{
|
||
candidates = spatial.ElementsInElementExact(zone, true);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (candidates == null || candidates.Length == 0)
|
||
{
|
||
Result.UsedExactSpatial = true;
|
||
return true;
|
||
}
|
||
|
||
Array.Sort(candidates, delegate (DbElement a, DbElement b)
|
||
{
|
||
return GetOwnerDepth(b).CompareTo(GetOwnerDepth(a));
|
||
});
|
||
|
||
Result.UsedExactSpatial = true;
|
||
foreach (var element in candidates)
|
||
{
|
||
ProcessExactCandidate(element, zone);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void ProcessExactCandidate(DbElement element, DbElement zone)
|
||
{
|
||
if (!IsElementValid(element))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Result.TotalVisited++;
|
||
|
||
var typeName = GetElementTypeName(element);
|
||
var normalizedType = string.IsNullOrEmpty(typeName) ? string.Empty : typeName.ToUpperInvariant();
|
||
|
||
if (_keepTypes.Contains(normalizedType))
|
||
{
|
||
Result.KeptCount++;
|
||
return;
|
||
}
|
||
|
||
if (OverlapsAnyShellBox(element))
|
||
{
|
||
Result.ShellKeptCount++;
|
||
Result.KeptCount++;
|
||
return;
|
||
}
|
||
|
||
bool overlapsDeleteCore;
|
||
if (!TryElementInBox(element, _innerDeleteBox, out overlapsDeleteCore))
|
||
{
|
||
float[] box;
|
||
if (TryGetLimitsBox(element, out box) && IsInsideInnerBox(box, _innerDeleteBox, 0f))
|
||
{
|
||
RemoveElement(element, normalizedType, zone, "exact-inner");
|
||
return;
|
||
}
|
||
|
||
SnapshotUnsafe(elementPath: BuildElementPath(element), typeName: normalizedType, reason: "无法稳定计算删除核包围盒关系");
|
||
return;
|
||
}
|
||
|
||
if (overlapsDeleteCore)
|
||
{
|
||
RemoveElement(element, normalizedType, zone, "exact-overlap");
|
||
return;
|
||
}
|
||
|
||
Result.KeptCount++;
|
||
}
|
||
|
||
private void ProcessFallbackCandidates(DbElement zone)
|
||
{
|
||
var candidates = new List<ShrinkwrapCandidate>();
|
||
CollectFallbackCandidates(zone, candidates, zone);
|
||
|
||
candidates.Sort(delegate (ShrinkwrapCandidate a, ShrinkwrapCandidate b)
|
||
{
|
||
return b.Depth.CompareTo(a.Depth);
|
||
});
|
||
|
||
for (int i = 0; i < candidates.Count; i++)
|
||
{
|
||
ProcessFallbackCandidate(candidates[i], zone);
|
||
}
|
||
}
|
||
|
||
private void CollectFallbackCandidates(DbElement parent, List<ShrinkwrapCandidate> candidates, DbElement zone)
|
||
{
|
||
var members = SnapshotMembers(parent);
|
||
for (int i = 0; i < members.Count; i++)
|
||
{
|
||
var child = members[i];
|
||
if (!IsElementValid(child))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
candidates.Add(new ShrinkwrapCandidate(child, GetOwnerDepth(child)));
|
||
CollectFallbackCandidates(child, candidates, zone);
|
||
}
|
||
}
|
||
|
||
private void ProcessFallbackCandidate(ShrinkwrapCandidate candidate, DbElement zone)
|
||
{
|
||
var element = candidate.Element;
|
||
if (!IsElementValid(element))
|
||
{
|
||
return;
|
||
}
|
||
|
||
Result.TotalVisited++;
|
||
|
||
var typeName = GetElementTypeName(element);
|
||
var normalizedType = string.IsNullOrEmpty(typeName) ? string.Empty : typeName.ToUpperInvariant();
|
||
|
||
if (_keepTypes.Contains(normalizedType))
|
||
{
|
||
Result.KeptCount++;
|
||
return;
|
||
}
|
||
|
||
if (OverlapsAnyShellBox(element))
|
||
{
|
||
Result.ShellKeptCount++;
|
||
Result.KeptCount++;
|
||
return;
|
||
}
|
||
|
||
float[] box;
|
||
if (!TryGetLimitsBox(element, out box))
|
||
{
|
||
SnapshotUnsafe(BuildElementPath(element), normalizedType, "无法计算包围盒");
|
||
return;
|
||
}
|
||
|
||
if (IsInsideInnerBox(box, _innerDeleteBox, 0f))
|
||
{
|
||
RemoveElement(element, normalizedType, zone, "fallback-inner");
|
||
return;
|
||
}
|
||
|
||
Result.KeptCount++;
|
||
}
|
||
|
||
private bool OverlapsAnyShellBox(DbElement element)
|
||
{
|
||
if (_shellBoxes == null || _shellBoxes.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
foreach (var shellBox in _shellBoxes)
|
||
{
|
||
bool overlaps;
|
||
if (TryElementInBox(element, shellBox, out overlaps) && overlaps)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void RemoveElement(DbElement element, string typeName, DbElement zone, string source)
|
||
{
|
||
var elementPath = BuildElementPath(element);
|
||
|
||
try
|
||
{
|
||
string linkedReason;
|
||
if (ShouldSkipLinkedElement(element, Request.SkipLinkedElements, out linkedReason))
|
||
{
|
||
Result.KeptCount++;
|
||
SnapshotSkipped(elementPath, typeName, linkedReason);
|
||
return;
|
||
}
|
||
|
||
string unsafeReason;
|
||
Db elementDb;
|
||
if (!IsSafeToDelete(element, zone, out elementDb, out unsafeReason))
|
||
{
|
||
SnapshotUnsafe(elementPath, typeName, unsafeReason);
|
||
return;
|
||
}
|
||
|
||
if (Request.DryRun)
|
||
{
|
||
SnapshotRemoval(elementPath, typeName, true);
|
||
return;
|
||
}
|
||
|
||
element.Delete();
|
||
SnapshotRemoval(elementPath, typeName, false);
|
||
TrackTouchedDb(elementDb);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Result.Errors.Add("删除元素 " + elementPath + " 失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
private void SnapshotRemoval(string elementPath, string typeName, bool dryRun)
|
||
{
|
||
Result.RemovedCount++;
|
||
|
||
if (Result.RemovedElements.Count < MaxRemovedSnapshots)
|
||
{
|
||
Result.RemovedElements.Add((dryRun ? "[DRYRUN-DEL] " : "[DEL] ") + elementPath + " (" + typeName + ")");
|
||
}
|
||
}
|
||
|
||
private void SnapshotSkipped(string elementPath, string typeName, string reason)
|
||
{
|
||
Result.SkippedLinkedCount++;
|
||
|
||
if (Result.SkippedLinkedElements.Count < MaxSkippedSnapshots)
|
||
{
|
||
var suffix = string.IsNullOrEmpty(reason) ? string.Empty : (" [" + reason + "]");
|
||
Result.SkippedLinkedElements.Add("[SKIP-LINKED] " + elementPath + " (" + typeName + ")" + suffix);
|
||
}
|
||
}
|
||
|
||
private void SnapshotUnsafe(string elementPath, string typeName, string reason)
|
||
{
|
||
Result.KeptCount++;
|
||
Result.SkippedUnsafeCount++;
|
||
|
||
if (Result.SkippedUnsafeElements.Count < MaxSkippedSnapshots)
|
||
{
|
||
var suffix = string.IsNullOrEmpty(reason) ? string.Empty : (" [" + reason + "]");
|
||
Result.SkippedUnsafeElements.Add("[SKIP-UNSAFE] " + elementPath + " (" + typeName + ")" + suffix);
|
||
}
|
||
}
|
||
|
||
private bool IsSafeToDelete(DbElement element, DbElement zone, out Db elementDb, out string reason)
|
||
{
|
||
elementDb = null;
|
||
reason = null;
|
||
|
||
if (!IsElementValid(element))
|
||
{
|
||
reason = "元素无效";
|
||
return false;
|
||
}
|
||
|
||
if (!IsElementValid(zone))
|
||
{
|
||
reason = "Zone无效";
|
||
return false;
|
||
}
|
||
|
||
var owner = element.Owner;
|
||
if (!IsElementValid(owner))
|
||
{
|
||
reason = "Owner无效";
|
||
return false;
|
||
}
|
||
|
||
if (!IsElementUnderOwner(element, zone))
|
||
{
|
||
reason = "元素已不在目标Zone下";
|
||
return false;
|
||
}
|
||
|
||
if (!TryGetElementDb(element, out elementDb) || elementDb == null)
|
||
{
|
||
reason = "无法确认元素所属DB";
|
||
return false;
|
||
}
|
||
|
||
if (!IsDesignDb(elementDb))
|
||
{
|
||
reason = "元素所属DB不是Design DB";
|
||
return false;
|
||
}
|
||
|
||
if (IsDbReadOnly(elementDb))
|
||
{
|
||
reason = "元素所属DB为只读";
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void TrackTouchedDb(Db db)
|
||
{
|
||
if (db == null || IsDbReadOnly(db))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!ContainsDb(_touchedWritableDbs, db))
|
||
{
|
||
_touchedWritableDbs.Add(db);
|
||
}
|
||
}
|
||
|
||
private static List<DbElement> SnapshotMembers(DbElement parent)
|
||
{
|
||
var snapshot = new List<DbElement>();
|
||
if (!IsElementValid(parent))
|
||
{
|
||
return snapshot;
|
||
}
|
||
|
||
DbElement[] members = null;
|
||
try
|
||
{
|
||
members = parent.Members();
|
||
}
|
||
catch
|
||
{
|
||
return snapshot;
|
||
}
|
||
|
||
if (members == null)
|
||
{
|
||
return snapshot;
|
||
}
|
||
|
||
for (int i = 0; i < members.Length; i++)
|
||
{
|
||
snapshot.Add(members[i]);
|
||
}
|
||
|
||
return snapshot;
|
||
}
|
||
|
||
private static int GetOwnerDepth(DbElement element)
|
||
{
|
||
var current = element;
|
||
int depth = 0;
|
||
int guard = 0;
|
||
|
||
while (IsElementValid(current) && guard < 512)
|
||
{
|
||
depth++;
|
||
current = current.Owner;
|
||
guard++;
|
||
}
|
||
|
||
return depth;
|
||
}
|
||
|
||
private static bool TryElementInBox(DbElement element, float[] box, out bool overlaps)
|
||
{
|
||
overlaps = false;
|
||
if (!IsElementValid(element) || box == null || box.Length < 6)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
float[] elementBox;
|
||
if (!TryGetLimitsBox(element, out elementBox))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
overlaps = BoxesOverlap(elementBox, box);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static bool TryGetLimitsBox(DbElement element, out float[] box)
|
||
{
|
||
box = null;
|
||
if (!IsElementValid(element))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
var spatial = Spatial.Instance;
|
||
if (spatial == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var spatialType = spatial.GetType();
|
||
|
||
var arrayMethod = spatialType.GetMethod("LimitsBox", new[] { typeof(DbElement), typeof(float[]) });
|
||
if (arrayMethod != null)
|
||
{
|
||
var args = new object[] { element, new float[6] };
|
||
var okObj = arrayMethod.Invoke(spatial, args);
|
||
if (okObj is bool && (bool)okObj)
|
||
{
|
||
if (TryNormalizeBox(args[1] as float[], out box))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
var methods = spatialType.GetMethods().Where(m => m.Name == "LimitsBox").ToArray();
|
||
foreach (var method in methods)
|
||
{
|
||
var parameters = method.GetParameters();
|
||
if (parameters.Length != 2 || parameters[0].ParameterType != typeof(DbElement) || !parameters[1].ParameterType.IsByRef)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
object boxValue;
|
||
try
|
||
{
|
||
boxValue = Activator.CreateInstance(parameters[1].ParameterType.GetElementType());
|
||
}
|
||
catch
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var args = new object[] { element, boxValue };
|
||
var okObj = method.Invoke(spatial, args);
|
||
if (!(okObj is bool) || !(bool)okObj)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var converted = ConvertLimitsBoxObject(args[1]);
|
||
if (TryNormalizeBox(converted, out box))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static float[] ConvertLimitsBoxObject(object limits)
|
||
{
|
||
if (limits == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var type = limits.GetType();
|
||
|
||
try
|
||
{
|
||
float xmin = ReadFloat(limits, type, new[] { "XMIN", "XMin", "MinX", "Xmin" });
|
||
float ymin = ReadFloat(limits, type, new[] { "YMIN", "YMin", "MinY", "Ymin" });
|
||
float zmin = ReadFloat(limits, type, new[] { "ZMIN", "ZMin", "MinZ", "Zmin" });
|
||
float xmax = ReadFloat(limits, type, new[] { "XMAX", "XMax", "MaxX", "Xmax" });
|
||
float ymax = ReadFloat(limits, type, new[] { "YMAX", "YMax", "MaxY", "Ymax" });
|
||
float zmax = ReadFloat(limits, type, new[] { "ZMAX", "ZMax", "MaxZ", "Zmax" });
|
||
return new[] { xmin, ymin, zmin, xmax, ymax, zmax };
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static float ReadFloat(object boxed, Type type, string[] names)
|
||
{
|
||
foreach (var name in names)
|
||
{
|
||
var prop = type.GetProperty(name);
|
||
if (prop != null)
|
||
{
|
||
var value = prop.GetValue(boxed, null);
|
||
if (value != null)
|
||
{
|
||
return Convert.ToSingle(value);
|
||
}
|
||
}
|
||
|
||
var field = type.GetField(name);
|
||
if (field != null)
|
||
{
|
||
var value = field.GetValue(boxed);
|
||
if (value != null)
|
||
{
|
||
return Convert.ToSingle(value);
|
||
}
|
||
}
|
||
}
|
||
|
||
throw new InvalidOperationException("LimitsBox缺少必要字段/属性");
|
||
}
|
||
|
||
private static bool TryNormalizeBox(float[] source, out float[] normalized)
|
||
{
|
||
normalized = null;
|
||
if (source == null || source.Length < 6)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
float xmin = Math.Min(source[0], source[3]);
|
||
float ymin = Math.Min(source[1], source[4]);
|
||
float zmin = Math.Min(source[2], source[5]);
|
||
float xmax = Math.Max(source[0], source[3]);
|
||
float ymax = Math.Max(source[1], source[4]);
|
||
float zmax = Math.Max(source[2], source[5]);
|
||
|
||
if ((xmax - xmin) < MinBoxEdge || (ymax - ymin) < MinBoxEdge || (zmax - zmin) < MinBoxEdge)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
normalized = new[] { xmin, ymin, zmin, xmax, ymax, zmax };
|
||
return true;
|
||
}
|
||
|
||
private static float[] BuildInnerBox(float[] outerBox, float padding)
|
||
{
|
||
if (outerBox == null || outerBox.Length < 6)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var inner = new float[6];
|
||
inner[0] = outerBox[0] + padding;
|
||
inner[1] = outerBox[1] + padding;
|
||
inner[2] = outerBox[2] + padding;
|
||
inner[3] = outerBox[3] - padding;
|
||
inner[4] = outerBox[4] - padding;
|
||
inner[5] = outerBox[5] - padding;
|
||
|
||
if (inner[3] < inner[0])
|
||
{
|
||
inner[0] = outerBox[0];
|
||
inner[3] = outerBox[3];
|
||
}
|
||
|
||
if (inner[4] < inner[1])
|
||
{
|
||
inner[1] = outerBox[1];
|
||
inner[4] = outerBox[4];
|
||
}
|
||
|
||
if (inner[5] < inner[2])
|
||
{
|
||
inner[2] = outerBox[2];
|
||
inner[5] = outerBox[5];
|
||
}
|
||
|
||
return inner;
|
||
}
|
||
|
||
private static float[] BuildDeleteCoreBox(float[] innerBox, float tolerance)
|
||
{
|
||
if (innerBox == null || innerBox.Length < 6)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var core = new float[6];
|
||
var offset = Math.Max(0f, tolerance);
|
||
core[0] = innerBox[0] + offset;
|
||
core[1] = innerBox[1] + offset;
|
||
core[2] = innerBox[2] + offset;
|
||
core[3] = innerBox[3] - offset;
|
||
core[4] = innerBox[4] - offset;
|
||
core[5] = innerBox[5] - offset;
|
||
|
||
float[] normalized;
|
||
return TryNormalizeBox(core, out normalized) ? normalized : null;
|
||
}
|
||
|
||
private static List<float[]> BuildShellBoxes(float[] outerBox, float[] innerDeleteBox)
|
||
{
|
||
var boxes = new List<float[]>();
|
||
if (outerBox == null || innerDeleteBox == null)
|
||
{
|
||
return boxes;
|
||
}
|
||
|
||
AddShellBox(boxes, outerBox[0], outerBox[1], outerBox[2], innerDeleteBox[0], outerBox[4], outerBox[5]); // Left
|
||
AddShellBox(boxes, innerDeleteBox[3], outerBox[1], outerBox[2], outerBox[3], outerBox[4], outerBox[5]); // Right
|
||
AddShellBox(boxes, innerDeleteBox[0], outerBox[1], outerBox[2], innerDeleteBox[3], innerDeleteBox[1], outerBox[5]); // Front
|
||
AddShellBox(boxes, innerDeleteBox[0], innerDeleteBox[4], outerBox[2], innerDeleteBox[3], outerBox[4], outerBox[5]); // Back
|
||
AddShellBox(boxes, innerDeleteBox[0], innerDeleteBox[1], outerBox[2], innerDeleteBox[3], innerDeleteBox[4], innerDeleteBox[2]); // Bottom
|
||
AddShellBox(boxes, innerDeleteBox[0], innerDeleteBox[1], innerDeleteBox[5], innerDeleteBox[3], innerDeleteBox[4], outerBox[5]); // Top
|
||
|
||
return boxes;
|
||
}
|
||
|
||
private static void AddShellBox(List<float[]> boxes, float xmin, float ymin, float zmin, float xmax, float ymax, float zmax)
|
||
{
|
||
var candidate = new[] { xmin, ymin, zmin, xmax, ymax, zmax };
|
||
float[] normalized;
|
||
if (TryNormalizeBox(candidate, out normalized))
|
||
{
|
||
boxes.Add(normalized);
|
||
}
|
||
}
|
||
|
||
private static bool IsInsideInnerBox(float[] box, float[] innerBox, float tolerance)
|
||
{
|
||
if (box == null || innerBox == null || box.Length < 6 || innerBox.Length < 6)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return (box[0] > innerBox[0] + tolerance) &&
|
||
(box[1] > innerBox[1] + tolerance) &&
|
||
(box[2] > innerBox[2] + tolerance) &&
|
||
(box[3] < innerBox[3] - tolerance) &&
|
||
(box[4] < innerBox[4] - tolerance) &&
|
||
(box[5] < innerBox[5] - tolerance);
|
||
}
|
||
|
||
private static bool BoxesOverlap(float[] a, float[] b)
|
||
{
|
||
if (a == null || b == null || a.Length < 6 || b.Length < 6)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return (a[0] <= b[3]) && (a[3] >= b[0]) &&
|
||
(a[1] <= b[4]) && (a[4] >= b[1]) &&
|
||
(a[2] <= b[5]) && (a[5] >= b[2]);
|
||
}
|
||
|
||
private static float[] MergeBoxes(IList<float[]> boxes)
|
||
{
|
||
if (boxes == null || boxes.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
float xmin = float.MaxValue;
|
||
float ymin = float.MaxValue;
|
||
float zmin = float.MaxValue;
|
||
float xmax = float.MinValue;
|
||
float ymax = float.MinValue;
|
||
float zmax = float.MinValue;
|
||
bool found = false;
|
||
|
||
for (int i = 0; i < boxes.Count; i++)
|
||
{
|
||
var box = boxes[i];
|
||
if (box == null || box.Length < 6)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
xmin = Math.Min(xmin, box[0]);
|
||
ymin = Math.Min(ymin, box[1]);
|
||
zmin = Math.Min(zmin, box[2]);
|
||
xmax = Math.Max(xmax, box[3]);
|
||
ymax = Math.Max(ymax, box[4]);
|
||
zmax = Math.Max(zmax, box[5]);
|
||
found = true;
|
||
}
|
||
|
||
if (!found)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
float[] merged;
|
||
return TryNormalizeBox(new[] { xmin, ymin, zmin, xmax, ymax, zmax }, out merged) ? merged : null;
|
||
}
|
||
|
||
private class ShrinkwrapCandidate
|
||
{
|
||
public ShrinkwrapCandidate(DbElement element, int depth)
|
||
{
|
||
Element = element;
|
||
Depth = depth;
|
||
}
|
||
|
||
public DbElement Element { get; private set; }
|
||
public int Depth { get; private set; }
|
||
}
|
||
}
|
||
}
|
||
}
|