From 95ae9dfd1872b6d674b0ec4c3e66ab5ba65ba574 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 09:19:56 +0300 Subject: [PATCH] feat: port p-rec-annot line-number refresh pipeline --- .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 2 +- .../LineNumberOwnedAnnotationReader.cs | 20 ++++++ .../LineNumberRefreshCommand.cs | 64 +++++++++++++++++++ .../LineNumberEditCommandRegistrationTests.cs | 13 ++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRefreshCommand.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 9c520d5..feedf6d 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -40,7 +40,7 @@ public sealed class EntryPoint : IExtensionApplication public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA"); [CommandMethod("PXP-LN")] public void AnnotateLineNumbersCommand() => RunLineNumberPreview(); - [CommandMethod("PXP-ULN")] public void UpdateLineNumbersCommand() => RunDiagnostic("PXP-ULN"); + [CommandMethod("PXP-ULN")] public void UpdateLineNumbersCommand() => LineNumberRefreshCommand.Run(); [CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber(); [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText(); [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition(); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs index 06792a3..4f8ccb3 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs @@ -60,6 +60,26 @@ internal sealed class LineNumberOwnedAnnotationReader return new(stableId, markerNumber, text, new(localPosition.X, localPosition.Y, localPosition.Z)); } + public IReadOnlyList ReadMarkers(Document document) + { + using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction(); + var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead); + var rows = new List(); + foreach (ObjectId id in paper) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference marker || !HasRole(marker, "MARKER")) continue; + var values = marker.GetXDataForApplication(ApplicationName)?.AsArray() ?? []; + var strings = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).ToArray(); + var stableId = strings.FirstOrDefault(value => !string.Equals(value, "MARKER", StringComparison.OrdinalIgnoreCase)); + var markerNumber = values.Where(value => value.TypeCode == 1071).Select(value => Convert.ToInt32(value.Value)).FirstOrDefault(); + var text = strings.LastOrDefault() ?? string.Empty; + if (string.IsNullOrWhiteSpace(stableId) || markerNumber <= 0) continue; + rows.Add(new(stableId, markerNumber, text, default)); + } + return rows.OrderBy(value => value.MarkerNumber).ThenBy(value => value.SourceStableId, StringComparer.Ordinal).ToArray(); + } + private static bool HasRole(BlockReference reference, string role) => reference.GetXDataForApplication(ApplicationName)?.AsArray().Any(value => value.TypeCode == 1000 && string.Equals(value.Value as string, role, StringComparison.OrdinalIgnoreCase)) == true; diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRefreshCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRefreshCommand.cs new file mode 100644 index 0000000..ad0f280 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRefreshCommand.cs @@ -0,0 +1,64 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.ProcessPower.PlantInstance; +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; + +namespace PRecAnnot.Plant3D2026; + +// PXP-ULN refreshes the source-backed text of already-owned annotations. The +// legacy command drives the same operation across selected Ortho drawings; the +// modern command keeps the mutation pipeline scoped to the active controlled +// drawing until the Ortho drawing enumerator is ported. +internal static class LineNumberRefreshCommand +{ + public static void Run() + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null) return; + try + { + var configuration = LineNumberRuntimeConfigurationReader.Read(); + var definition = configuration.Annotations.Annotations.OrderBy(value => value.Nickname, StringComparer.Ordinal).First(); + var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("LINE_NUMBER_PIPING_DATALINKS_UNAVAILABLE"); + var owned = new LineNumberOwnedAnnotationReader().ReadMarkers(document); + if (owned.Count == 0) throw new InvalidOperationException("LINE_NUMBER_OWNED_MARKERS_NOT_FOUND"); + var candidates = owned.Select(row => ReadCandidate(document.Database, row, manager, configuration.Assets)).ToArray(); + var preview = new LineNumberCommandPreviewService( + new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()), + new LineNumberFieldResolver(new PlantDataLinksPropertyReader(manager))) + .Preview(definition, candidates, owned.Select(row => new ExistingLineNumberAnnotation(row.SourceStableId, row.SourceStableId, row.Text))); + if (!preview.Success) throw new InvalidOperationException(preview.FailureCode); + + var rows = owned.ToDictionary(row => row.SourceStableId, StringComparer.OrdinalIgnoreCase); + var apply = preview.Plan!.Operations.Where(operation => operation.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update) + .Select(operation => new LineNumberApplyPreflight(operation.StableId, rows[operation.StableId].MarkerNumber, operation.Text!, operation.Position, operation.Rotation, operation.Layer, operation.TextStyle)) + .OrderBy(operation => operation.MarkerNumber).ToArray(); + var writer = new LineNumberAnnotationWriter(); + var markerResult = writer.Apply(document, apply, configuration.Assets); + var mutations = apply.Select(item => new LineNumberRowMutation(item.StableId, rows[item.StableId].MarkerNumber, item.MarkerNumber, item.Text, null)).ToArray(); + var listResult = writer.ApplyEdits(document, mutations); + document.Editor.WriteMessage($"\nPXP-ULN: updated markers={markerResult.Updated}; lists={listResult.Updated}; already-current={markerResult.Unchanged}."); + RuntimeDiagnostics.Write("LineNumberRefreshCompleted", commandName: "PXP-ULN", resultStatus: markerResult.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document, + details: new Dictionary { ["UpdatedMarkers"] = markerResult.Updated, ["UpdatedLists"] = listResult.Updated, ["AlreadyCurrent"] = markerResult.Unchanged, ["Scope"] = "ActiveDrawing" }); + } + catch (System.Exception exception) + { + RuntimeDiagnostics.Write("LineNumberRefreshFailed", "ERROR", commandName: "PXP-ULN", resultStatus: exception.Message, exception: exception, document: document); + document.Editor.WriteMessage($"\nPXP-ULN: {exception.Message}"); + } + } + + private static LineNumberCandidate ReadCandidate(Database database, LineNumberOwnedRow row, Autodesk.ProcessPower.DataLinks.DataLinksManager manager, AnnotateLineNumbersAssets assets) + { + const string prefix = "acad:"; + if (!row.SourceStableId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_NUMBER_SOURCE_ID_UNSUPPORTED"); + if (!long.TryParse(row.SourceStableId[prefix.Length..], System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var handle)) + throw new InvalidOperationException("LINE_NUMBER_SOURCE_HANDLE_INVALID"); + var id = database.GetObjectId(false, new Handle(handle), 0); + var read = new PlantLineNumberCandidateReader(manager).Read(id, assets.Marker.Layer, assets.Marker.Texts[0].TextStyle); + if (!read.Success) throw new InvalidOperationException(read.FailureCode); + return read.Candidate!; + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs index f1ee1cf..6afce6b 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs @@ -41,4 +41,17 @@ public sealed class LineNumberEditCommandRegistrationTests var commands = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs")); Assert.Equal(3, System.Text.RegularExpressions.Regex.Matches(commands, "ApplyEdits\\(document").Count); } + + [Fact] + public void UpdateCommand_UsesOwnedSourceRefreshInsteadOfDiagnosticOnlyHandler() + { + var entry = File.ReadAllText(Path.Combine(ProjectRoot, "EntryPoint.cs")); + var declaration = entry.Split('\n').Single(value => value.Contains("CommandMethod(\"PXP-ULN\")", StringComparison.Ordinal)); + Assert.Contains("LineNumberRefreshCommand.Run", declaration); + Assert.DoesNotContain("RunDiagnostic", declaration); + var refresh = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberRefreshCommand.cs")); + Assert.Contains("PlantLineNumberCandidateReader", refresh); + Assert.Contains("LineNumberCommandPreviewService", refresh); + Assert.Contains("ApplyEdits", refresh); + } }