diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 8a194e3..5738fb4 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -1,5 +1,117 @@ namespace PRecAnnot.Abstractions; +public sealed record RuntimeDiagnostic +{ + public required string Code { get; init; } + public required string Message { get; init; } + public string Severity { get; init; } = "Warning"; +} + +public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed } + +public sealed record OrthoDetectionResult +{ + public required OrthoDetectionStatus Status { get; init; } + public required IReadOnlyList Evidence { get; init; } + public required IReadOnlyList Diagnostics { get; init; } +} + +public readonly record struct Point2Dto(double X, double Y); +public readonly record struct Point3Dto(double X, double Y, double Z); +public readonly record struct Vector2Dto(double X, double Y); +public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum); + +public sealed record GeometrySnapshot +{ + public Point3Dto? WorldPosition { get; init; } + public Point2Dto? PaperPosition { get; init; } + public Bounds2Dto? ProjectedBounds { get; init; } + public double? Rotation { get; init; } + public double? Elevation { get; init; } +} + +public sealed record OrthoViewportInfo +{ + public required string Id { get; init; } + public required string DisplayName { get; init; } + public double CenterX { get; init; } + public double CenterY { get; init; } + public double Width { get; init; } + public double Height { get; init; } + public double Scale { get; init; } + public double TwistAngle { get; init; } + public bool IsOn { get; init; } + public bool IsLocked { get; init; } + public string? SourceDrawingName { get; init; } + public string? ViewName { get; init; } +} + +public sealed record PlantObjectSnapshot +{ + public required string SnapshotId { get; init; } + public required string ObjectClass { get; init; } + public string? Tag { get; init; } + public string? LineNumber { get; init; } + public string? SupportNumber { get; init; } + public string? EquipmentNumber { get; init; } + public required GeometrySnapshot Geometry { get; init; } + public IReadOnlyDictionary Properties { get; init; } = new Dictionary(); + public IReadOnlyList Diagnostics { get; init; } = Array.Empty(); +} + +public sealed record PlantRuntimeContext +{ + public bool HasActiveDocument { get; init; } + public bool HasPlantProject { get; init; } + public bool IsOrthoDrawing { get; init; } + public string? DrawingName { get; init; } + public string? ProjectDisplayName { get; init; } + public IReadOnlyList Viewports { get; init; } = Array.Empty(); + public IReadOnlyList Diagnostics { get; init; } = Array.Empty(); + public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty(), Diagnostics = Array.Empty() }; +} + +public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); } +public interface IOrthoViewportReader { IReadOnlyList ReadViewports(CancellationToken cancellationToken); } + +public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported } + +public sealed record AnnotationOperationPlan +{ + public required string SourceSnapshotId { get; init; } + public required AnnotationOperationKind Kind { get; init; } + public string? AnnotationType { get; init; } + public string? Text { get; init; } + public Point2Dto? Position { get; init; } + public double? Rotation { get; init; } + public string? ExistingAnnotationId { get; init; } + public required IReadOnlyList Reasons { get; init; } +} + +public sealed record AnnotationPlanSummary +{ + public int ObjectsExamined { get; init; } + public int CreateCount { get; init; } + public int UpdateCount { get; init; } + public int SkipCount { get; init; } + public int ConflictCount { get; init; } + public int UnsupportedCount { get; init; } + public int WarningCount { get; init; } + public int ErrorCount { get; init; } +} + +public sealed record AnnotationPlanningRequest +{ + public required string CommandName { get; init; } + public required OrthoViewportInfo Viewport { get; init; } + public IReadOnlyList Objects { get; init; } = Array.Empty(); + public IReadOnlyList Existing { get; init; } = Array.Empty(); +} + +public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); } +public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply } +public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); } + public sealed record OrthoViewportRef(string DrawingId, string ViewportId); public sealed record PlantObjectData @@ -36,6 +148,11 @@ public sealed record AnnotationIntent public sealed record AnnotationPlan { + public string PlanId { get; init; } = string.Empty; + public string CommandName { get; init; } = string.Empty; + public IReadOnlyList Operations { get; init; } = Array.Empty(); + public IReadOnlyList Diagnostics { get; init; } = Array.Empty(); + public AnnotationPlanSummary Summary { get; init; } = new(); public required IReadOnlyList Create { get; init; } public required IReadOnlyList Update { get; init; } public required IReadOnlyList Delete { get; init; } @@ -58,7 +175,7 @@ public interface IOrthoDrawingContext public interface IPlantObjectReader { - IReadOnlyList ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken); + IReadOnlyList ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken); } public interface IAnnotationRepository diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationPlanBuilder.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationPlanBuilder.cs new file mode 100644 index 0000000..71be31c --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationPlanBuilder.cs @@ -0,0 +1,109 @@ +using System.Security.Cryptography; +using System.Text; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder +{ + public AnnotationPlan Build(AnnotationPlanningRequest request) + { + var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray(); + var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase); + var operations = new List(objects.Length); + var creates = new List(); + var updates = new List(); + var diagnostics = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var item in objects) + { + if (!seen.Add(item.SnapshotId)) + { + operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Conflict, Reasons = new[] { "duplicate-snapshot-id" } }); + continue; + } + + if (string.IsNullOrWhiteSpace(item.ObjectClass)) + { + operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "missing-object-class" } }); + continue; + } + + var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "missing-line-number-or-tag" } }); + continue; + } + + var position = item.Geometry.PaperPosition; + if (position is null) + { + diagnostics.Add(new RuntimeDiagnostic { Code = "MISSING_GEOMETRY", Message = $"No paper position for {item.SnapshotId}." }); + } + + var stableKey = $"{request.Viewport.SourceDrawingName ?? string.Empty}:{request.Viewport.Id}:{item.SnapshotId}"; + var existingAnnotation = existing.TryGetValue(stableKey, out var match) ? match : existing.TryGetValue(item.SnapshotId, out var directMatch) ? directMatch : null; + var kind = existingAnnotation is null ? AnnotationOperationKind.Create : AnnotationOperationKind.Update; + operations.Add(new AnnotationOperationPlan + { + SourceSnapshotId = item.SnapshotId, + Kind = kind, + AnnotationType = "PlantObject", + Text = text, + Position = position, + Rotation = item.Geometry.Rotation, + ExistingAnnotationId = existingAnnotation?.StableKey, + Reasons = Array.Empty() + }); + + var intent = new AnnotationIntent + { + StableKey = stableKey, + Text = text, + Position = position is { } point ? new AnnotationPoint(point.X, point.Y) : new AnnotationPoint(0, 0), + Rotation = item.Geometry.Rotation ?? 0, + ClassName = item.ObjectClass, + Layer = "PXP_ANNOTATION" + }; + if (kind == AnnotationOperationKind.Create) creates.Add(intent); else updates.Add(intent); + } + + var summary = new AnnotationPlanSummary + { + ObjectsExamined = objects.Length, + CreateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Create), + UpdateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Update), + SkipCount = operations.Count(x => x.Kind == AnnotationOperationKind.Skip), + ConflictCount = operations.Count(x => x.Kind == AnnotationOperationKind.Conflict), + UnsupportedCount = operations.Count(x => x.Kind == AnnotationOperationKind.Unsupported), + WarningCount = diagnostics.Count(x => !string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)), + ErrorCount = diagnostics.Count(x => string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)) + }; + + return new AnnotationPlan + { + PlanId = BuildPlanId(request, objects), + CommandName = request.CommandName, + Operations = operations, + Diagnostics = diagnostics, + Summary = summary, + Create = creates, + Update = updates, + Delete = Array.Empty(), + SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(), + ExaminedCount = objects.Length, + IsDeterministic = true + }; + } + + private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList objects) + { + var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id); + foreach (var item in objects) + canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition); + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString())); + return "plan-" + Convert.ToHexString(hash)[..24]; + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs index 0c935e8..88ea0ac 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs @@ -6,43 +6,21 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine { public AnnotationPlan BuildPlan(AnnotationRequest request) { - var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase); - var create = new List(); - var update = new List(); - var skipped = new List(); - - foreach (var item in request.Objects.OrderBy(x => x.ObjectId, StringComparer.Ordinal)) + var objects = request.Objects.Select(item => new PlantObjectSnapshot { - if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag)) - { - skipped.Add($"{item.ObjectId}:missing-line-number-or-tag"); - continue; - } + SnapshotId = item.ObjectId, + ObjectClass = item.ClassName, + Tag = item.Tag, + LineNumber = item.LineNumber, + Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation } + }).ToArray(); - var text = BuildText(item); - var intent = new AnnotationIntent - { - StableKey = $"{request.Viewport.DrawingId}:{request.Viewport.ViewportId}:{item.ObjectId}", - Text = text, - Position = item.Position, - Rotation = item.Rotation, - ClassName = item.ClassName, - Layer = "PXP_ANNOTATION" - }; - - if (existing.ContainsKey(intent.StableKey)) update.Add(intent); else create.Add(intent); - } - - return new AnnotationPlan + return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { - Create = create, - Update = update, - Delete = Array.Empty(), - SkipReasons = skipped, - ExaminedCount = request.Objects.Count - }; + CommandName = "PHP_REC_ANNOT", + Viewport = new OrthoViewportInfo { Id = request.Viewport.ViewportId, DisplayName = request.Viewport.ViewportId, SourceDrawingName = request.Viewport.DrawingId }, + Objects = objects, + Existing = request.Existing + }); } - - private static string BuildText(PlantObjectData item) => - string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim(); } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ExecutionModePolicy.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ExecutionModePolicy.cs new file mode 100644 index 0000000..98604f6 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ExecutionModePolicy.cs @@ -0,0 +1,8 @@ +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class ExecutionModeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard +{ + public void EnsureWriteAllowed(string operation) => throw new InvalidOperationException(mode == PRecAnnotExecutionMode.Apply ? "APPLY_MODE_NOT_IMPLEMENTED" : "SMOKE_WRITE_BLOCKED"); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoDetectionClassifier.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoDetectionClassifier.cs new file mode 100644 index 0000000..52ab708 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoDetectionClassifier.cs @@ -0,0 +1,23 @@ +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public static class OrthoDetectionClassifier +{ + public static OrthoDetectionResult Classify(bool hasPlantProject, bool hasOrthoPart, bool apiConfirmed, bool metadataAmbiguous = false) + { + if (!hasPlantProject) return Result(OrthoDetectionStatus.NotOrtho, "NO_PLANT_PROJECT", "Active Plant project is absent."); + if (metadataAmbiguous) return Result(OrthoDetectionStatus.Ambiguous, "AMBIGUOUS_ORTHO_METADATA", "Plant metadata is insufficient to confirm Ortho."); + if (!hasOrthoPart) return Result(OrthoDetectionStatus.Unsupported, "ORTHO_PART_UNAVAILABLE", "Ortho project part is unavailable."); + return apiConfirmed + ? Result(OrthoDetectionStatus.Confirmed, "ORTHO_API_CONFIRMED", "OrthoDocUtils confirmed the active drawing.") + : Result(OrthoDetectionStatus.NotOrtho, "ORTHO_API_NEGATIVE", "OrthoDocUtils did not confirm the active drawing."); + } + + private static OrthoDetectionResult Result(OrthoDetectionStatus status, string code, string message) => new() + { + Status = status, + Evidence = new[] { code }, + Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } + }; +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 7e86916..5e21a1e 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -1,9 +1,9 @@ using System.Diagnostics; using Autodesk.AutoCAD.ApplicationServices; -using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; -using Autodesk.ProcessPower.PlantInstance; using Autodesk.Windows; +using PRecAnnot.Abstractions; +using PRecAnnot.Core; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; namespace PRecAnnot.Plant3D2026; @@ -39,20 +39,24 @@ public sealed class EntryPoint : IExtensionApplication var stopwatch = Stopwatch.StartNew(); Document? document = null; RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName); - try { document = AcadApplication.DocumentManager.MdiActiveDocument; - var project = PlantApplication.CurrentProject; + var mode = ParseMode(RuntimeDiagnostics.RuntimeMode); + if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED"); + + var context = new PlantRuntimeContextReader().ReadCurrentContext(); var details = new Dictionary { - ["IsPlantProjectOpen"] = project is not null, - ["IsOrthoDrawing"] = false, - ["ViewportCount"] = null, - ["DataLinksAvailable"] = DataLinksAvailable(project) + ["ExecutionMode"] = mode.ToString(), + ["IsPlantProjectDetected"] = context.HasPlantProject, + ["IsPlantProjectOpen"] = context.HasPlantProject, + ["IsOrthoDrawing"] = context.IsOrthoDrawing, + ["ViewportCount"] = context.Viewports.Count, + ["DataLinksAvailable"] = DataLinksAvailable(context.HasPlantProject) }; - if (document is null || project is null) + if (document is null || !context.HasPlantProject) { RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details); WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён."); @@ -61,47 +65,75 @@ public sealed class EntryPoint : IExtensionApplication return; } - RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details); - if (SmokeGuard.IsSmoke) SmokeGuard.EnsureReadOnly(); - WriteEditor(document, $"\nP_Rec_Annot {commandName}: Plant 3D-проект '{project.Name}' найден; Smoke/diagnostic-only, DWG не изменён."); - WriteUsage(operationId, commandName, "PreflightAccepted", document, details, stopwatch.ElapsedMilliseconds); - RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details); - } - catch (SmokeWriteBlockedException exception) - { - RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "SMOKE_WRITE_BLOCKED", exception, document); - WriteUsage(operationId, commandName, "SMOKE_WRITE_BLOCKED", document, null, stopwatch.ElapsedMilliseconds, exception); - WriteEditor(document, "\nP_Rec_Annot: SMOKE_WRITE_BLOCKED; запись запрещена."); + if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing) + { + var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho"; + RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details); + WriteEditor(document, $"\nP_Rec_Annot {commandName}: проект '{context.ProjectDisplayName}' найден; тип чертежа: {(context.IsOrthoDrawing ? "Ortho" : "NotOrtho")}; DWG не изменён."); + WriteUsage(operationId, commandName, status, document, details, stopwatch.ElapsedMilliseconds); + RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details); + return; + } + + var builder = new AnnotationPlanBuilder(); + var plans = new List(); + foreach (var viewport in context.Viewports) + { + var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None); + plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects })); + } + + var summary = new + { + ObjectsExamined = plans.Sum(x => x.Summary.ObjectsExamined), + CreateCount = plans.Sum(x => x.Summary.CreateCount), + UpdateCount = plans.Sum(x => x.Summary.UpdateCount), + SkipCount = plans.Sum(x => x.Summary.SkipCount), + ConflictCount = plans.Sum(x => x.Summary.ConflictCount), + UnsupportedCount = plans.Sum(x => x.Summary.UnsupportedCount), + WarningCount = plans.Sum(x => x.Summary.WarningCount), + ErrorCount = plans.Sum(x => x.Summary.ErrorCount) + }; + details["ObjectsExamined"] = summary.ObjectsExamined; + details["PlanCreateCount"] = summary.CreateCount; + details["PlanUpdateCount"] = summary.UpdateCount; + details["PlanSkipCount"] = summary.SkipCount; + details["PlanConflictCount"] = summary.ConflictCount; + details["PlanUnsupportedCount"] = summary.UnsupportedCount; + details["ErrorCount"] = summary.ErrorCount; + RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details); + WriteEditor(document, $"\nP_Rec_Annot — диагностический план\nПроект: {context.ProjectDisplayName}\nТип чертежа: Ortho\nViewport: {context.Viewports.Count}\nОбъектов найдено: {summary.ObjectsExamined}\nБудет создано: {summary.CreateCount}\nБудет обновлено: {summary.UpdateCount}\nБудет пропущено: {summary.SkipCount}\nКонфликтов: {summary.ConflictCount}\nОшибок: {summary.ErrorCount}\nDWG не изменён."); + WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds); + RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details); } catch (System.Exception exception) { - RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "Failed", exception, document); - WriteUsage(operationId, commandName, "Failed", document, null, stopwatch.ElapsedMilliseconds, exception); + RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document); + WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception); WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}"); } } - private static bool DataLinksAvailable(Autodesk.ProcessPower.ProjectManager.PlantProject? project) + private static PRecAnnotExecutionMode ParseMode(string value) => + value.Equals("Smoke", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Smoke : + value.Equals("Apply", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Apply : PRecAnnotExecutionMode.DryRun; + + private static bool DataLinksAvailable(bool hasPlantProject) { - try { return PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; } + try { return hasPlantProject && Autodesk.ProcessPower.PlantInstance.PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; } catch { return false; } } - private static void WriteUsage(string operationId, string commandName, string resultStatus, Document? document, IReadOnlyDictionary? details, long duration, System.Exception? exception = null) + private static void WriteUsage(string operationId, string commandName, string status, Document? document, IReadOnlyDictionary? details, long duration, System.Exception? exception = null) { var eventDetails = new Dictionary { - ["ObjectsExamined"] = 0, - ["AnnotationsCreated"] = 0, - ["AnnotationsUpdated"] = 0, - ["AnnotationsDeleted"] = 0, - ["AnnotationsSkipped"] = 0, - ["WarningCount"] = 0, - ["ErrorCount"] = exception is null ? 0 : 1, - ["DurationMilliseconds"] = duration + ["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0, + ["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0, + ["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration }; if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value; - RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, resultStatus, exception, document, eventDetails); + RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, status, exception, document, eventDetails); } private static void WriteEditor(Document? document, string message) @@ -115,7 +147,6 @@ internal static class RibbonIntegration private const string TabId = "PRecAnnot.Plant3D2026"; private const string PanelId = "PRecAnnot.Plant3D2026.Annotations"; private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT"; - private const string Command = "PHP_REC_ANNOT "; private static bool deferred; public static void Register() @@ -125,23 +156,16 @@ internal static class RibbonIntegration { if (ComponentManager.Ribbon is null) { - if (!deferred) - { - ComponentManager.ItemInitialized += OnItemInitialized; - deferred = true; - } + if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; } RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred"); return; } RegisterNow(); } - catch (System.Exception exception) - { - RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); - } + catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); } } - private static void OnItemInitialized(object? sender, System.EventArgs args) + private static void OnItemInitialized(object? sender, EventArgs args) { if (ComponentManager.Ribbon is null) return; ComponentManager.ItemInitialized -= OnItemInitialized; @@ -151,24 +175,13 @@ internal static class RibbonIntegration private static void RegisterNow() { - RibbonControl ribbon = ComponentManager.Ribbon!; - RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase)); - if (tab is null) - { - tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; - ribbon.Tabs.Add(tab); - } - - RibbonPanel? panel = tab.Panels.FirstOrDefault(item => string.Equals(item.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase)); - if (panel is not null) - { - RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); - RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); - return; - } - - RibbonPanelSource source = new() { Id = PanelId, Title = "Аннотации" }; - source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = Command }); + var ribbon = ComponentManager.Ribbon!; + var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase)); + if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); } + var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase)); + if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; } + var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" }; + source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " }); tab.Panels.Add(new RibbonPanel { Source = source }); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created"); } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj index b978064..adaef5e 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj @@ -11,7 +11,10 @@ $(AutoCAD2026ManagedDir)\AcCoreMgd.dllfalsefalse $(AutoCAD2026ManagedDir)\AdWindows.dllfalsefalse $(AutoCAD2026ManagedDir)\PnPDataLinks.dllfalsefalse + $(AutoCAD2026ManagedDir)\PnPDataObjects.dllfalsefalse $(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dllfalsefalse $(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dllfalsefalse + $(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dllfalsefalse + $(Plant3D2026ManagedDir)\PnPDwg2dDef.dllfalsefalse diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs new file mode 100644 index 0000000..ef70177 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs @@ -0,0 +1,206 @@ +using System.Security.Cryptography; +using System.Text; +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.ProcessPower.DataLinks; +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-проект не найден."); + } + + 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 = project.Name, + Viewports = viewports, + Diagnostics = 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, + 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 = sheet.DwgName, + 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]; + } +} + +internal sealed class PlantObjectReader : IPlantObjectReader +{ + public IReadOnlyList ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken) + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null) return Array.Empty(); + + DataLinksManager? dataLinks; + try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; } + catch { return Array.Empty(); } + if (dataLinks is null) return Array.Empty(); + + var snapshots = new List(); + foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database)) + { + 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(Path.GetFileName(document.Name), 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" } + }); + } + catch + { + // Keep the read path best-effort; no write operation is attempted. + } + } + return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray(); + } + + 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 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"); + } +}