feat: resolve Ortho source using confirmed ModelFiles metadata
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Autodesk.ProcessPower.Drawings2d;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// This is a diagnostic-only probe. It is never part of source resolution and is
|
||||||
|
// called only by the explicit DryRun/OrthoStructure environment flag combination.
|
||||||
|
internal static class OrthoRuntimeStructureInspector
|
||||||
|
{
|
||||||
|
public static void Inspect(IDwgViewDef view)
|
||||||
|
{
|
||||||
|
if (!RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = view.DwgQuery;
|
||||||
|
var modelFiles = query?.ModelFiles;
|
||||||
|
var details = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ViewRuntimeType"] = TypeName(view.GetType()),
|
||||||
|
["DwgQueryRuntimeType"] = query is null ? null : TypeName(query.GetType()),
|
||||||
|
["ModelFilesDeclaredType"] = typeof(ModelFile[]).FullName,
|
||||||
|
["ModelFilesRuntimeType"] = modelFiles?.GetType().AssemblyQualifiedName,
|
||||||
|
["ModelFilesCount"] = modelFiles?.Length ?? 0,
|
||||||
|
["ModelFileElements"] = modelFiles?.Select(DescribeModelFile).ToArray() ?? Array.Empty<IReadOnlyDictionary<string, object?>>()
|
||||||
|
};
|
||||||
|
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspected", resultStatus: "Captured", details: details);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspectionFailed", "WARNING", resultStatus: "Failed", exception: exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, object?> DescribeModelFile(ModelFile? modelFile)
|
||||||
|
{
|
||||||
|
if (modelFile is null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?> { ["IsNull"] = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = modelFile.GetType()
|
||||||
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(property => property.CanRead && property.GetIndexParameters().Length == 0)
|
||||||
|
.OrderBy(property => property.Name, StringComparer.Ordinal)
|
||||||
|
.Select(property => DescribeProperty(modelFile, property))
|
||||||
|
.ToArray();
|
||||||
|
return new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["RuntimeType"] = TypeName(modelFile.GetType()),
|
||||||
|
["Properties"] = properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, object?> DescribeProperty(ModelFile modelFile, PropertyInfo property)
|
||||||
|
{
|
||||||
|
object? value;
|
||||||
|
try { value = property.GetValue(modelFile); }
|
||||||
|
catch { value = null; }
|
||||||
|
return new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["PropertyName"] = property.Name,
|
||||||
|
["PropertyType"] = property.PropertyType.FullName,
|
||||||
|
["ValueKind"] = value?.GetType().FullName ?? "null",
|
||||||
|
["IsNull"] = value is null,
|
||||||
|
["StringLength"] = value is string text ? text.Length : null,
|
||||||
|
["PathKind"] = property.Name == "ModelFileName" && value is string path
|
||||||
|
? (Path.IsPathRooted(path) ? "Absolute" : "ProjectRelative")
|
||||||
|
: null,
|
||||||
|
["CollectionCount"] = value is ICollection collection ? collection.Count : null,
|
||||||
|
["StableHash"] = value is null ? null : Hash(value.ToString() ?? string.Empty)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TypeName(Type type) => $"{type.FullName}, {type.Assembly.GetName().Name}, {type.Assembly.GetName().Version}";
|
||||||
|
|
||||||
|
private static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..16];
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
<Reference Include="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<Reference Include="PnP3dProjectPartsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
using Autodesk.AutoCAD.DatabaseServices;
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
using Autodesk.ProcessPower.DataLinks;
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using Autodesk.ProcessPower.Drawings2d;
|
||||||
using Autodesk.ProcessPower.P3dProjectParts;
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
using Autodesk.ProcessPower.ProjectManager;
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
@@ -151,50 +152,134 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
|||||||
return "vp-" + Convert.ToHexString(hash)[..20];
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ReadSourceDrawingPath(object view)
|
private static string? ReadSourceDrawingPath(IDwgViewDef view)
|
||||||
{
|
{
|
||||||
// The Plant 3D 2026 view definition owns a DwgQuery. Its ModelFiles are
|
// Confirmed Plant 3D 2026 contract (PnPDwg2dDef/PnPDwg2dUtil):
|
||||||
// the authoritative source-model references; sheet.DwgName is the Ortho
|
// IDwgViewDef.DwgQuery -> IDwgQuery.ModelFiles -> ModelFile.ModelFileName.
|
||||||
// sheet and must not be used for Piping DataLinks.
|
// The Ortho sheet is never used as a Piping DataLinks source.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var query = view.GetType().GetProperty("DwgQuery")?.GetValue(view);
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
||||||
var files = query?.GetType().GetProperty("ModelFiles")?.GetValue(query) as System.Collections.IEnumerable;
|
|
||||||
if (files is null) return null;
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
{
|
||||||
var value = file as string
|
RuntimeDiagnostics.Write("OrthoModelFilesContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
||||||
?? file?.GetType().GetProperty("ModelFileName")?.GetValue(file)?.ToString()
|
return null;
|
||||||
?? file?.GetType().GetProperty("Value")?.GetValue(file)?.ToString();
|
|
||||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled)
|
||||||
|
{
|
||||||
|
OrthoRuntimeStructureInspector.Inspect(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.DwgQuery?.ModelFiles
|
||||||
|
.Select(file => file.ModelFileName)
|
||||||
|
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoModelFilesReadFailed", "WARNING", resultStatus: "ORTHO_MODEL_FILES_API_UNSUPPORTED", exception: exception);
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class OrthoModelFilesContract
|
||||||
|
{
|
||||||
|
private const string ExpectedViewType = "Autodesk.ProcessPower.Drawings2d.DwgViewDef";
|
||||||
|
private const string ExpectedQueryType = "Autodesk.ProcessPower.Drawings2d.DwgQueryByBox";
|
||||||
|
private const string ExpectedModelFileType = "Autodesk.ProcessPower.Drawings2d.ModelFile";
|
||||||
|
private const string ExpectedDefAssembly = "PnPDwg2dDef";
|
||||||
|
private const string ExpectedUtilAssembly = "PnPDwg2dUtil";
|
||||||
|
private const string ExpectedVersion = "0.0.0.0";
|
||||||
|
|
||||||
|
public static bool IsSupported(IDwgViewDef view, out OrthoModelFilesContractDiagnostic diagnostic)
|
||||||
|
{
|
||||||
|
var query = view.DwgQuery;
|
||||||
|
var files = query?.ModelFiles;
|
||||||
|
var valid = HasExpectedType(view, ExpectedViewType, ExpectedDefAssembly) &&
|
||||||
|
query is not null && HasExpectedType(query, ExpectedQueryType, ExpectedDefAssembly) &&
|
||||||
|
files is not null && files.GetType() == typeof(ModelFile[]) &&
|
||||||
|
files.All(file => file is not null && HasExpectedType(file, ExpectedModelFileType, ExpectedUtilAssembly));
|
||||||
|
diagnostic = new OrthoModelFilesContractDiagnostic(
|
||||||
|
valid ? "ORTHO_MODEL_FILES_API_SUPPORTED" : "ORTHO_MODEL_FILES_API_UNSUPPORTED",
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ExpectedViewType"] = ExpectedViewType,
|
||||||
|
["ActualViewType"] = TypeName(view),
|
||||||
|
["ExpectedQueryType"] = ExpectedQueryType,
|
||||||
|
["ActualQueryType"] = query is null ? null : TypeName(query),
|
||||||
|
["ExpectedModelFileType"] = ExpectedModelFileType,
|
||||||
|
["ActualModelFilesType"] = files?.GetType().AssemblyQualifiedName,
|
||||||
|
["ExpectedAssemblyVersion"] = ExpectedVersion,
|
||||||
|
["ActualViewAssemblyVersion"] = view.GetType().Assembly.GetName().Version?.ToString(),
|
||||||
|
["ActualQueryAssemblyVersion"] = query?.GetType().Assembly.GetName().Version?.ToString(),
|
||||||
|
["ActualModelFilesAssemblyVersion"] = files?.GetType().Assembly.GetName().Version?.ToString()
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasExpectedType(object value, string expectedType, string expectedAssembly) =>
|
||||||
|
string.Equals(value.GetType().FullName, expectedType, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(value.GetType().Assembly.GetName().Name, expectedAssembly, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(value.GetType().Assembly.GetName().Version?.ToString(), ExpectedVersion, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string TypeName(object value) => $"{value.GetType().FullName}, {value.GetType().Assembly.GetName().Name}, {value.GetType().Assembly.GetName().Version}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record OrthoModelFilesContractDiagnostic(string Code, IReadOnlyDictionary<string, object?> Details);
|
||||||
|
|
||||||
internal sealed class PlantObjectReader : IPlantObjectReader
|
internal sealed class PlantObjectReader : IPlantObjectReader
|
||||||
{
|
{
|
||||||
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName))
|
||||||
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
PipingProject? pipingProject;
|
||||||
|
try { pipingProject = PlantApplication.ProjectPart("Piping") as PipingProject; }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport, exception);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
if (pipingProject is null)
|
||||||
|
{
|
||||||
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceDrawing = ResolveProjectDrawing(pipingProject, viewport.SourceDrawingName);
|
||||||
|
if (sourceDrawing is null) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
DataLinksManager? dataLinks;
|
DataLinksManager? dataLinks;
|
||||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
try { dataLinks = pipingProject.DataLinksManager; }
|
||||||
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
catch (Exception exception)
|
||||||
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||||
if (!Path.IsPathRooted(viewport.SourceDrawingName) || !File.Exists(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
if (dataLinks is null)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
var snapshots = new List<PlantObjectSnapshot>();
|
var snapshots = new List<PlantObjectSnapshot>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var sourceDatabase = new Database(false, true);
|
// Confirmed Plant 3D 2026 contract: resolve the source model to the
|
||||||
sourceDatabase.ReadDwgFile(viewport.SourceDrawingName, FileShare.ReadWrite, true, null);
|
// Piping project drawing record, then read its DataLinks by drawing Id.
|
||||||
sourceDatabase.CloseInput(true);
|
// The active Ortho database is deliberately never passed to DataLinks.
|
||||||
foreach (var rowId in dataLinks.SelectAcPpRowIds(sourceDatabase))
|
var rowIds = dataLinks.SelectAcPpRowIds(sourceDrawing.Id);
|
||||||
|
if (rowIds.Count == 0)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_NO_ROWS", viewport, details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var rowId in rowIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -217,20 +302,78 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
// Keep the read path best-effort; no write operation is attempted.
|
RuntimeDiagnostics.Write("SourceDataLinksRowReadFailed", "WARNING", resultStatus: "SOURCE_DATABASE_READ_FAILED", exception: exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (DLException)
|
catch (DLException exception)
|
||||||
{
|
{
|
||||||
// An Ortho sheet can have no direct Piping DataLinks rows. The caller
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||||
// receives an empty read-only result rather than a command failure.
|
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATABASE_READ_FAILED", viewport, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteOutcome(snapshots.Count == 0 ? "SOURCE_PIPE_ROWS_NOT_FOUND" : "SOURCE_DATALINKS_READ", viewport,
|
||||||
|
details: new Dictionary<string, object?> { ["SourcePlantObjectsRead"] = snapshots.Count, ["PipeObjectsRead"] = snapshots.Count });
|
||||||
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PnPProjectDrawing? ResolveProjectDrawing(PipingProject pipingProject, string sourceDrawingPath)
|
||||||
|
{
|
||||||
|
if (!Path.IsPathRooted(sourceDrawingPath))
|
||||||
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedSource;
|
||||||
|
try { normalizedSource = Path.GetFullPath(sourceDrawingPath); }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", null, exception);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = pipingProject.GetPnPDrawingFiles()
|
||||||
|
.Where(drawing => MatchesSourcePath(drawing, normalizedSource))
|
||||||
|
.OrderBy(drawing => drawing.Id)
|
||||||
|
.ToArray();
|
||||||
|
if (matches.Length == 1)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("SourceProjectDrawingResolved", resultStatus: "SOURCE_FROM_PROJECT_DRAWING_ROW", details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteOutcome(matches.Length == 0 ? "ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT" : "ORTHO_SOURCE_DRAWING_AMBIGUOUS", null,
|
||||||
|
details: new Dictionary<string, object?> { ["CandidateCount"] = matches.Length });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSourcePath(PnPProjectDrawing drawing, string normalizedSource) =>
|
||||||
|
EquivalentPath(drawing.AbsoluteFileName, normalizedSource) ||
|
||||||
|
EquivalentPath(drawing.ResolvedFilePath, normalizedSource);
|
||||||
|
|
||||||
|
private static bool EquivalentPath(string? candidate, string normalizedSource)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate)) return false;
|
||||||
|
try { return string.Equals(Path.GetFullPath(candidate), normalizedSource, StringComparison.OrdinalIgnoreCase); }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteOutcome(string code, OrthoViewportInfo? viewport, Exception? exception = null, IReadOnlyDictionary<string, object?>? details = null)
|
||||||
|
{
|
||||||
|
var sanitizedDetails = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["SourceResolved"] = viewport?.SourceDrawingName is not null
|
||||||
|
};
|
||||||
|
if (details is not null) foreach (var detail in details) sanitizedDetails[detail.Key] = detail.Value;
|
||||||
|
RuntimeDiagnostics.Write("SourceDataLinksReadCompleted", code == "SOURCE_DATALINKS_READ" ? "INFO" : "WARNING", resultStatus: code, exception: exception, details: sanitizedDetails);
|
||||||
|
}
|
||||||
|
|
||||||
private static string StableId(string drawingName, int rowId, string? className)
|
private static string StableId(string drawingName, int rowId, string? className)
|
||||||
{
|
{
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ internal static class RuntimeDiagnostics
|
|||||||
|
|
||||||
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
||||||
|
|
||||||
|
public static bool IsOrthoStructureDiagnosticsEnabled =>
|
||||||
|
string.Equals(RuntimeMode, "DryRun", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_DIAGNOSTICS"), "OrthoStructure", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public static void Write(
|
public static void Write(
|
||||||
string eventType,
|
string eventType,
|
||||||
string level = "INFO",
|
string level = "INFO",
|
||||||
|
|||||||
@@ -164,6 +164,51 @@ public sealed class FoundationTests
|
|||||||
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", source.EvidenceCodes);
|
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", source.EvidenceCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void DwgQueryContract_UsesConfirmedModelFileContract()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("view.DwgQuery?.ModelFiles", source);
|
||||||
|
Assert.Contains("file.ModelFileName", source);
|
||||||
|
Assert.Contains("ORTHO_MODEL_FILES_API_UNSUPPORTED", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void DwgQueryContract_DoesNotGuessAlternativePropertyNames()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("GetProperty(\"ModelFileName\")", source);
|
||||||
|
Assert.DoesNotContain("GetProperty(\"Value\")", source);
|
||||||
|
Assert.DoesNotContain("GetProperty(\"FileName\")", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_UsesResolvedPipingProjectDrawingId()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("pipingProject.GetPnPDrawingFiles()", source);
|
||||||
|
Assert.Contains("dataLinks.SelectAcPpRowIds(sourceDrawing.Id)", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_DoesNotQueryTheActiveOrthoDatabase()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("SelectAcPpRowIds(sourceDatabase)", source);
|
||||||
|
Assert.DoesNotContain("ReadDwgFile(viewport.SourceDrawingName", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_ReportsDistinctSourceDiagnostics()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", source);
|
||||||
|
Assert.Contains("SOURCE_DATALINKS_UNAVAILABLE", source);
|
||||||
|
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
||||||
|
Assert.Contains("P_REC_ANNOT_DIAGNOSTICS", source);
|
||||||
|
Assert.Contains("OrthoStructure", source);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
||||||
|
|
||||||
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||||
|
|||||||
Reference in New Issue
Block a user