From b381f83bbdcd2d901af2926845ae02490b6f83b4 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 13:21:32 +0300 Subject: [PATCH] feat: port p-rec-annot ortho status and attribute maintenance --- .../PRecAnnot.Core/AttributeSwapPlanner.cs | 15 ++++ .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 3 + .../PRecAnnot.Plant3D2026.csproj | 1 + .../SwapAttributesCommand.cs | 86 +++++++++++++++++++ .../UpdateOrthoViewBlocksCommand.cs | 50 ++++++++++- .../AttributeSwapPlannerTests.cs | 14 +++ 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AttributeSwapPlanner.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/SwapAttributesCommand.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AttributeSwapPlannerTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AttributeSwapPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AttributeSwapPlanner.cs new file mode 100644 index 0000000..a1eda0e --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AttributeSwapPlanner.cs @@ -0,0 +1,15 @@ +namespace PRecAnnot.Core; + +public static class AttributeSwapPlanner +{ + public static double ComputeShelfLength(double firstWidth, double firstOffset, double secondWidth, double secondOffset) => + Math.Abs(Math.Max(firstWidth + firstOffset, secondWidth + secondOffset) + .9); + + public static IReadOnlyDictionary Swap(IReadOnlyDictionary metadata, string first, string second) + { + var result = new Dictionary(metadata, StringComparer.Ordinal); + if (result.TryGetValue(first, out var firstValue) && result.TryGetValue(second, out var secondValue)) + { result[first] = secondValue; result[second] = firstValue; } + return result; + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index a6dd286..9297ce2 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -18,6 +18,7 @@ public sealed class EntryPoint : IExtensionApplication var manifest = CompositeModuleManifest.Validate(); RuntimeDiagnostics.Write("CompositeModulesValidated", resultStatus: "Validated", details: new Dictionary { ["PackageVersion"] = manifest.Version, ["ModuleCount"] = manifest.Modules.Count }); RibbonIntegration.Register(); + SwapAttributesContextMenu.Attach(); // Test automation is driven only by the explicitly generated /b script. // Initialize remains production-equivalent and must not orchestrate commands. RuntimeDiagnostics.Write("AssemblyInitializeCompleted"); @@ -27,6 +28,7 @@ public sealed class EntryPoint : IExtensionApplication { RuntimeDiagnostics.Write("AssemblyTerminateStarted"); RibbonIntegration.Terminate(); + SwapAttributesContextMenu.Detach(); RuntimeDiagnostics.Write("AssemblyTerminateCompleted"); } @@ -49,6 +51,7 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual(); [CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport(); [CommandMethod("PXP-UOB")] public void UpdateOrthoViewBlocksCommand() => PRecAnnot.Plant3D2026.UpdateOrthoViewBlocksCommand.Run(); + [CommandMethod("PXP-AC", CommandFlags.UsePickSet)] public void SwapAttributesCommand() => PRecAnnot.Plant3D2026.SwapAttributesCommand.Run(); [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); 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 cfe7ca8..3c844df 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj @@ -17,6 +17,7 @@ $(AutoCAD2026ManagedDir)\PnP3dPartsRepository.dllfalsefalse $(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.dllfalsefalse $(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dllfalsefalse + $(Plant3D2026ManagedDir)\PnP3dOrthoDrawingsUI.dllfalsefalse $(Plant3D2026ManagedDir)\PnPDwg2dDef.dllfalsefalse $(Plant3D2026ManagedDir)\PnPDwg2dUtil.dllfalsefalse diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/SwapAttributesCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/SwapAttributesCommand.cs new file mode 100644 index 0000000..abe04cb --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/SwapAttributesCommand.cs @@ -0,0 +1,86 @@ +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.Runtime; +using Autodesk.AutoCAD.Windows; +using Autodesk.ProcessPower.PlantInstance; +using PRecAnnot.Core; +using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; + +namespace PRecAnnot.Plant3D2026; + +internal static class SwapAttributesCommand +{ + private const string ApplicationName = "PXP-ALC"; + + public static void Run() + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null || PlantApplication.CurrentProject is null) return; + var selected = document.Editor.SelectImplied(); + if (selected.Status != PromptStatus.OK || selected.Value.Count == 0) selected = document.Editor.GetSelection(); + if (selected.Status != PromptStatus.OK || selected.Value.Count == 0) return; + using var documentLock = document.LockDocument(); + using var transaction = document.Database.TransactionManager.StartTransaction(); + if (transaction.GetObject(selected.Value.GetObjectIds()[0], OpenMode.ForWrite, false) is not BlockReference block || block.AttributeCollection.Count != 2) + { document.Editor.WriteMessage("\nPXP-AC requires a block with exactly two attributes."); return; } + var first = transaction.GetObject(block.AttributeCollection[0], OpenMode.ForWrite, false) as AttributeReference; + var second = transaction.GetObject(block.AttributeCollection[1], OpenMode.ForWrite, false) as AttributeReference; + if (first is null || second is null || first.IsConstant || second.IsConstant || !first.IsMTextAttribute || !second.IsMTextAttribute) + { document.Editor.WriteMessage("\nPXP-AC requires two editable multiline attributes."); return; } + var data = block.GetXDataForApplication(ApplicationName)?.AsArray(); + if (data is null) { document.Editor.WriteMessage("\nRun PXP-ALC before swapping connection attributes."); return; } + var firstText = first.MTextAttribute.Contents; + var secondText = second.MTextAttribute.Contents; + first.TextString = secondText; second.TextString = firstText; + first.UpdateMTextAttribute(); second.UpdateMTextAttribute(); + ResizeShelf(block, first, second); + SwapXData(data, "LINENUMBER1", "LINENUMBER2"); + SwapXData(data, "ANNOTATIONTEXT1", "ANNOTATIONTEXT2"); + block.XData = new ResultBuffer(data); + transaction.Commit(); + document.Editor.WriteMessage("\nPXP-AC: attributes swapped and shelf resized; AutoCAD UNDO is available."); + RuntimeDiagnostics.Write("SwapAttributesCompleted", commandName: "PXP-AC", resultStatus: "Applied", document: document); + } + + private static void ResizeShelf(BlockReference block, AttributeReference first, AttributeReference second) + { + var rotation = block.Rotation; + var firstOffset = new Point3d(block.Position.GetVectorTo(first.AlignmentPoint).ToArray()).RotateBy(-rotation, Vector3d.ZAxis, Point3d.Origin); + var secondOffset = new Point3d(block.Position.GetVectorTo(second.AlignmentPoint).ToArray()).RotateBy(-rotation, Vector3d.ZAxis, Point3d.Origin); + first.Rotation = 0; second.Rotation = 0; first.UpdateMTextAttribute(); second.UpdateMTextAttribute(); + var length = AttributeSwapPlanner.ComputeShelfLength( + first.MTextAttribute.GeometricExtents.MaxPoint.X - first.MTextAttribute.GeometricExtents.MinPoint.X, firstOffset.X, + second.MTextAttribute.GeometricExtents.MaxPoint.X - second.MTextAttribute.GeometricExtents.MinPoint.X, secondOffset.X); + foreach (DynamicBlockReferenceProperty property in block.DynamicBlockReferencePropertyCollection) + if (string.Equals(property.PropertyName, "L", StringComparison.OrdinalIgnoreCase) && !property.ReadOnly) { property.Value = Math.Abs(length); break; } + first.Rotation = rotation; second.Rotation = rotation; first.UpdateMTextAttribute(); second.UpdateMTextAttribute(); + } + + private static void SwapXData(TypedValue[] values, string firstMarker, string secondMarker) + { + var first = Array.FindIndex(values, value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value?.ToString(), firstMarker, StringComparison.Ordinal)); + var second = Array.FindIndex(values, value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value?.ToString(), secondMarker, StringComparison.Ordinal)); + if (first < 0 || second < 0 || first + 1 >= values.Length || second + 1 >= values.Length) return; + (values[first + 1], values[second + 1]) = (values[second + 1], values[first + 1]); + } +} + +internal static class SwapAttributesContextMenu +{ + private static ContextMenuExtension? menu; + public static void Attach() + { + if (menu is not null) return; + menu = new ContextMenuExtension(); + var item = new MenuItem("Swap attributes and resize shelf"); + item.Click += OnClick; menu.MenuItems.Add(item); + AcadApplication.AddObjectContextMenuExtension(RXObject.GetClass(typeof(BlockReference)), menu); + } + public static void Detach() + { + if (menu is null) return; + AcadApplication.RemoveObjectContextMenuExtension(RXObject.GetClass(typeof(BlockReference)), menu); menu = null; + } + private static void OnClick(object? sender, EventArgs args) => AcadApplication.DocumentManager.MdiActiveDocument?.SendStringToExecute("_.PXP-AC ", true, false, false); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoViewBlocksCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoViewBlocksCommand.cs index c27b8df..004c82a 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoViewBlocksCommand.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoViewBlocksCommand.cs @@ -1,5 +1,9 @@ using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; +using Autodesk.ProcessPower.DataObjects; +using Autodesk.ProcessPower.Drawings2d; +using Autodesk.ProcessPower.PlantInstance; +using Autodesk.ProcessPower.PnP3dOrthoDrawingsUI; using PRecAnnot.Abstractions; using PRecAnnot.Core; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application; @@ -39,13 +43,14 @@ internal static class UpdateOrthoViewBlocksCommand ValidateFixture(document); using var documentLock = document.LockDocument(); using var write = document.Database.TransactionManager.StartTransaction(); var layerTable = (LayerTable)write.GetObject(document.Database.LayerTableId, OpenMode.ForRead); var linetypes = (LinetypeTable)write.GetObject(document.Database.LinetypeTableId, OpenMode.ForRead); + var representationCount = ApplyRepresentationRules(document, selected.ObjectId, viewName, rules, write, linetypes); foreach (var change in changes) { var layer = (LayerTableRecord)write.GetObject(layerTable[change.Layer], OpenMode.ForWrite); Apply(layer, change.Rule, linetypes); } var viewportToUpdate = (Viewport)write.GetObject(selected.ObjectId, OpenMode.ForWrite); if (layerTable.Has("Defpoints")) viewportToUpdate.LayerId = layerTable["Defpoints"]; - write.Commit(); document.Editor.WriteMessage($"\nPXP-UOB Apply: updated-layers={changes.Count}; AutoCAD UNDO is available."); - RuntimeDiagnostics.Write("UpdateOrthoViewBlocksApplyCompleted", commandName: "PXP-UOB", resultStatus: "Applied", document: document, details: new Dictionary { ["LayerCount"] = changes.Count }); + write.Commit(); document.Editor.WriteMessage($"\nPXP-UOB Apply: updated-layers={changes.Count}; updated-representations={representationCount}; AutoCAD UNDO is available."); + RuntimeDiagnostics.Write("UpdateOrthoViewBlocksApplyCompleted", commandName: "PXP-UOB", resultStatus: "Applied", document: document, details: new Dictionary { ["LayerCount"] = changes.Count, ["RepresentationCount"] = representationCount }); } catch (Exception exception) { RuntimeDiagnostics.Write("UpdateOrthoViewBlocksFailed", "ERROR", commandName: "PXP-UOB", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-UOB: " + exception.Message); } } @@ -56,6 +61,47 @@ internal static class UpdateOrthoViewBlocksCommand if (rule.Red.HasValue) layer.Color = AcadColor.FromRgb(rule.Red.Value, rule.Green!.Value, rule.Blue!.Value); else if (rule.ColorIndex.HasValue) layer.Color = AcadColor.FromColorIndex(Autodesk.AutoCAD.Colors.ColorMethod.ByColor, rule.ColorIndex.Value); } + private static int ApplyRepresentationRules(Autodesk.AutoCAD.ApplicationServices.Document document, ObjectId viewportId, string viewName, IReadOnlyList rules, Transaction transaction, LinetypeTable linetypes) + { + var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("ORTHO_PIPING_DATALINKS_UNAVAILABLE"); + var database = manager.GetPnPDatabase(); + IDwgSheetDef sheet; + IDwgViewDef view; + List representations; + if (!UIUtils.GetDwgSheetFromOrthoDrawingName(document.Database.Filename, out sheet) || + !UIUtils.GetViewDefByViewName(viewName, sheet, out view) || + !UIUtils.GetOrthoEntityListInView(UIUtils.GetDwgSheetDefResolvedPath(sheet), viewName, out representations)) + throw new InvalidOperationException("ORTHO_VIEW_REPRESENTATIONS_UNAVAILABLE"); + var changed = 0; + foreach (var id in representations.Where(value => !value.IsNull).OrderBy(value => value.Handle.Value)) + { + if (transaction.GetObject(id, OpenMode.ForWrite, false) is not BlockReference block) continue; + var modelId = PnPDwg2dUtil.GetModelObjectId(id, document.Database); + var rowId = PnPDwg2dUtil.PipingRowId(modelId); + var engineering = database.Tables["EngineeringItems"].Select("PnPID=" + rowId); + var equipment = database.Tables["Equipment"].Select("PnPID=" + rowId); + if (engineering.Length == 1 && equipment.Length == 0) + { + var status = Convert.ToString(engineering[0]["Status"]) ?? string.Empty; + var statusRule = rules.FirstOrDefault(rule => rule.MatchesStatus(status)); + if (statusRule is not null) Apply(block, statusRule, linetypes); + } + else if (engineering.Length == 1 && equipment.Length == 1) block.LineWeight = LineWeight.ByLineWeightDefault; + if (transaction.GetObject(block.BlockTableRecord, OpenMode.ForRead) is BlockTableRecord definition) + foreach (ObjectId nestedId in definition) + if (transaction.GetObject(nestedId, OpenMode.ForWrite, false) is Entity nested && rules.FirstOrDefault(rule => rule.MatchesLayer(viewName, nested.Layer)) is { } nestedRule) + Apply(nested, nestedRule, linetypes); + changed++; + } + return changed; + } + private static void Apply(Entity entity, OrthoLayerRule rule, LinetypeTable linetypes) + { + if (rule.LineWeight.HasValue) entity.LineWeight = (LineWeight)rule.LineWeight.Value; + if (!string.IsNullOrWhiteSpace(rule.LineType) && linetypes.Has(rule.LineType)) entity.LinetypeId = linetypes[rule.LineType]; + if (rule.Red.HasValue) entity.Color = AcadColor.FromRgb(rule.Red.Value, rule.Green!.Value, rule.Blue!.Value); + else if (rule.ColorIndex.HasValue) entity.Color = AcadColor.FromColorIndex(Autodesk.AutoCAD.Colors.ColorMethod.ByColor, rule.ColorIndex.Value); + } private static void ValidateFixture(Autodesk.AutoCAD.ApplicationServices.Document document) { var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER"); diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AttributeSwapPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AttributeSwapPlannerTests.cs new file mode 100644 index 0000000..f44f7bb --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AttributeSwapPlannerTests.cs @@ -0,0 +1,14 @@ +using PRecAnnot.Core; +using Xunit; + +namespace PRecAnnot.Core.Tests; + +public sealed class AttributeSwapPlannerTests +{ + [Fact] public void Shelf_length_preserves_legacy_longer_attribute_rule() => Assert.Equal(6.9, AttributeSwapPlanner.ComputeShelfLength(4, 1, 5, 1)); + [Fact] public void Metadata_values_are_swapped_without_losing_other_ownership_fields() + { + var result = AttributeSwapPlanner.Swap(new Dictionary { ["LINENUMBER1"] = "10", ["LINENUMBER2"] = "20", ["VIEW"] = "A" }, "LINENUMBER1", "LINENUMBER2"); + Assert.Equal("20", result["LINENUMBER1"]); Assert.Equal("10", result["LINENUMBER2"]); Assert.Equal("A", result["VIEW"]); + } +}