feat: add p-rec-annot elevation annotation planning

This commit is contained in:
codex
2026-08-07 08:59:18 +03:00
parent 3128568965
commit 6f39bb204d
3 changed files with 71 additions and 0 deletions
@@ -64,6 +64,10 @@ public sealed record LineNumberApplyResult(int Created, int Updated, int Unchang
public sealed record LineNumberListApplyPreflight(string StableId, AnnotationPoint Position, double Rotation, IReadOnlyList<LineNumberApplyPreflight> Rows);
public sealed record LineNumberPreviewResult(bool Success, LineNumberCommandPlan? Plan, IReadOnlyList<LineNumberApplyPreflight> Apply, string? FailureCode = null);
public sealed record LineNumberCandidateReadResult(bool Success, LineNumberCandidate? Candidate, string? FailureCode = null);
public sealed record ElevationCandidate(string StableId, double ModelElevation, double DatumOffset, AnnotationPoint Position, double Rotation, string Layer, string TextStyle);
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 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,39 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class ElevationAnnotationPlanner
{
public ElevationAnnotationPlan Build(IEnumerable<ElevationCandidate> candidates, IEnumerable<ExistingElevationAnnotation> existing)
{
var known = existing.GroupBy(value => value.SourceStableId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.OrdinalIgnoreCase);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var operations = new List<ElevationAnnotationOperation>();
foreach (var candidate in candidates.OrderBy(value => value.StableId, StringComparer.Ordinal))
{
if (!seen.Add(candidate.StableId))
{
operations.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_CANDIDATE"]));
continue;
}
if (string.IsNullOrWhiteSpace(candidate.Layer) || string.IsNullOrWhiteSpace(candidate.TextStyle) || double.IsNaN(candidate.ModelElevation) || double.IsInfinity(candidate.ModelElevation))
{
operations.Add(new(candidate.StableId, AnnotationOperationKind.Skip, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["ELEVATION_CANDIDATE_INVALID"]));
continue;
}
var text = LegacyElevationFormatter.Calculate(candidate.ModelElevation, candidate.DatumOffset);
var matches = known.GetValueOrDefault(candidate.StableId, []);
if (matches.Length > 1)
{
operations.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_EXISTING_ANNOTATION"]));
continue;
}
if (matches.Length == 1 && string.Equals(matches[0].Text, text, StringComparison.Ordinal) && matches[0].Position == candidate.Position)
operations.Add(new(candidate.StableId, AnnotationOperationKind.Skip, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["ALREADY_CURRENT"]));
else
operations.Add(new(candidate.StableId, matches.Length == 1 ? AnnotationOperationKind.Update : AnnotationOperationKind.Create, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, Array.Empty<string>()));
}
return new(operations);
}
}
@@ -0,0 +1,28 @@
using PRecAnnot.Abstractions;
using PRecAnnot.Core;
namespace PRecAnnot.Core.Tests;
public sealed class ElevationAnnotationPlannerTests
{
[Fact]
public void PlansCreateUpdateAlreadyCurrentAndConflictsDeterministically()
{
var a = new ElevationCandidate("a", 5, 0, new(0, 0), 0, "PXP", "GOSTC");
var b = new ElevationCandidate("b", 10, 0, new(1, 0), 0, "PXP", "GOSTC");
var plan = new ElevationAnnotationPlanner().Build([b, a, a], [new("old-a", "a", "+0,005", new(0, 0)), new("old-b1", "b", "+0,010", new()), new("old-b2", "b", "+0,010", new())]);
Assert.Collection(plan.Operations,
value => Assert.Equal(AnnotationOperationKind.Skip, value.Kind),
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind),
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind));
Assert.Equal("ALREADY_CURRENT", plan.Operations[0].Reasons.Single());
}
[Fact]
public void PlansUpdateWhenTheFormattedElevationChanged()
{
var plan = new ElevationAnnotationPlanner().Build([new ElevationCandidate("a", 13, 0, new(), 0, "PXP", "GOSTC")], [new("old", "a", "+0,010", new())]);
Assert.Equal(AnnotationOperationKind.Update, plan.Operations.Single().Kind);
Assert.Equal("+0,015", plan.Operations.Single().Text);
}
}