diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 7d74950..2b2cc4f 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -60,6 +60,11 @@ public sealed record ExistingLineNumberAnnotation(string StableId, string Source 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 LineNumberApplyPreflight(string StableId, string Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle); +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 Texts, IReadOnlyList Lines); +public sealed record LineNumberListAsset(string Layer, double MaskScaleFactor, double LineHeight, double LineSpacingDistance, LineNumberTextAsset MarkerText, LineNumberTextAsset LineNumberText, LineNumberLineAsset Line); +public sealed record AnnotateLineNumbersAssets(LineNumberMarkerAsset Marker, LineNumberListAsset List); public sealed record GeometrySnapshot { diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotateLineNumbersSettingsLoader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotateLineNumbersSettingsLoader.cs new file mode 100644 index 0000000..04bfe29 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotateLineNumbersSettingsLoader.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using System.Xml.Linq; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class AnnotateLineNumbersSettingsLoader +{ + public AnnotateLineNumbersAssets Load(string xml) + { + var lineNumbers = XElement.Parse(xml).Element("LineNumbers") ?? throw Invalid("LineNumbers"); + var marker = lineNumbers.Element("Marker") ?? throw Invalid("Marker"); + var list = lineNumbers.Element("List") ?? throw Invalid("List"); + var texts = marker.Elements("Text").Select(x => Text(x)).ToArray(); + var lines = marker.Elements("Line").Select(Line).ToArray(); + if (texts.Length == 0 || lines.Length == 0) throw Invalid("MarkerContent"); + return new( + new(Required(marker, "layer"), Number(marker, "maskscalefactor"), texts, lines), + new(Required(list, "layer"), Number(list, "maskscalefactor"), Number(list, "lineheight"), Number(list, "linespacingdistance"), Text(RequiredElement(list, "MarkerText"), false), Text(RequiredElement(list, "LineNumberText"), false), Line(RequiredElement(list, "Line")))); + } + + private static LineNumberTextAsset Text(XElement value, bool readValue = true) => new(Required(value, "layer"), Required(value, "textstyle"), Number(value, "size"), readValue ? Required(value, "textvalue", true) : string.Empty, new(Number(value, "positionx"), Number(value, "positiony"))); + private static LineNumberLineAsset Line(XElement value) => new(Required(value, "layer"), new(Number(value, "startx"), Number(value, "starty")), new(OptionalNumber(value, "endx"), OptionalNumber(value, "endy"))); + private static XElement RequiredElement(XElement parent, string name) => parent.Element(name) ?? throw Invalid(name); + private static string Required(XElement value, string name, bool emptyAllowed = false) + { + var attribute = value.Attribute(name) ?? throw Invalid(name); + if (!emptyAllowed && string.IsNullOrWhiteSpace(attribute.Value)) throw Invalid(name); + return attribute.Value; + } + private static double Number(XElement value, string name) => Parse(Required(value, name)); + private static double OptionalNumber(XElement value, string name) => value.Attribute(name) is { } attribute ? Parse(attribute.Value) : 0; + private static double Parse(string value) => double.TryParse(value.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) ? result : throw Invalid("number"); + private static InvalidDataException Invalid(string field) => new("LINE_NUMBER_SETTINGS_INVALID:" + field); +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AnnotateLineNumbersSettingsLoaderTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AnnotateLineNumbersSettingsLoaderTests.cs new file mode 100644 index 0000000..13eecc0 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/AnnotateLineNumbersSettingsLoaderTests.cs @@ -0,0 +1,8 @@ +using PRecAnnot.Core; +namespace PRecAnnot.Core.Tests; +public sealed class AnnotateLineNumbersSettingsLoaderTests +{ + private const string Xml = "
"; + [Fact] public void LoadsLegacyMarkerAndListContract() { var x = new AnnotateLineNumbersSettingsLoader().Load(Xml); Assert.Equal(1.5, x.Marker.MaskScaleFactor); Assert.Equal("M", x.Marker.Layer); Assert.Equal(6, x.List.LineSpacingDistance); Assert.Equal(0, x.List.Line.End.X); } + [Fact] public void RejectsMissingRequiredLayer() => Assert.Equal("LINE_NUMBER_SETTINGS_INVALID:layer", Assert.Throws(() => new AnnotateLineNumbersSettingsLoader().Load(Xml.Replace(" layer='M'", ""))).Message); +}