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:
tian 2026-08-05 12:33:41 +08:00
parent 832caaffef
commit 98d01595c3
5 changed files with 269 additions and 12 deletions

View File

@ -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;
}

View File

@ -91,6 +91,11 @@ namespace NavisworksTransport
/// </summary>
public bool IsToolPluginActive => _isToolPluginActive;
/// <summary>
/// 路径数据库是否已初始化F1 副实例检测前等待数据库就绪,避免路径导入时数据库未建导致丢失)。
/// </summary>
public bool IsDatabaseReady => _pathDatabase != null;
/// <summary>
/// 获取当前是否在预览模式
/// </summary>

View File

@ -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. 导出局部 NWDUI 线程NW API + 导出)
// 2. 导出局部 NWDUI 线程NW API + 导出)同时构建盒内对象映射chain→主PathIdF2 用)
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);
}
}
}

View File

@ -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)
});
}
}
// fallbackAllCollisions 无 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)
}
};
});

View File

@ -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;
}