feat: port p-rec-annot ortho status and attribute maintenance

This commit is contained in:
codex
2026-08-07 13:21:32 +03:00
parent d4016237d8
commit b381f83bbd
6 changed files with 167 additions and 2 deletions
@@ -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<string, string> Swap(IReadOnlyDictionary<string, string> metadata, string first, string second)
{
var result = new Dictionary<string, string>(metadata, StringComparer.Ordinal);
if (result.TryGetValue(first, out var firstValue) && result.TryGetValue(second, out var secondValue))
{ result[first] = secondValue; result[second] = firstValue; }
return result;
}
}
@@ -18,6 +18,7 @@ public sealed class EntryPoint : IExtensionApplication
var manifest = CompositeModuleManifest.Validate(); var manifest = CompositeModuleManifest.Validate();
RuntimeDiagnostics.Write("CompositeModulesValidated", resultStatus: "Validated", details: new Dictionary<string, object?> { ["PackageVersion"] = manifest.Version, ["ModuleCount"] = manifest.Modules.Count }); RuntimeDiagnostics.Write("CompositeModulesValidated", resultStatus: "Validated", details: new Dictionary<string, object?> { ["PackageVersion"] = manifest.Version, ["ModuleCount"] = manifest.Modules.Count });
RibbonIntegration.Register(); RibbonIntegration.Register();
SwapAttributesContextMenu.Attach();
// Test automation is driven only by the explicitly generated /b script. // Test automation is driven only by the explicitly generated /b script.
// Initialize remains production-equivalent and must not orchestrate commands. // Initialize remains production-equivalent and must not orchestrate commands.
RuntimeDiagnostics.Write("AssemblyInitializeCompleted"); RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
@@ -27,6 +28,7 @@ public sealed class EntryPoint : IExtensionApplication
{ {
RuntimeDiagnostics.Write("AssemblyTerminateStarted"); RuntimeDiagnostics.Write("AssemblyTerminateStarted");
RibbonIntegration.Terminate(); RibbonIntegration.Terminate();
SwapAttributesContextMenu.Detach();
RuntimeDiagnostics.Write("AssemblyTerminateCompleted"); RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
} }
@@ -49,6 +51,7 @@ public sealed class EntryPoint : IExtensionApplication
[CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual(); [CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual();
[CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport(); [CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport();
[CommandMethod("PXP-UOB")] public void UpdateOrthoViewBlocksCommand() => PRecAnnot.Plant3D2026.UpdateOrthoViewBlocksCommand.Run(); [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)] [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
@@ -17,6 +17,7 @@
<Reference Include="PnP3dPartsRepository"><HintPath>$(AutoCAD2026ManagedDir)\PnP3dPartsRepository.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference> <Reference Include="PnP3dPartsRepository"><HintPath>$(AutoCAD2026ManagedDir)\PnP3dPartsRepository.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dProjectPartsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference> <Reference Include="PnP3dProjectPartsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference> <Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dOrthoDrawingsUI"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoDrawingsUI.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference> <Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference> <Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
</ItemGroup> </ItemGroup>
@@ -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);
}
@@ -1,5 +1,9 @@
using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput; 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.Abstractions;
using PRecAnnot.Core; using PRecAnnot.Core;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
@@ -39,13 +43,14 @@ internal static class UpdateOrthoViewBlocksCommand
ValidateFixture(document); ValidateFixture(document);
using var documentLock = document.LockDocument(); using var write = document.Database.TransactionManager.StartTransaction(); 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 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) foreach (var change in changes)
{ {
var layer = (LayerTableRecord)write.GetObject(layerTable[change.Layer], OpenMode.ForWrite); Apply(layer, change.Rule, linetypes); 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"]; 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."); 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<string, object?> { ["LayerCount"] = changes.Count }); RuntimeDiagnostics.Write("UpdateOrthoViewBlocksApplyCompleted", commandName: "PXP-UOB", resultStatus: "Applied", document: document, details: new Dictionary<string, object?> { ["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); } 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); 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); 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<OrthoLayerRule> 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<ObjectId> 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) private static void ValidateFixture(Autodesk.AutoCAD.ApplicationServices.Document document)
{ {
var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER"); var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER");
@@ -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<string, string> { ["LINENUMBER1"] = "10", ["LINENUMBER2"] = "20", ["VIEW"] = "A" }, "LINENUMBER1", "LINENUMBER2");
Assert.Equal("20", result["LINENUMBER1"]); Assert.Equal("10", result["LINENUMBER2"]); Assert.Equal("A", result["VIEW"]);
}
}