feat: add p-rec-annot line connection planning

This commit is contained in:
codex
2026-08-07 09:00:25 +03:00
parent 6f39bb204d
commit 7090321167
3 changed files with 73 additions and 0 deletions
@@ -68,6 +68,8 @@ public sealed record ElevationCandidate(string StableId, double ModelElevation,
public sealed record ExistingElevationAnnotation(string StableId, string SourceStableId, string Text, AnnotationPoint Position);
public sealed record ElevationAnnotationOperation(string StableId, AnnotationOperationKind Kind, string? Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyList<string> Reasons);
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);
public sealed record LineNumberTextAsset(string Layer, string TextStyle, double Size, string TextValue, Point2Dto Position);
public sealed record LineNumberLineAsset(string Layer, Point2Dto Start, Point2Dto End);
public sealed record LineNumberMarkerAsset(string Layer, double MaskScaleFactor, IReadOnlyList<LineNumberTextAsset> Texts, IReadOnlyList<LineNumberLineAsset> Lines);
@@ -0,0 +1,42 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class LineNumberConnectionPlanner
{
public IReadOnlyList<LineConnectionGroup> Build(IEnumerable<LineConnectionCandidate> candidates, double tolerance)
{
if (tolerance < 0 || double.IsNaN(tolerance) || double.IsInfinity(tolerance)) throw new ArgumentOutOfRangeException(nameof(tolerance));
return candidates
.Where(value => !string.IsNullOrWhiteSpace(value.LineNumber))
.OrderBy(value => value.StableId, StringComparer.Ordinal)
.GroupBy(value => value.LineNumber, StringComparer.OrdinalIgnoreCase)
.Select(group => BuildGroup(group.Key, group.ToArray(), tolerance))
.Where(group => group is not null)
.Cast<LineConnectionGroup>()
.OrderBy(group => group.LineNumber, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static LineConnectionGroup? BuildGroup(string number, LineConnectionCandidate[] members, double tolerance)
{
if (members.Length < 2) return null;
var connected = members.Where(first => members.Any(second => !ReferenceEquals(first, second) && Connected(first, second, tolerance))).OrderBy(value => value.StableId, StringComparer.Ordinal).ToArray();
if (connected.Length < 2) return null;
var anchor = connected[0];
return new(number, connected, anchor.Position, anchor.Rotation);
}
private static bool Connected(LineConnectionCandidate first, LineConnectionCandidate second, double tolerance)
{
if (Distance(first.FirstEndpoint, second.FirstEndpoint) <= tolerance || Distance(first.FirstEndpoint, second.SecondEndpoint) <= tolerance || Distance(first.SecondEndpoint, second.FirstEndpoint) <= tolerance || Distance(first.SecondEndpoint, second.SecondEndpoint) <= tolerance)
return true;
return ExtentsIntersectionClassifier.Classify(first.Bounds, second.Bounds) is not ExtentsIntersectionType.DoNotIntersect;
}
private static double Distance(Point3Dto first, Point3Dto second)
{
var x = first.X - second.X; var y = first.Y - second.Y; var z = first.Z - second.Z;
return Math.Sqrt(x * x + y * y + z * z);
}
}
@@ -0,0 +1,29 @@
using PRecAnnot.Abstractions;
using PRecAnnot.Core;
namespace PRecAnnot.Core.Tests;
public sealed class LineNumberConnectionPlannerTests
{
[Fact]
public void GroupsConnectedSameLineCandidatesInStableOrder()
{
var a = Candidate("a", "L-1", new(0, 0, 0), new(1, 0, 0));
var b = Candidate("b", "l-1", new(1.05, 0, 0), new(2, 0, 0));
var c = Candidate("c", "L-2", new(0, 0, 0), new(1, 0, 0));
var result = new LineNumberConnectionPlanner().Build([b, c, a], .1);
var group = Assert.Single(result);
Assert.Equal("L-1", group.LineNumber);
Assert.Equal(new[] { "a", "b" }, group.Members.Select(value => value.StableId));
}
[Fact]
public void DoesNotGroupDisjointOrSingleLineCandidates()
{
var a = Candidate("a", "L-1", new(0, 0, 0), new(1, 0, 0));
var b = Candidate("b", "L-1", new(10, 0, 0), new(11, 0, 0));
Assert.Empty(new LineNumberConnectionPlanner().Build([a, b], .1));
}
private static LineConnectionCandidate Candidate(string id, string line, Point3Dto first, Point3Dto second) => new(id, line, first, second, new(new(Math.Min(first.X, second.X), Math.Min(first.Y, second.Y), Math.Min(first.Z, second.Z)), new(Math.Max(first.X, second.X), Math.Max(first.Y, second.Y), Math.Max(first.Z, second.Z))), new(first.X, first.Y), 0);
}