feat: resolve Ortho source using confirmed ModelFiles metadata

This commit is contained in:
codex
2026-08-03 09:08:38 +03:00
parent 75c27ee8e5
commit 243080a1aa
5 changed files with 304 additions and 29 deletions
@@ -3,6 +3,7 @@ using System.Text;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.ProcessPower.DataLinks;
using Autodesk.ProcessPower.Drawings2d;
using Autodesk.ProcessPower.P3dProjectParts;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.ProcessPower.ProjectManager;
@@ -151,50 +152,134 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
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
// the authoritative source-model references; sheet.DwgName is the Ortho
// sheet and must not be used for Piping DataLinks.
// Confirmed Plant 3D 2026 contract (PnPDwg2dDef/PnPDwg2dUtil):
// IDwgViewDef.DwgQuery -> IDwgQuery.ModelFiles -> ModelFile.ModelFileName.
// The Ortho sheet is never used as a Piping DataLinks source.
try
{
var query = view.GetType().GetProperty("DwgQuery")?.GetValue(view);
var files = query?.GetType().GetProperty("ModelFiles")?.GetValue(query) as System.Collections.IEnumerable;
if (files is null) return null;
foreach (var file in files)
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
{
var value = file as string
?? file?.GetType().GetProperty("ModelFileName")?.GetValue(file)?.ToString()
?? file?.GetType().GetProperty("Value")?.GetValue(file)?.ToString();
if (!string.IsNullOrWhiteSpace(value)) return value;
RuntimeDiagnostics.Write("OrthoModelFilesContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
return null;
}
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;
}
}
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
{
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName))
{
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;
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
catch { return Array.Empty<PlantObjectSnapshot>(); }
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
if (!Path.IsPathRooted(viewport.SourceDrawingName) || !File.Exists(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
try { dataLinks = pipingProject.DataLinksManager; }
catch (Exception exception)
{
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
return Array.Empty<PlantObjectSnapshot>();
}
if (dataLinks is null)
{
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport);
return Array.Empty<PlantObjectSnapshot>();
}
var snapshots = new List<PlantObjectSnapshot>();
try
{
using var sourceDatabase = new Database(false, true);
sourceDatabase.ReadDwgFile(viewport.SourceDrawingName, FileShare.ReadWrite, true, null);
sourceDatabase.CloseInput(true);
foreach (var rowId in dataLinks.SelectAcPpRowIds(sourceDatabase))
// Confirmed Plant 3D 2026 contract: resolve the source model to the
// Piping project drawing record, then read its DataLinks by drawing Id.
// The active Ortho database is deliberately never passed to DataLinks.
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
{
@@ -217,20 +302,78 @@ internal sealed class PlantObjectReader : IPlantObjectReader
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
// receives an empty read-only result rather than a command failure.
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
}
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();
}
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)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));