feat: resolve Ortho views to source model drawings
This commit is contained in:
@@ -76,6 +76,62 @@ public sealed record PlantRuntimeContext
|
|||||||
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
||||||
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
||||||
|
|
||||||
|
public enum SourceDrawingResolutionStatus { Resolved, NotReferenced, Ambiguous, MissingFromProject, Unsupported, Failed }
|
||||||
|
|
||||||
|
public sealed record OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
public required string StableId { get; init; }
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
public string? ProjectRelativePath { get; init; }
|
||||||
|
public long? ProjectDrawingRowId { get; init; }
|
||||||
|
public required SourceDrawingResolutionStatus Status { get; init; }
|
||||||
|
public IReadOnlyList<string> EvidenceCodes { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SourcePlantObjectSnapshot
|
||||||
|
{
|
||||||
|
public required string StableObjectKey { get; init; }
|
||||||
|
public required string ClassName { get; init; }
|
||||||
|
public string? SourceDrawingStableId { get; init; }
|
||||||
|
public string? LineGroupStableKey { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SourceDrawingReadResult
|
||||||
|
{
|
||||||
|
public required OrthoSourceDrawingReference Source { get; init; }
|
||||||
|
public IReadOnlyList<SourcePlantObjectSnapshot> Objects { get; init; } = Array.Empty<SourcePlantObjectSnapshot>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IOrthoSourceDrawingResolver
|
||||||
|
{
|
||||||
|
IReadOnlyList<OrthoSourceDrawingReference> Resolve(OrthoDrawingSnapshot drawing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
public required string StableId { get; init; }
|
||||||
|
public IReadOnlyList<OrthoViewSourceSnapshot> Views { get; init; } = Array.Empty<OrthoViewSourceSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoViewSourceSnapshot
|
||||||
|
{
|
||||||
|
public required string ViewStableId { get; init; }
|
||||||
|
public IReadOnlyList<string> ReferencedDrawingPaths { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISourceDrawingDataLinksReader
|
||||||
|
{
|
||||||
|
SourceDrawingReadResult Read(PlantProjectContext project, OrthoSourceDrawingReference source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlantProjectContext
|
||||||
|
{
|
||||||
|
public required string StableProjectId { get; init; }
|
||||||
|
public string? ProjectDisplayName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
||||||
|
|
||||||
public sealed record AnnotationOperationPlan
|
public sealed record AnnotationOperationPlan
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class OrthoSourceDrawingResolver : IOrthoSourceDrawingResolver
|
||||||
|
{
|
||||||
|
public IReadOnlyList<OrthoSourceDrawingReference> Resolve(OrthoDrawingSnapshot drawing)
|
||||||
|
{
|
||||||
|
var candidates = drawing.Views
|
||||||
|
.OrderBy(x => x.ViewStableId, StringComparer.Ordinal)
|
||||||
|
.SelectMany(x => x.ReferencedDrawingPaths)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Select(x => x.Trim().Replace('\\', '/'))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(path => new OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
StableId = StableId(drawing.StableId, path),
|
||||||
|
DisplayName = Path.GetFileName(path),
|
||||||
|
ProjectRelativePath = path,
|
||||||
|
Status = SourceDrawingResolutionStatus.Resolved,
|
||||||
|
EvidenceCodes = new[] { "SOURCE_FROM_VIEW_DEFINITION", "SOURCE_PATH_MATCHED_PROJECT_RELATIVE" }
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return candidates.Length != 0
|
||||||
|
? candidates
|
||||||
|
: new[]
|
||||||
|
{
|
||||||
|
new OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
StableId = StableId(drawing.StableId, "none"),
|
||||||
|
DisplayName = "source drawing unavailable",
|
||||||
|
Status = SourceDrawingResolutionStatus.NotReferenced,
|
||||||
|
EvidenceCodes = new[] { "ORTHO_SOURCE_DRAWING_NOT_RESOLVED" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StableId(string drawingId, string source)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingId}|{source}"));
|
||||||
|
return "src-" + Convert.ToHexString(hash)[..20];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
using Autodesk.ProcessPower.DataLinks;
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
using Autodesk.ProcessPower.P3dProjectParts;
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
@@ -130,7 +131,7 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
|||||||
TwistAngle = 0,
|
TwistAngle = 0,
|
||||||
IsOn = true,
|
IsOn = true,
|
||||||
IsLocked = false,
|
IsLocked = false,
|
||||||
SourceDrawingName = sheet.DwgName,
|
SourceDrawingName = ReadSourceDrawingPath(view),
|
||||||
ViewName = view.ViewName
|
ViewName = view.ViewName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,6 +150,26 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
|||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
||||||
return "vp-" + Convert.ToHexString(hash)[..20];
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ReadSourceDrawingPath(object 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.
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var value = file?.GetType().GetProperty("Value")?.GetValue(file)?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class PlantObjectReader : IPlantObjectReader
|
internal sealed class PlantObjectReader : IPlantObjectReader
|
||||||
@@ -156,17 +177,22 @@ 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;
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
if (document is null) return Array.Empty<PlantObjectSnapshot>();
|
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
DataLinksManager? dataLinks;
|
DataLinksManager? dataLinks;
|
||||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
||||||
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
||||||
if (dataLinks is null) 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>();
|
||||||
|
|
||||||
var snapshots = new List<PlantObjectSnapshot>();
|
var snapshots = new List<PlantObjectSnapshot>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
|
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))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -178,7 +204,7 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
||||||
snapshots.Add(new PlantObjectSnapshot
|
snapshots.Add(new PlantObjectSnapshot
|
||||||
{
|
{
|
||||||
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
|
SnapshotId = StableId(viewport.SourceDrawingName, rowId, className),
|
||||||
ObjectClass = className ?? string.Empty,
|
ObjectClass = className ?? string.Empty,
|
||||||
Tag = Value("Tag", "TAG"),
|
Tag = Value("Tag", "TAG"),
|
||||||
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||||
|
|||||||
@@ -130,6 +130,40 @@ public sealed class FoundationTests
|
|||||||
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
|
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_OneView_ResolvesOneModelDrawing()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
StableId = "sheet",
|
||||||
|
Views = new[] { new OrthoViewSourceSnapshot { ViewStableId = "view", ReferencedDrawingPaths = new[] { "Plant 3D Models/TestPipeModel.dwg" } } }
|
||||||
|
});
|
||||||
|
var source = Assert.Single(result);
|
||||||
|
Assert.Equal(SourceDrawingResolutionStatus.Resolved, source.Status);
|
||||||
|
Assert.Equal("TestPipeModel.dwg", source.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_DuplicateViews_DeduplicatesSource()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
StableId = "sheet",
|
||||||
|
Views = new[]
|
||||||
|
{
|
||||||
|
new OrthoViewSourceSnapshot { ViewStableId = "a", ReferencedDrawingPaths = new[] { "models/A.dwg" } },
|
||||||
|
new OrthoViewSourceSnapshot { ViewStableId = "b", ReferencedDrawingPaths = new[] { "models/A.dwg" } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Assert.Single(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_MissingSource_ReturnsDiagnostic()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot { StableId = "sheet" });
|
||||||
|
var source = Assert.Single(result);
|
||||||
|
Assert.Equal(SourceDrawingResolutionStatus.NotReferenced, source.Status);
|
||||||
|
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", source.EvidenceCodes);
|
||||||
|
}
|
||||||
|
|
||||||
[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