feat: port p-rec-annot line connection boundary rules

This commit is contained in:
codex
2026-08-07 09:24:32 +03:00
parent 43c9e42b5b
commit a8749ffa69
3 changed files with 78 additions and 0 deletions
@@ -70,6 +70,18 @@ public sealed record ElevationAnnotationOperation(string StableId, AnnotationOpe
public sealed record ElevationAnnotationPlan(IReadOnlyList<ElevationAnnotationOperation> 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<LineConnectionCandidate> 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<string> 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);
@@ -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<LineConnectionBoundaryPlan> Build(IEnumerable<LineConnectionBoundaryCandidate> candidates)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var result = new List<LineConnectionBoundaryPlan>();
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<string>()));
}
return result;
}
}
@@ -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);
}