diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 3b8da97..1c11eba 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -85,7 +85,7 @@ public sealed record LineConnectionBoundaryCandidate( double Rotation, string? FirstLineGroupStableId = null, string? SecondLineGroupStableId = null); -public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList Reasons, string? FirstText = null, string? SecondText = null); +public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList Reasons, string? FirstText = null, string? SecondText = null, Point3Dto? ConnectorPosition = null); public sealed record LineConnectionAssets(string PolylineLayer, string TextLayer, string TextStyle, string BlockName = "BLOCK_LINENUMBER_CONNECTION"); public sealed record LineConnectionApplyResult(int Deleted, int Created); // The geometry adapter supplies the paper-space connector point and orientation. @@ -154,6 +154,7 @@ public sealed record ComponentGeometryDefinition( Bounds3Dto Bounds, bool ImperialDimensions); public sealed record ComponentGeometryReadResult(bool Success, ComponentGeometryDefinition? Geometry, string? FailureCode = null); +public sealed record ViewportLayerPlan(bool RequiresChange, string TargetLayer, string? FailureCode = null); public interface IPlantComponentGeometryReader { ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs index 112190a..7eb1fe2 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs @@ -15,7 +15,7 @@ public sealed class LineConnectionBoundaryPlanner { if (!seen.Add(candidate.StableId)) { - result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"])); + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"], ConnectorPosition: candidate.FirstEndpoint)); continue; } var first = candidate.FirstLineNumber?.Trim() ?? string.Empty; @@ -27,21 +27,21 @@ public sealed class LineConnectionBoundaryPlanner if (!string.IsNullOrWhiteSpace(candidate.FirstLineGroupStableId) && string.Equals(candidate.FirstLineGroupStableId, candidate.SecondLineGroupStableId, StringComparison.OrdinalIgnoreCase)) { - result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"])); + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint)); continue; } if (first.Length == 0 || second.Length == 0) { - result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"])); + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"], ConnectorPosition: candidate.FirstEndpoint)); continue; } if (string.Equals(first, second, StringComparison.OrdinalIgnoreCase)) { - result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"])); + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint)); continue; } var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray(); - result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty(), first, second)); + result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty(), first, second, candidate.FirstEndpoint)); } return result; } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ViewportLayerPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ViewportLayerPlanner.cs new file mode 100644 index 0000000..92fb3e0 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ViewportLayerPlanner.cs @@ -0,0 +1,13 @@ +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class ViewportLayerPlanner +{ + public ViewportLayerPlan Build(string? currentLayer, bool targetLayerExists, string targetLayer = "Defpoints") + { + if (string.IsNullOrWhiteSpace(targetLayer)) return new(false, string.Empty, "VIEWPORT_TARGET_LAYER_INVALID"); + if (!targetLayerExists) return new(false, targetLayer, "VIEWPORT_TARGET_LAYER_MISSING"); + return new(!string.Equals(currentLayer, targetLayer, StringComparison.OrdinalIgnoreCase), targetLayer); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 07d7d93..f1963ca 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -47,6 +47,7 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run(); [CommandMethod("PXP-AE")] public void AutoElevationCommand() => ElevationCommands.RunAutomatic(); [CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual(); + [CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport(); [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs index b365ee4..4d2432f 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs @@ -49,6 +49,16 @@ internal sealed class LineConnectionAnnotationWriter attribute.LayerId = layers[assets.TextLayer]; attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); } + if (plan.ConnectorPosition is { } connector) + { + using var leader = new Polyline(3) { LayerId = layers[assets.PolylineLayer], XData = Ownership(viewportStableId, plan) }; + leader.AddVertexAt(0, new Point2d(connector.X, connector.Y), 0, 0, 0); + // Legacy PXP-ALC uses an orthogonal two-segment leader. Its + // elbow shares the annotation Y coordinate for a stable route. + leader.AddVertexAt(1, new Point2d(connector.X, plan.Position.Y), 0, 0, 0); + leader.AddVertexAt(2, new Point2d(plan.Position.X, plan.Position.Y), 0, 0, 0); + paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true); + } } transaction.Commit(); return new(deleted, insertable.Length); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoMaintenanceCommands.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoMaintenanceCommands.cs new file mode 100644 index 0000000..179a9a6 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoMaintenanceCommands.cs @@ -0,0 +1,67 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.EditorInput; +using Autodesk.ProcessPower.PlantInstance; +using Autodesk.ProcessPower.P3dProjectParts; +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application; + +namespace PRecAnnot.Plant3D2026; + +internal static class OrthoMaintenanceCommands +{ + public static void CheckViewport() + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null) return; + try + { + var options = new PromptEntityOptions("\nВыберите видовой экран для проверки: "); + options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true); + var selected = document.Editor.GetEntity(options); if (selected.Status != PromptStatus.OK) return; + OrthoViewportInfo view; + ViewportLayerPlan layerPlan; + using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction()) + { + var viewport = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead); + var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead); + layerPlan = new ViewportLayerPlanner().Build(viewport.Layer, layers.Has("Defpoints")); + view = Resolve(document, viewport); + } + if (layerPlan.FailureCode is not null) throw new InvalidOperationException(layerPlan.FailureCode); + var objects = new PlantObjectReader().ReadObjects(view, CancellationToken.None); + var resolved = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved); + var outside = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView); + var unresolved = objects.Count - resolved - outside; + document.Editor.WriteMessage($"\nPXP-COV preview: resolved={resolved}; outside={outside}; unresolved={unresolved}; viewport-layer-change={layerPlan.RequiresChange}. DWG не изменён."); + RuntimeDiagnostics.Write("OrthoViewportCheckCompleted", commandName: "PXP-COV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary { ["Resolved"] = resolved, ["Outside"] = outside, ["Unresolved"] = unresolved, ["LayerChange"] = layerPlan.RequiresChange }); + if (!layerPlan.RequiresChange || !string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return; + ValidateFixture(document); + using var documentLock = document.LockDocument(); + using var write = document.Database.TransactionManager.StartTransaction(); + var target = (Viewport)write.GetObject(selected.ObjectId, OpenMode.ForWrite); + var layerTable = (LayerTable)write.GetObject(document.Database.LayerTableId, OpenMode.ForRead); + target.LayerId = layerTable[layerPlan.TargetLayer]; + write.Commit(); + document.Editor.WriteMessage("\nPXP-COV Apply: viewport moved to Defpoints; AutoCAD UNDO is available."); + } + catch (Exception exception) { RuntimeDiagnostics.Write("OrthoViewportCheckFailed", "ERROR", commandName: "PXP-COV", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-COV: " + exception.Message); } + } + + private static OrthoViewportInfo Resolve(Document document, Viewport selected) + { + var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_PROJECT_UNAVAILABLE"); + var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None) + .Where(x => Math.Abs(x.CenterX - selected.CenterPoint.X) < .001 && Math.Abs(x.CenterY - selected.CenterPoint.Y) < .001 && Math.Abs(x.Width - selected.Width) < .001 && Math.Abs(x.Height - selected.Height) < .001).ToArray(); + return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ORTHO_VIEW_NOT_FOUND" : "ORTHO_VIEW_AMBIGUOUS"); + } + + private static void ValidateFixture(Document document) + { + var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER"); + if (string.IsNullOrWhiteSpace(marker) || !Path.IsPathFullyQualified(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_MAINTENANCE_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED"); + var root = Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar; + if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED"); + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs index bfa7593..ad316f9 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs @@ -13,6 +13,7 @@ public sealed class LineConnectionBoundaryPlannerTests Assert.Equal("a", result[0].StableId); Assert.Contains("SAME_LINE_GROUP", result[0].Reasons); Assert.Equal("L-100 / L-200", result[1].Text); + Assert.Equal(new Point3Dto(), result[1].ConnectorPosition); Assert.Empty(result[1].Reasons); } diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ViewportLayerPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ViewportLayerPlannerTests.cs new file mode 100644 index 0000000..f169c27 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ViewportLayerPlannerTests.cs @@ -0,0 +1,17 @@ +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class ViewportLayerPlannerTests +{ + [Theory] + [InlineData("0", true)] + [InlineData("VIEWPORTS", true)] + public void PlansLegacyDefpointsAssignment(string current, bool exists) => Assert.True(new ViewportLayerPlanner().Build(current, exists).RequiresChange); + + [Fact] + public void PreservesAlreadyAssignedViewport() => Assert.False(new ViewportLayerPlanner().Build("defpoints", true).RequiresChange); + + [Fact] + public void RejectsMissingTargetLayer() => Assert.Equal("VIEWPORT_TARGET_LAYER_MISSING", new ViewportLayerPlanner().Build("0", false).FailureCode); +}