From a8749ffa693ab5aa5a250fccccc2085c5f3673b9 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 09:24:32 +0300 Subject: [PATCH] feat: port p-rec-annot line connection boundary rules --- .../src/PRecAnnot.Abstractions/Contracts.cs | 12 ++++++ .../LineConnectionBoundaryPlanner.cs | 38 +++++++++++++++++++ .../LineConnectionBoundaryPlannerTests.cs | 28 ++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index d8b09a0..3704eef 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -70,6 +70,18 @@ public sealed record ElevationAnnotationOperation(string StableId, AnnotationOpe public sealed record ElevationAnnotationPlan(IReadOnlyList Operations); public sealed record LineConnectionCandidate(string StableId, string LineNumber, Point3Dto FirstEndpoint, Point3Dto SecondEndpoint, Bounds3Dto Bounds, AnnotationPoint Position, double Rotation); public sealed record LineConnectionGroup(string LineNumber, IReadOnlyList Members, AnnotationPoint Placement, double Rotation); +// A legacy PXP-ALC annotation belongs to a connector between two *different* +// line groups; it is not a duplicate marker for one line group. +public sealed record LineConnectionBoundaryCandidate( + string StableId, + string FirstLineNumber, + string SecondLineNumber, + Point3Dto FirstEndpoint, + Point3Dto SecondEndpoint, + Bounds3Dto Bounds, + AnnotationPoint Position, + double Rotation); +public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList Reasons); public sealed record LineNumberOwnedRow(string SourceStableId, int MarkerNumber, string Text, AnnotationPoint MarkerAttributePosition); public sealed record LineNumberRowMutation(string SourceStableId, int PreviousMarkerNumber, int MarkerNumber, string Text, AnnotationPoint? MarkerAttributePosition); public sealed record LineNumberTextAsset(string Layer, string TextStyle, double Size, string TextValue, Point2Dto Position); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs new file mode 100644 index 0000000..b4179aa --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs @@ -0,0 +1,38 @@ +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +// Source-backed PXP-ALC decision logic: only a connector whose related parts +// resolve to different line groups produces an annotation. The connector is +// the stable ownership identity, preserving repeat/update behaviour. +public sealed class LineConnectionBoundaryPlanner +{ + public IReadOnlyList Build(IEnumerable candidates) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + foreach (var candidate in candidates.OrderBy(value => value.StableId, StringComparer.Ordinal)) + { + if (!seen.Add(candidate.StableId)) + { + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"])); + continue; + } + var first = candidate.FirstLineNumber?.Trim() ?? string.Empty; + var second = candidate.SecondLineNumber?.Trim() ?? string.Empty; + if (first.Length == 0 || second.Length == 0) + { + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"])); + continue; + } + if (string.Equals(first, second, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"])); + continue; + } + var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray(); + result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty())); + } + return result; + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs new file mode 100644 index 0000000..bfa7593 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionBoundaryPlannerTests.cs @@ -0,0 +1,28 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class LineConnectionBoundaryPlannerTests +{ + [Fact] + public void EmitsSortedTextOnlyForDifferentLineGroups() + { + var result = new LineConnectionBoundaryPlanner().Build([ + Candidate("b", "L-200", "L-100"), Candidate("a", "L-100", "L-100")]); + Assert.Equal("a", result[0].StableId); + Assert.Contains("SAME_LINE_GROUP", result[0].Reasons); + Assert.Equal("L-100 / L-200", result[1].Text); + Assert.Empty(result[1].Reasons); + } + + [Fact] + public void RejectsDuplicateAndMissingLineGroupsDeterministically() + { + var result = new LineConnectionBoundaryPlanner().Build([Candidate("a", "", "L-2"), Candidate("a", "L-1", "L-2")]); + Assert.Contains("LINE_GROUP_NOT_RESOLVED", result[0].Reasons); + Assert.Contains("DUPLICATE_CONNECTOR_CANDIDATE", result[1].Reasons); + } + + private static LineConnectionBoundaryCandidate Candidate(string id, string first, string second) => new(id, first, second, new(), new(1, 1, 1), new(new(), new(1, 1, 1)), new(2, 3), 0); +}