From 10af8c8f1202472cabcef886c1992e75b775bbe8 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 08:22:40 +0300 Subject: [PATCH] feat: port p-rec-annot line-number command planning --- .../src/PRecAnnot.Abstractions/Contracts.cs | 4 ++++ .../LineNumberCommandPlanner.cs | 20 +++++++++++++++++++ .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 6 ++++++ .../LineNumberCommandPlannerTests.cs | 13 ++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 1723fd1..bfce26d 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -55,6 +55,10 @@ public interface ILineNumberAnnotationFormatter public sealed record DataLinksPropertyReadResult(bool Success, IReadOnlyDictionary Properties, string? FailureCode = null); public interface IDataLinksPropertyReader { DataLinksPropertyReadResult ReadProperties(long rowId); } public sealed record LineNumberFieldResolution(bool Success, IReadOnlyDictionary Values, string? FailureCode = null); +public sealed record LineNumberCandidate(string StableId, long RowId, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyDictionary Properties); +public sealed record ExistingLineNumberAnnotation(string StableId, string SourceStableId, string Text); +public sealed record LineNumberCommandOperation(string StableId, AnnotationOperationKind Kind, string? Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyList Reasons); +public sealed record LineNumberCommandPlan(IReadOnlyList Operations); public sealed record GeometrySnapshot { diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs new file mode 100644 index 0000000..4fb5b0d --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs @@ -0,0 +1,20 @@ +using PRecAnnot.Abstractions; +namespace PRecAnnot.Core; + +public sealed class LineNumberCommandPlanner(ILineNumberAnnotationFormatter formatter) +{ + public LineNumberCommandPlan Build(LineNumberAnnotationDefinition definition, IEnumerable candidates, IEnumerable existing) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var indexed = existing.GroupBy(x => x.SourceStableId, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase); + var result = new List(); + foreach (var candidate in candidates.OrderBy(x => x.StableId, StringComparer.Ordinal)) + { + if (!seen.Add(candidate.StableId)) { result.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_CANDIDATE"])); continue; } + var text = formatter.Format(definition, candidate.Properties); + if (!text.Success) { result.Add(new(candidate.StableId, AnnotationOperationKind.Skip, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, [text.ErrorCode ?? "FORMAT_FAILED"])); continue; } + result.Add(new(candidate.StableId, indexed.ContainsKey(candidate.StableId) ? AnnotationOperationKind.Update : AnnotationOperationKind.Create, text.Text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, Array.Empty())); + } + return new(result); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 14481b0..3a4c495 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -35,6 +35,12 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-OA")] public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA"); + [CommandMethod("PXP-LN")] public void AnnotateLineNumbersCommand() => RunDiagnostic("PXP-LN"); + [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("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs new file mode 100644 index 0000000..8c7952a --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs @@ -0,0 +1,13 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +namespace PRecAnnot.Core.Tests; +public sealed class LineNumberCommandPlannerTests +{ + [Fact] public void ProducesDeterministicCreateUpdateAndDuplicateDecisions() + { + var def = new LineNumberAnnotationDefinition { Nickname="LN", Tags=[new() { Field="EngineeringItems.LineNumber" }] }; + var c = new LineNumberCandidate("b", 1, new(0,0), 0, "PXP", "Standard", new Dictionary { ["EngineeringItems.LineNumber"]="L-2" }); + var plan = new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()).Build(def, [c, c, c with { StableId="a" }], [new("x", "b", "old")]); + Assert.Collection(plan.Operations, x => Assert.Equal(AnnotationOperationKind.Create,x.Kind), x => Assert.Equal(AnnotationOperationKind.Update,x.Kind), x => Assert.Equal(AnnotationOperationKind.Conflict,x.Kind)); + } +}