From 95dba856c894b64f2c5108ac372d7a6dfc40e07c Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 3 Aug 2026 08:16:11 +0300 Subject: [PATCH] feat: resolve Ortho views to source model drawings --- .../src/PRecAnnot.Abstractions/Contracts.cs | 56 +++++++++++++++++++ .../OrthoSourceDrawingResolver.cs | 47 ++++++++++++++++ .../PRecAnnot.Plant3D2026/PlantReadPath.cs | 34 +++++++++-- .../PRecAnnot.Core.Tests/FoundationTests.cs | 34 +++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoSourceDrawingResolver.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index af5a996..39813a6 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -76,6 +76,62 @@ public sealed record PlantRuntimeContext public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); } public interface IOrthoViewportReader { IReadOnlyList 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 EvidenceCodes { get; init; } = Array.Empty(); +} + +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 Properties { get; init; } = new Dictionary(); +} + +public sealed record SourceDrawingReadResult +{ + public required OrthoSourceDrawingReference Source { get; init; } + public IReadOnlyList Objects { get; init; } = Array.Empty(); + public IReadOnlyList Diagnostics { get; init; } = Array.Empty(); +} + +public interface IOrthoSourceDrawingResolver +{ + IReadOnlyList Resolve(OrthoDrawingSnapshot drawing); +} + +public sealed record OrthoDrawingSnapshot +{ + public required string StableId { get; init; } + public IReadOnlyList Views { get; init; } = Array.Empty(); +} + +public sealed record OrthoViewSourceSnapshot +{ + public required string ViewStableId { get; init; } + public IReadOnlyList ReferencedDrawingPaths { get; init; } = Array.Empty(); +} + +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 sealed record AnnotationOperationPlan diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoSourceDrawingResolver.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoSourceDrawingResolver.cs new file mode 100644 index 0000000..2e37fb1 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoSourceDrawingResolver.cs @@ -0,0 +1,47 @@ +using System.Security.Cryptography; +using System.Text; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class OrthoSourceDrawingResolver : IOrthoSourceDrawingResolver +{ + public IReadOnlyList 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]; + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs index 2204de6..420ba39 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; using System.Text; using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; using Autodesk.ProcessPower.DataLinks; using Autodesk.ProcessPower.P3dProjectParts; using Autodesk.ProcessPower.PlantInstance; @@ -130,7 +131,7 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader TwistAngle = 0, IsOn = true, IsLocked = false, - SourceDrawingName = sheet.DwgName, + SourceDrawingName = ReadSourceDrawingPath(view), ViewName = view.ViewName }); } @@ -149,6 +150,26 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName)); 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 @@ -156,17 +177,22 @@ internal sealed class PlantObjectReader : IPlantObjectReader public IReadOnlyList ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken) { var document = AcadApplication.DocumentManager.MdiActiveDocument; - if (document is null) return Array.Empty(); + if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty(); DataLinksManager? dataLinks; try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; } catch { return Array.Empty(); } if (dataLinks is null) return Array.Empty(); + if (!Path.IsPathRooted(viewport.SourceDrawingName) || !File.Exists(viewport.SourceDrawingName)) return Array.Empty(); + var snapshots = new List(); 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 { @@ -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; snapshots.Add(new PlantObjectSnapshot { - SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className), + SnapshotId = StableId(viewport.SourceDrawingName, rowId, className), ObjectClass = className ?? string.Empty, Tag = Value("Tag", "TAG"), LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"), diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs index 46b0d8a..449bf99 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs @@ -130,6 +130,40 @@ public sealed class FoundationTests 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(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test")); [Fact] public void DryRunPath_HasNoForWriteOperations()