diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 64255be..1723fd1 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -52,6 +52,10 @@ public interface ILineNumberAnnotationFormatter LineNumberFormattingResult Format(LineNumberAnnotationDefinition definition, IReadOnlyDictionary fields); } +public sealed record DataLinksPropertyReadResult(bool Success, IReadOnlyDictionary Properties, string? FailureCode = null); +public interface IDataLinksPropertyReader { DataLinksPropertyReadResult ReadProperties(long rowId); } +public sealed record LineNumberFieldResolution(bool Success, IReadOnlyDictionary Values, string? FailureCode = null); + public sealed record GeometrySnapshot { public Point3Dto? WorldPosition { get; init; } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberFieldResolver.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberFieldResolver.cs new file mode 100644 index 0000000..64d59e4 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberFieldResolver.cs @@ -0,0 +1,23 @@ +using PRecAnnot.Abstractions; +namespace PRecAnnot.Core; + +public sealed class LineNumberFieldResolver(IDataLinksPropertyReader reader) +{ + // Source-backed Convert_field_to_value port. The adapter supplies a + // deterministic, read-only flattened key set: Table.Column and Column. + public LineNumberFieldResolution Resolve(long rowId, IEnumerable fields) + { + var read = reader.ReadProperties(rowId); + if (!read.Success) return new(false, new Dictionary(), read.FailureCode ?? "DATALINKS_READ_FAILED"); + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var field in fields.Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(field)) return new(false, values, "LINE_NUMBER_FIELD_EMPTY"); + var dot = field.IndexOf('.', StringComparison.Ordinal); + if (dot <= 0 || dot == field.Length - 1) return new(false, values, "LINE_NUMBER_FIELD_INVALID:" + field); + if (!read.Properties.TryGetValue(field, out var value) && !read.Properties.TryGetValue(field[(dot + 1)..], out value)) return new(false, values, "LINE_NUMBER_COLUMN_MISSING:" + field); + values[field] = value ?? string.Empty; + } + return new(true, values); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantDataLinksPropertyReader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantDataLinksPropertyReader.cs new file mode 100644 index 0000000..e371ee5 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantDataLinksPropertyReader.cs @@ -0,0 +1,20 @@ +using Autodesk.ProcessPower.DataLinks; +using PRecAnnot.Abstractions; +namespace PRecAnnot.Plant3D2026; + +internal sealed class PlantDataLinksPropertyReader(DataLinksManager dataLinks) : IDataLinksPropertyReader +{ + public DataLinksPropertyReadResult ReadProperties(long rowId) + { + if (rowId <= 0) return new(false, new Dictionary(), "DATALINKS_ROW_ID_INVALID"); + try + { + var values = dataLinks.GetAllProperties((int)rowId, true) + .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .ToDictionary(x => x.Key, x => (string?)x.Last().Value, StringComparer.OrdinalIgnoreCase); + return new(true, values); + } + catch (DLException) { return new(false, new Dictionary(), "DATALINKS_PROPERTY_READ_FAILED"); } + catch (Exception) { return new(false, new Dictionary(), "DATALINKS_PROPERTY_READ_FAILED"); } + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberFieldResolverTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberFieldResolverTests.cs new file mode 100644 index 0000000..5d4b1cf --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberFieldResolverTests.cs @@ -0,0 +1,10 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +namespace PRecAnnot.Core.Tests; +public sealed class LineNumberFieldResolverTests +{ + [Fact] public void ResolvesNullAsLegacyEmptyString() => Assert.Equal(string.Empty, new LineNumberFieldResolver(new Reader(true, new Dictionary { ["EngineeringItems.LineNumber"] = null })).Resolve(5, ["EngineeringItems.LineNumber"]).Values["EngineeringItems.LineNumber"]); + [Fact] public void RejectsInvalidField() => Assert.Equal("LINE_NUMBER_FIELD_INVALID:LineNumber", new LineNumberFieldResolver(new Reader(true, new Dictionary())).Resolve(5, ["LineNumber"]).FailureCode); + [Fact] public void PropagatesAdapterFailure() => Assert.Equal("DATALINKS_PROPERTY_READ_FAILED", new LineNumberFieldResolver(new Reader(false, new Dictionary())).Resolve(5, ["EngineeringItems.LineNumber"]).FailureCode); + private sealed record Reader(bool Ok, IReadOnlyDictionary Values) : IDataLinksPropertyReader { public DataLinksPropertyReadResult ReadProperties(long _) => new(Ok, Values, Ok ? null : "DATALINKS_PROPERTY_READ_FAILED"); } +}