using System.Security.Cryptography; using System.Collections.Specialized; 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; using PRecAnnot.Abstractions; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; namespace PRecAnnot.Plant3D2026; internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader { public PlantRuntimeContext ReadCurrentContext() { var document = AcadApplication.DocumentManager.MdiActiveDocument; if (document is null) { return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует."); } PlantProject? project; try { project = PlantApplication.CurrentProject; } catch (Exception exception) { return Empty(true, false, "PLANT_PROJECT_READ_FAILED", exception.GetType().Name); } if (project is null) { return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден."); } var displayName = string.IsNullOrWhiteSpace(project.Name) ? null : project.Name.Trim(); var projectStatus = displayName is null ? PlantProjectDetectionStatus.FoundWithoutDisplayName : PlantProjectDetectionStatus.FoundWithName; OrthoProject? orthoProject; try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; } catch (Exception exception) { return Empty(true, true, "ORTHO_PROJECT_PART_READ_FAILED", exception.GetType().Name); } if (orthoProject is null) { return Empty(true, true, "ORTHO_PROJECT_PART_UNAVAILABLE", "Ortho project part недоступен.", OrthoDetectionStatus.Unsupported, "ProjectPart(Ortho)"); } try { string resolvedPath; var isOrtho = OrthoDocUtils.IsValidOrthoDrawing(document.Name, orthoProject, out resolvedPath); if (!isOrtho) { return Empty(true, true, "NOT_ORTHO_DRAWING", "OrthoDocUtils.IsValidOrthoDrawing вернул false.", OrthoDetectionStatus.NotOrtho, "Ortho project part", "OrthoDocUtils.IsValidOrthoDrawing=false"); } var viewports = new PlantOrthoViewportReader(orthoProject, document.Name).ReadViewports(CancellationToken.None); return new PlantRuntimeContext { HasActiveDocument = true, HasPlantProject = true, IsOrthoDrawing = true, DrawingName = Path.GetFileName(document.Name), ProjectDisplayName = displayName ?? "display name unavailable", ProjectDetectionStatus = projectStatus, Viewports = viewports, Diagnostics = displayName is null ? new[] { new RuntimeDiagnostic { Code = "PLANT_PROJECT_DISPLAY_NAME_EMPTY", Message = "Plant project is open but no display name is available.", Severity = "Warning" } } : Array.Empty(), Detection = new OrthoDetectionResult { Status = OrthoDetectionStatus.Confirmed, Evidence = new[] { "ProjectPart(Ortho)", "OrthoDocUtils.IsValidOrthoDrawing=true", "DwgSheetDefinitions" }, Diagnostics = Array.Empty() } }; } catch (Exception exception) { return Empty(true, true, "ORTHO_DETECTION_FAILED", exception.GetType().Name, OrthoDetectionStatus.Failed, "ProjectPart(Ortho)"); } } private static PlantRuntimeContext Empty(bool hasDocument, bool hasProject, string code, string message, OrthoDetectionStatus status = OrthoDetectionStatus.NotOrtho, params string[] evidence) => new() { HasActiveDocument = hasDocument, HasPlantProject = hasProject, IsOrthoDrawing = false, ProjectDisplayName = hasProject ? "display name unavailable" : null, ProjectDetectionStatus = hasProject ? PlantProjectDetectionStatus.Ambiguous : PlantProjectDetectionStatus.NotFound, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } }, Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } } }; } internal sealed class PlantOrthoViewportReader : IOrthoViewportReader { private readonly OrthoProject project; private readonly string drawingName; public PlantOrthoViewportReader(OrthoProject project, string drawingName) { this.project = project; this.drawingName = Path.GetFileName(drawingName); } public IReadOnlyList ReadViewports(CancellationToken cancellationToken) { var result = new List(); foreach (var sheet in project.DwgSheetDefinitions.AsArray()) { cancellationToken.ThrowIfCancellationRequested(); if (!string.Equals(Path.GetFileName(sheet.DwgName), drawingName, StringComparison.OrdinalIgnoreCase)) continue; foreach (var view in sheet.DwgViewDefs.AsArray()) { try { cancellationToken.ThrowIfCancellationRequested(); var id = StableId(sheet.DwgGuid.ToString(), view.ViewName); result.Add(new OrthoViewportInfo { Id = id, DisplayName = view.ViewName, CenterX = view.CenterPoint.X, CenterY = view.CenterPoint.Y, Width = view.ViewWidth, Height = view.ViewHeight, Scale = view.ViewScale, TwistAngle = 0, IsOn = true, IsLocked = false, SourceDrawingName = ReadSourceDrawingPath(view), ViewName = view.ViewName }); } catch { // One malformed view must not abort the remaining read-only analysis. } } } return result.OrderByDescending(x => x.CenterY).ThenBy(x => x.CenterX).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray(); } private static string StableId(string sheetId, string viewName) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName)); return "vp-" + Convert.ToHexString(hash)[..20]; } private static string? ReadSourceDrawingPath(IDwgViewDef view) { // 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 { if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic)) { 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); } 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 { ["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 Details); internal sealed class PlantObjectReader : IPlantObjectReader { public IReadOnlyList ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) { WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", viewport); return Array.Empty(); } PipingProject? pipingProject; try { pipingProject = PlantApplication.ProjectPart("Piping") as PipingProject; } catch (Exception exception) { WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport, exception); return Array.Empty(); } if (pipingProject is null) { WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport); return Array.Empty(); } var sourceDrawing = ResolveProjectDrawing(pipingProject, viewport.SourceDrawingName); if (sourceDrawing is null) return Array.Empty(); DataLinksManager? dataLinks; try { dataLinks = pipingProject.DataLinksManager; } catch (Exception exception) { WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception); return Array.Empty(); } if (dataLinks is null) { WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport); return Array.Empty(); } var snapshots = new List(); try { // 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 { ["ProjectDrawingResolved"] = true }); return Array.Empty(); } var membership = PlantViewportMembershipReader.Read(viewport, dataLinks, rowIds); foreach (var rowId in rowIds) { try { cancellationToken.ThrowIfCancellationRequested(); var className = dataLinks.GetObjectClassname(rowId); var properties = dataLinks.GetAllProperties(rowId, true) .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase); 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(viewport.SourceDrawingName, rowId, className), ObjectClass = className ?? string.Empty, Tag = Value("Tag", "TAG"), LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"), SupportNumber = Value("Support Number", "SupportNumber"), EquipmentNumber = Value("Equipment Number", "EquipmentNumber"), Geometry = new GeometrySnapshot(), Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase), Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }, MembershipStatus = membership.StatusFor(rowId), MembershipEvidenceCodes = membership.EvidenceFor(rowId) }); } catch (Exception exception) { RuntimeDiagnostics.Write("SourceDataLinksRowReadFailed", "WARNING", resultStatus: "SOURCE_DATABASE_READ_FAILED", exception: exception); } } } catch (DLException exception) { 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 { ["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 { ["ProjectDrawingResolved"] = true }); return matches[0]; } WriteOutcome(matches.Length == 0 ? "ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT" : "ORTHO_SOURCE_DRAWING_AMBIGUOUS", null, details: new Dictionary { ["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? details = null) { var sanitizedDetails = new Dictionary { ["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}")); return "obj-" + Convert.ToHexString(hash)[..24]; } } internal sealed class PlantViewportMembershipReader { internal readonly record struct MembershipReadResult( IReadOnlySet SelectedRows, ViewMembershipStatus DefaultStatus, string DefaultCode) { public ViewMembershipStatus StatusFor(int rowId) => DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId) ? ViewMembershipStatus.ObjectOutsideView : DefaultStatus; public IReadOnlyList EvidenceFor(int rowId) => DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId) ? new[] { "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP" } : new[] { DefaultCode }; } internal static MembershipReadResult Read(OrthoViewportInfo viewport, DataLinksManager dataLinks, Autodesk.ProcessPower.DataObjects.PnPRowIdArray sourceRows) { try { var project = PlantApplication.ProjectPart("Ortho") as OrthoProject; if (project is null) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED"); var candidates = project.DwgSheetDefinitions.AsArray() .SelectMany(sheet => sheet.DwgViewDefs.AsArray() .Where(view => string.Equals(StableId(sheet.DwgGuid.ToString(), view.ViewName), viewport.Id, StringComparison.Ordinal)) .Select(view => (sheet, view))) .ToArray(); if (candidates.Length != 1) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED"); var view = candidates[0].view; if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic)) { RuntimeDiagnostics.Write("ViewportMembershipContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details); return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED"); } var selectedRows = new HashSet(); var selected = view.DwgQuery.Execute(new StringCollection(), new StringCollection()); for (var index = 0; index < selected.Count; index++) { Autodesk.ProcessPower.Drawings2d.MdObjectId sourceId = default; if (!selected.GetItem(index, ref sourceId) || sourceId.IsNull || sourceId.PrjObjectId.IsNull) continue; try { var rowId = dataLinks.FindAcPpRowId(sourceId.PrjObjectId); if (sourceRows.Contains(rowId)) selectedRows.Add(rowId); } catch (DLException) { // A generated entity without a Piping DataLinks row is outside this reader's source set. } } RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "INFO", resultStatus: "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", details: new Dictionary { ["SelectedSourceRows"] = selectedRows.Count, ["SourceRows"] = sourceRows.Count }); return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID"); } catch (Exception exception) { RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "WARNING", resultStatus: "VIEW_MEMBERSHIP_API_UNSUPPORTED", exception: exception); return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED"); } } private static MembershipReadResult Unsupported(string code) => new(new HashSet(), ViewMembershipStatus.NotResolved, code); // Ortho viewport IDs are generated by PlantOrthoViewportReader from exactly this pair. // Only an exact regenerated ID is accepted; the reader never chooses an arbitrary view. private static string StableId(string sheetId, string? viewName) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + (viewName ?? string.Empty))); return "vp-" + Convert.ToHexString(hash)[..20]; } } internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard { public void EnsureWriteAllowed(string operation) { if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED"); throw new InvalidOperationException("SMOKE_WRITE_BLOCKED"); } }