feat: F2 碰撞对象映射回主模型(层级链为桥)+ F1 稳定性修复
F2: - 副实例 report collidedObjects 升级为对象详情(name + 副 PathId + ancestorChain) - 主 NW 导出时构建盒内对象映射(chain去根 → 主 PathId) - 碰撞对象按层级链(去根一致)映射回主模型 PathId,报告可定位(实测 4/4 映射成功) 稳定性修复(F1 间歇失败根因): - UnitsConverter 缓存 NaN 兜底:无参因子方法 NaN 时用显式 ActiveDocument.Units(副实例导出 NWD 单位不稳定,两缓存因子不同步) - 副实例数据库就绪等待:PathPlanningManager.IsDatabaseReady + status 端点暴露 + F1 导入前轮询(修复'路径先导入、库后创建'导致路径丢失→检测记录外键失败) - 修复 UnitsConverter 编辑事故(无参/显式 GetMetersToUnitsConversionFactor 重复) 验证:F1 Ground + Chair Lounge Couch Double,碰撞 28/28,F2 映射 4/4
This commit is contained in:
parent
832caaffef
commit
98d01595c3
@ -501,6 +501,18 @@ namespace NavisworksTransport.Core
|
||||
LogManager.Info(
|
||||
$"[批处理] 剖面盒自动检测完成: {pathRoute.Name}, 碰撞={detection.TotalCollisions}, " +
|
||||
$"唯一对象={detection.UniqueCollidedObjectsCount}, 对象=[{string.Join(", ", detection.CollidedObjects)}]");
|
||||
|
||||
// F2:碰撞对象映射(副实例 → 主模型 PathId,按层级链匹配)
|
||||
int mappedCount = 0;
|
||||
foreach (var mapping in detection.CollidedObjectMappings)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(mapping.MainPathId))
|
||||
{
|
||||
mappedCount++;
|
||||
LogManager.Info($"[F2映射] {mapping.Name}: 副实例={mapping.SecondaryPathId} -> 主模型={mapping.MainPathId}");
|
||||
}
|
||||
}
|
||||
LogManager.Info($"[F2映射] 完成: {mappedCount}/{detection.CollidedObjectMappings.Count} 个碰撞对象映射到主模型");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -91,6 +91,11 @@ namespace NavisworksTransport
|
||||
/// </summary>
|
||||
public bool IsToolPluginActive => _isToolPluginActive;
|
||||
|
||||
/// <summary>
|
||||
/// 路径数据库是否已初始化(F1 副实例检测前等待数据库就绪,避免路径导入时数据库未建导致丢失)。
|
||||
/// </summary>
|
||||
public bool IsDatabaseReady => _pathDatabase != null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否在预览模式
|
||||
/// </summary>
|
||||
|
||||
@ -50,6 +50,21 @@ namespace NavisworksTransport.Core.Services
|
||||
public int TotalFrames { get; set; }
|
||||
public string LocalModelPath { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// F2:碰撞对象映射(名称 → 主模型 PathId,按层级链匹配)
|
||||
/// </summary>
|
||||
public List<CollidedObjectMapping> CollidedObjectMappings { get; set; } = new List<CollidedObjectMapping>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2:碰撞对象映射(副实例 → 主模型)。
|
||||
/// </summary>
|
||||
public class CollidedObjectMapping
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string SecondaryPathId { get; set; }
|
||||
public string MainPathId { get; set; }
|
||||
}
|
||||
|
||||
private const string DbPathEnvironmentVariable = "TRANSPORTPLUGIN_TEST_DB_PATH";
|
||||
@ -106,8 +121,9 @@ namespace NavisworksTransport.Core.Services
|
||||
|
||||
BoundingBox3D bounds = SectionClipHelper.CalculatePathClipBox(adjustedPoints, 4.0, 2.0);
|
||||
|
||||
// 2. 导出局部 NWD(UI 线程:NW API + 导出)
|
||||
// 2. 导出局部 NWD(UI 线程:NW API + 导出);同时构建盒内对象映射(chain→主PathId,F2 用)
|
||||
bool exported = false;
|
||||
var chainToMainPathId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
exported = System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
@ -120,6 +136,26 @@ namespace NavisworksTransport.Core.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
// F2:记录盒内对象映射(层级链去根 → 主模型 PathId)
|
||||
foreach (ModelItem boxItem in traversal.ObjectsInBox)
|
||||
{
|
||||
string chain = BuildAncestorChain(boxItem);
|
||||
if (string.IsNullOrWhiteSpace(chain))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = document.Models.CreatePathId(boxItem);
|
||||
chainToMainPathId[TrimRootFromChain(chain)] = path.PathId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
LogManager.Info($"[剖面盒批检测] 盒内对象映射: {chainToMainPathId.Count} 条");
|
||||
|
||||
string exportedPath = exporter.ExportToNwd(document, traversal, localModelPath);
|
||||
return !string.IsNullOrEmpty(exportedPath) && File.Exists(exportedPath);
|
||||
});
|
||||
@ -151,6 +187,13 @@ namespace NavisworksTransport.Core.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
// 等待副实例路径数据库就绪(数据库初始化晚于插件加载,路径导入前必须先建库,否则路径丢失)
|
||||
if (!await WaitDatabaseReadyAsync(client, TimeSpan.FromSeconds(60)).ConfigureAwait(false))
|
||||
{
|
||||
result.ErrorMessage = "副实例路径数据库未就绪(60s 超时)";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!await ImportRouteAsync(client, routeFilePath).ConfigureAwait(false))
|
||||
{
|
||||
result.ErrorMessage = "副实例导入路径失败";
|
||||
@ -173,7 +216,8 @@ namespace NavisworksTransport.Core.Services
|
||||
return await RunDetectionAsync(
|
||||
client, route, isVirtualObject, animatedObjectPath,
|
||||
virtualObjectLengthModelUnits, virtualObjectWidthModelUnits,
|
||||
virtualObjectHeightModelUnits, frameRate, durationSeconds, result).ConfigureAwait(false);
|
||||
virtualObjectHeightModelUnits, frameRate, durationSeconds, result,
|
||||
chainToMainPathId).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -298,6 +342,41 @@ namespace NavisworksTransport.Core.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待副实例路径数据库就绪(PathPlanningManager.IsDatabaseReady)。
|
||||
/// 副实例数据库初始化晚于插件加载,路径导入前必须已建库。
|
||||
/// </summary>
|
||||
private static async Task<bool> WaitDatabaseReadyAsync(HttpClient client, TimeSpan timeout)
|
||||
{
|
||||
DateTime deadline = DateTime.UtcNow.Add(timeout);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var response = await client.GetAsync("/api/test/status").ConfigureAwait(false))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
JObject payload = JObject.Parse(content);
|
||||
if (payload.Value<bool?>("ok") == true &&
|
||||
payload["data"]?["pathManager"]?["isDatabaseReady"]?.Value<bool?>() == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<bool> ImportRouteAsync(HttpClient client, string routeFilePath)
|
||||
{
|
||||
string requestUri = "/api/test/import-route-file?path=" + Uri.EscapeDataString(routeFilePath) + "&overwrite=true";
|
||||
@ -442,6 +521,37 @@ namespace NavisworksTransport.Core.Services
|
||||
return string.Join(" / ", segments.Skip(1).Select(s => s.Trim()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造 ModelItem 的层级链(AncestorsAndSelf 的 DisplayName,'/' 分隔)。
|
||||
/// </summary>
|
||||
private static string BuildAncestorChain(ModelItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var segments = new List<string>();
|
||||
var current = item;
|
||||
int guard = 0;
|
||||
|
||||
while (current != null && guard < 15)
|
||||
{
|
||||
var name = current.DisplayName;
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
segments.Add(name.Trim());
|
||||
}
|
||||
current = current.Parent;
|
||||
guard++;
|
||||
}
|
||||
|
||||
segments.Reverse();
|
||||
return string.Join(" / ", segments);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<DetectionResult> RunDetectionAsync(
|
||||
HttpClient client,
|
||||
PathRoute route,
|
||||
@ -452,7 +562,8 @@ namespace NavisworksTransport.Core.Services
|
||||
double objectHeightModelUnits,
|
||||
int frameRate,
|
||||
double durationSeconds,
|
||||
DetectionResult result)
|
||||
DetectionResult result,
|
||||
Dictionary<string, string> chainToMainPathId)
|
||||
{
|
||||
// 动画参数从模型单位换算回米(副实例 config 以米存储)
|
||||
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor();
|
||||
@ -501,7 +612,35 @@ namespace NavisworksTransport.Core.Services
|
||||
JArray collided = (JArray)report["collidedObjects"];
|
||||
if (collided != null)
|
||||
{
|
||||
result.CollidedObjects = collided.Select(t => (string)t).ToList();
|
||||
foreach (JToken token in collided)
|
||||
{
|
||||
JObject obj = (JObject)token;
|
||||
string name = (string)obj["name"];
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mapping = new CollidedObjectMapping
|
||||
{
|
||||
Name = name,
|
||||
SecondaryPathId = (string)obj["pathId"]
|
||||
};
|
||||
|
||||
// F2:按层级链(去根)查盒内对象映射 → 主模型 PathId
|
||||
string chain = (string)obj["ancestorChain"];
|
||||
if (!string.IsNullOrWhiteSpace(chain))
|
||||
{
|
||||
string tail = TrimRootFromChain(chain);
|
||||
if (tail != null && chainToMainPathId != null && chainToMainPathId.TryGetValue(tail, out string mainPathId))
|
||||
{
|
||||
mapping.MainPathId = mainPathId;
|
||||
}
|
||||
}
|
||||
|
||||
result.CollidedObjects.Add(name);
|
||||
result.CollidedObjectMappings.Add(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -477,6 +477,7 @@ namespace NavisworksTransport.Core.Services
|
||||
pathManager = new
|
||||
{
|
||||
isAvailable = pathManager != null,
|
||||
isDatabaseReady = pathManager != null && pathManager.IsDatabaseReady,
|
||||
routeCount = allRoutes?.Count ?? 0,
|
||||
currentRouteId = currentRoute?.Id,
|
||||
currentRouteName = currentRoute?.Name,
|
||||
@ -1151,6 +1152,70 @@ namespace NavisworksTransport.Core.Services
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2:序列化碰撞对象详情(name + 副实例 PathId + 层级链),
|
||||
/// 供主 NW 按层级链(导出保留树结构,去根一致)映射回主模型。
|
||||
/// </summary>
|
||||
private static List<object> SerializeCollidedObjects(
|
||||
List<CollisionResult> allCollisions, List<string> nameList)
|
||||
{
|
||||
var result = new List<object>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (allCollisions != null)
|
||||
{
|
||||
foreach (var collision in allCollisions)
|
||||
{
|
||||
if (collision?.Item2 == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = collision.Item2.DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(name) || !seen.Add(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Count >= 50)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string pathId = null;
|
||||
try
|
||||
{
|
||||
var path = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(collision.Item2);
|
||||
pathId = path.PathId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
result.Add(new
|
||||
{
|
||||
name,
|
||||
pathId,
|
||||
ancestorChain = BuildAncestorNameChain(collision.Item2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fallback:AllCollisions 无 Item2 引用时退回名字列表
|
||||
if (result.Count == 0 && nameList != null)
|
||||
{
|
||||
foreach (string name in nameList.Take(50))
|
||||
{
|
||||
if (seen.Add(name))
|
||||
{
|
||||
result.Add(new { name, pathId = (string)null, ancestorChain = (string)null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private object BuildSelectionPayload()
|
||||
{
|
||||
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
||||
@ -1361,10 +1426,8 @@ namespace NavisworksTransport.Core.Services
|
||||
screenshotCount = report.Screenshots?.Count ?? 0,
|
||||
resultId = report.ResultId,
|
||||
routeId = report.RouteId,
|
||||
// E4:碰撞对象名字列表(用于跨实例报告回传;PathId/位置映射属 F2 阶段)
|
||||
collidedObjects = report.CollidedObjectsList?.Count > 0
|
||||
? report.CollidedObjectsList.Take(50).ToList()
|
||||
: new List<string>()
|
||||
// F2:碰撞对象详情(name + 副实例 PathId + 层级链),用于跨实例报告映射回主模型
|
||||
collidedObjects = SerializeCollidedObjects(report.AllCollisions, report.CollidedObjectsList)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@ -62,10 +62,25 @@ namespace NavisworksTransport.Utils
|
||||
_cachedMetersToUnitsFactor = GetMetersToUnitsConversionFactor(currentUnits);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch
|
||||
{
|
||||
// 出错时保留上次的有效值(如果有),否则保持 NaN 以暴露错误
|
||||
// 不要设置默认值,避免掩盖问题
|
||||
// 主路径异常时,用显式文档单位兜底(副实例/文档加载瞬间 ActiveDocument.Units 访问可能抛,
|
||||
// 显式访问通常可用;避免缓存长期 NaN 导致尺寸/路径长度全错)
|
||||
try
|
||||
{
|
||||
var doc = Application.ActiveDocument;
|
||||
if (doc != null)
|
||||
{
|
||||
var currentUnits = doc.Units;
|
||||
_cachedUnits = currentUnits;
|
||||
_cachedUnitsToMetersFactor = GetUnitsToMetersConversionFactor(currentUnits);
|
||||
_cachedMetersToUnitsFactor = GetMetersToUnitsConversionFactor(currentUnits);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 仍失败则保留上次有效值(或 NaN 暴露错误)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +91,17 @@ namespace NavisworksTransport.Utils
|
||||
public static double GetUnitsToMetersConversionFactor()
|
||||
{
|
||||
UpdateCache();
|
||||
|
||||
// 缓存异常(NaN,副实例/文档加载瞬间)时用显式文档单位兜底
|
||||
if (double.IsNaN(_cachedUnitsToMetersFactor))
|
||||
{
|
||||
var doc = Application.ActiveDocument;
|
||||
if (doc != null)
|
||||
{
|
||||
return GetUnitsToMetersConversionFactor(doc.Units);
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedUnitsToMetersFactor;
|
||||
}
|
||||
|
||||
@ -116,12 +142,24 @@ namespace NavisworksTransport.Utils
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取米转换为Navisworks文档单位的系数
|
||||
/// <summary>
|
||||
/// 获取米转换为文档单位的系数
|
||||
/// </summary>
|
||||
/// <returns>转换系数(米转换为文档单位)</returns>
|
||||
public static double GetMetersToUnitsConversionFactor()
|
||||
{
|
||||
UpdateCache();
|
||||
|
||||
// 缓存异常(NaN,副实例/文档加载瞬间)时用显式文档单位兜底
|
||||
if (double.IsNaN(_cachedMetersToUnitsFactor))
|
||||
{
|
||||
var doc = Application.ActiveDocument;
|
||||
if (doc != null)
|
||||
{
|
||||
return GetMetersToUnitsConversionFactor(doc.Units);
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedMetersToUnitsFactor;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user