From 2626b64e6310105173bfdc4db533450a9e3851b2 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 09:09:47 +0300 Subject: [PATCH] feat: activate p-rec-annot line-number edit commands --- .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 6 +- .../LineNumberEditCommands.cs | 70 +++++++++++++++++ .../LineNumberOwnedAnnotationReader.cs | 76 +++++++++++++++++++ .../LineNumberEditCommandRegistrationTests.cs | 44 +++++++++++ 4 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberEditCommands.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 1751f59..9c520d5 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -41,9 +41,9 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-LN")] public void AnnotateLineNumbersCommand() => RunLineNumberPreview(); [CommandMethod("PXP-ULN")] public void UpdateLineNumbersCommand() => RunDiagnostic("PXP-ULN"); - [CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => RunDiagnostic("PXP-ESN"); - [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => RunDiagnostic("PXP-ELN"); - [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => RunDiagnostic("PXP-RMP"); + [CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber(); + [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText(); + [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition(); [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberEditCommands.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberEditCommands.cs new file mode 100644 index 0000000..879ca13 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberEditCommands.cs @@ -0,0 +1,70 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.EditorInput; +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; + +namespace PRecAnnot.Plant3D2026; + +internal static class LineNumberEditCommands +{ + public static void Renumber() + { + Run("PXP-ESN", (document, editor, selected) => + { + var start = editor.GetInteger(new PromptIntegerOptions("\nВведите новое начальное значение маркера: ") { LowerLimit = 1 }); + if (start.Status != PromptStatus.OK) return null; + var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected); + return new LineNumberAnnotationWriter().ApplyEdits(document, LineNumberEditPlanner.Renumber(rows, start.Value)); + }, "Выберите блок списка номеров линий: "); + } + + public static void EditText() + { + Run("PXP-ELN", (document, editor, selected) => + { + var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected); + var marker = editor.GetInteger(new PromptIntegerOptions("\nВведите номер редактируемого маркера: ") { LowerLimit = 1 }); + if (marker.Status != PromptStatus.OK) return null; + var row = rows.SingleOrDefault(value => value.MarkerNumber == marker.Value) ?? throw new InvalidOperationException("LINE_NUMBER_ROW_NOT_FOUND"); + var text = editor.GetString(new PromptStringOptions("\nВведите новый текст номера линии: ") { AllowSpaces = true }); + if (text.Status != PromptStatus.OK) return null; + return new LineNumberAnnotationWriter().ApplyEdits(document, [LineNumberEditPlanner.EditText(row, text.StringResult)]); + }, "Выберите блок списка номеров линий: "); + } + + public static void ResetMarkerPosition() + { + Run("PXP-RMP", (document, _, selected) => + { + var row = new LineNumberOwnedAnnotationReader().ReadMarker(document, selected); + var configured = LineNumberRuntimeConfigurationReader.Read().Assets.Marker.Texts[0].Position; + return new LineNumberAnnotationWriter().ApplyEdits(document, [LineNumberEditPlanner.ResetMarkerAttributePosition(row, configured)]); + }, "Выберите маркер номера линии: "); + } + + private static void Run(string command, Func operation, string prompt) + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null) return; + try + { + var options = new PromptEntityOptions($"\n{prompt}"); + options.SetRejectMessage("\nТребуется вхождение блока PXP-LN."); + options.AddAllowedClass(typeof(BlockReference), true); + var selected = document.Editor.GetEntity(options); + if (selected.Status != PromptStatus.OK) return; + var result = operation(document, document.Editor, selected.ObjectId); + if (result is null) return; + document.Editor.WriteMessage($"\n{command}: updated={result.Updated}; unchanged={result.Unchanged}."); + RuntimeDiagnostics.Write("LineNumberEditCompleted", commandName: command, resultStatus: result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document, + details: new Dictionary { ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged }); + } + catch (System.Exception exception) + { + RuntimeDiagnostics.Write("LineNumberEditFailed", "ERROR", commandName: command, resultStatus: exception.Message, exception: exception, document: document); + document.Editor.WriteMessage($"\n{command}: {exception.Message}"); + } + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs new file mode 100644 index 0000000..06792a3 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberOwnedAnnotationReader.cs @@ -0,0 +1,76 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Geometry; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Plant3D2026; + +internal sealed class LineNumberOwnedAnnotationReader +{ + private const string ApplicationName = "PXP-LN"; + + public IReadOnlyList ReadList(Document document, ObjectId listId) + { + using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction(); + if (transaction.GetObject(listId, OpenMode.ForRead) is not BlockReference list || !HasRole(list, "LIST")) + throw new InvalidOperationException("LINE_NUMBER_LIST_NOT_SELECTED"); + + var rows = new Dictionary(); + foreach (ObjectId id in list.AttributeCollection) + { + var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForRead); + var ownership = ReadOwnership(attribute); + if (ownership is null) continue; + rows.TryGetValue(ownership.Value.MarkerNumber, out var row); + rows[ownership.Value.MarkerNumber] = ownership.Value.Role == "LINENUMBER" + ? (ownership.Value.StableId, attribute.TextString.Replace("\\P", "\n", StringComparison.Ordinal)) + : (ownership.Value.StableId ?? row.StableId, row.Text); + } + + var result = rows.OrderBy(value => value.Key) + .Select(value => new LineNumberOwnedRow( + value.Value.StableId ?? throw new InvalidOperationException("LINE_NUMBER_ROW_STABLE_ID_MISSING"), + value.Key, + value.Value.Text ?? throw new InvalidOperationException("LINE_NUMBER_ROW_TEXT_MISSING"), + default)) + .ToArray(); + if (result.Length == 0) throw new InvalidOperationException("LINE_NUMBER_LIST_EMPTY"); + return result; + } + + public LineNumberOwnedRow ReadMarker(Document document, ObjectId markerId) + { + using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction(); + if (transaction.GetObject(markerId, OpenMode.ForRead) is not BlockReference marker) + throw new InvalidOperationException("LINE_NUMBER_MARKER_NOT_SELECTED"); + var values = marker.GetXDataForApplication(ApplicationName)?.AsArray() ?? []; + var stableId = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string) + .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 = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).LastOrDefault() ?? string.Empty; + if (string.IsNullOrWhiteSpace(stableId) || markerNumber <= 0) throw new InvalidOperationException("LINE_NUMBER_MARKER_OWNERSHIP_MISSING"); + var localPosition = Point3d.Origin; + foreach (ObjectId id in marker.AttributeCollection) + { + var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForRead); + if (!string.Equals(attribute.Tag, "NUMBER", StringComparison.OrdinalIgnoreCase)) continue; + localPosition = attribute.Position.TransformBy(marker.BlockTransform.Inverse()); + break; + } + return new(stableId, markerNumber, text, new(localPosition.X, localPosition.Y, localPosition.Z)); + } + + 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; + + private static (string? StableId, string Role, int MarkerNumber)? ReadOwnership(AttributeReference attribute) + { + var values = attribute.GetXDataForApplication(ApplicationName)?.AsArray(); + if (values is null) return null; + var strings = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).ToArray(); + var role = strings.FirstOrDefault(value => value is "MARKER" or "LINENUMBER"); + var number = values.Where(value => value.TypeCode == 1071).Select(value => Convert.ToInt32(value.Value)).FirstOrDefault(); + return role is null || number <= 0 ? null : (strings.FirstOrDefault(value => value != role), role, number); + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs new file mode 100644 index 0000000..f1ee1cf --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberEditCommandRegistrationTests.cs @@ -0,0 +1,44 @@ +using Xunit; + +namespace PRecAnnot.Core.Tests; + +public sealed class LineNumberEditCommandRegistrationTests +{ + private static string ProjectRoot + { + get + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "PRecAnnot2026.sln"))) directory = directory.Parent; + return Path.Combine(directory!.FullName, "src", "PRecAnnot.Plant3D2026"); + } + } + + [Theory] + [InlineData("PXP-ESN", "LineNumberEditCommands.Renumber")] + [InlineData("PXP-ELN", "LineNumberEditCommands.EditText")] + [InlineData("PXP-RMP", "LineNumberEditCommands.ResetMarkerPosition")] + public void EditCommand_UsesMutationPipeline(string command, string handler) + { + var entry = File.ReadAllText(Path.Combine(ProjectRoot, "EntryPoint.cs")); + var declaration = entry.Split('\n').Single(value => value.Contains($"CommandMethod(\"{command}\")", StringComparison.Ordinal)); + Assert.Contains(handler, declaration); + Assert.DoesNotContain("RunDiagnostic", declaration); + } + + [Fact] + public void OwnedReader_RequiresPxpLnIdentityForListsAndMarkers() + { + var reader = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberOwnedAnnotationReader.cs")); + Assert.Contains("GetXDataForApplication(ApplicationName)", reader); + Assert.Contains("LINE_NUMBER_LIST_NOT_SELECTED", reader); + Assert.Contains("LINE_NUMBER_MARKER_OWNERSHIP_MISSING", reader); + } + + [Fact] + public void MutationCommands_UseOneTransactionalWriter() + { + var commands = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs")); + Assert.Equal(3, System.Text.RegularExpressions.Regex.Matches(commands, "ApplyEdits\\(document").Count); + } +}