From 548c73cb276842ce2bcc7894cd06daf5a9634214 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 12:51:54 +0300 Subject: [PATCH] feat: expand p-rec-annot ortho annotation workflows --- .../src/PRecAnnot.Abstractions/Contracts.cs | 4 ++ .../OrthoAnnotationClassifier.cs | 44 +++++++++++++ .../OrthoAnnotationSettingsLoader.cs | 15 +++++ .../AutoAnnotateViewCommand.cs | 21 ++++++- .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 4 ++ .../OrthoAnnotationWriter.cs | 63 +++++++++++++++++++ .../OrthoAnnotationClassifierTests.cs | 23 +++++++ .../OrthoAnnotationSettingsLoaderTests.cs | 7 +++ 8 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationClassifier.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationSettingsLoader.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoAnnotationWriter.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationClassifierTests.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationSettingsLoaderTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 1c11eba..839c2f1 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -155,6 +155,10 @@ public sealed record ComponentGeometryDefinition( bool ImperialDimensions); public sealed record ComponentGeometryReadResult(bool Success, ComponentGeometryDefinition? Geometry, string? FailureCode = null); public sealed record ViewportLayerPlan(bool RequiresChange, string TargetLayer, string? FailureCode = null); +public enum OrthoAnnotationRepresentation { None, MultiLeader, BubbleBlock } +public sealed record OrthoAnnotationOptions(bool Supports, bool Valves, bool Instruments, bool Plugs, bool Boards); +public sealed record OrthoAnnotationClassification(string StableId, OrthoAnnotationRepresentation Representation, string Text, IReadOnlyList Reasons); +public sealed record OrthoAnnotationAssets(string Layer, string TextStyle, string MultiLeaderStyle, string BubbleBlock = "BLOCK_ANNO_POSITION"); public interface IPlantComponentGeometryReader { ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationClassifier.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationClassifier.cs new file mode 100644 index 0000000..546ebb3 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationClassifier.cs @@ -0,0 +1,44 @@ +using System.Text.RegularExpressions; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class OrthoAnnotationClassifier +{ + public OrthoAnnotationClassification Classify(PlantObjectSnapshot source, OrthoAnnotationOptions options) + { + string Property(params string[] names) => source.Properties.FirstOrDefault(x => names.Any(n => string.Equals(n, x.Key, StringComparison.OrdinalIgnoreCase))).Value?.Trim() ?? string.Empty; + var className = source.ObjectClass.Trim(); + if (className.Equals("Support", StringComparison.OrdinalIgnoreCase)) + { + var title = Property("Title"); var text = Normalize(Property("Annotation")); + return options.Supports && title.StartsWith("НЕСТАНДАРТНЫЕ", StringComparison.OrdinalIgnoreCase) && text.Length > 0 + ? Ready(source, OrthoAnnotationRepresentation.MultiLeader, text) + : Skip(source, "SUPPORT_NOT_NKR_OR_DISABLED"); + } + if (className.Equals("Valve", StringComparison.OrdinalIgnoreCase) || className.Equals("InlineInstrument", StringComparison.OrdinalIgnoreCase)) + { + var value = Normalize(className.Equals("Valve", StringComparison.OrdinalIgnoreCase) ? Property("Annotation") : Property("Tag")); + if (value.Contains('$')) return options.Instruments ? Ready(source, OrthoAnnotationRepresentation.BubbleBlock, value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "INSTRUMENT_ANNOTATION_DISABLED"); + return HasAlphaNumeric(value) && options.Valves ? Ready(source, OrthoAnnotationRepresentation.MultiLeader, value) : Skip(source, "VALVE_ANNOTATION_EMPTY_OR_DISABLED"); + } + if (className.Equals("Plug", StringComparison.OrdinalIgnoreCase)) + { + var value = Normalize(Property("TieInNumber")); + if (!options.Plugs || !HasAlphaNumeric(value)) return Skip(source, "PLUG_ANNOTATION_EMPTY_OR_DISABLED"); + var representation = value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader; + return Ready(source, representation, "Для подключения\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal)); + } + if (className.Equals("Board", StringComparison.OrdinalIgnoreCase)) + { + var value = Normalize(Property("Tag", "Annotation")); + return options.Boards && value.Length > 0 ? Ready(source, value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader, "Шкаф КИП для прибора\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "BOARD_ANNOTATION_EMPTY_OR_DISABLED"); + } + return Skip(source, "ORTHO_ANNOTATION_CLASS_UNSUPPORTED"); + } + + private static bool HasAlphaNumeric(string value) => Regex.IsMatch(value, "[a-zA-Z0-9]"); + private static string Normalize(string value) => value.Replace("{nl}", "\\P", StringComparison.Ordinal); + private static OrthoAnnotationClassification Ready(PlantObjectSnapshot source, OrthoAnnotationRepresentation representation, string text) => new(source.SnapshotId, representation, text, Array.Empty()); + private static OrthoAnnotationClassification Skip(PlantObjectSnapshot source, string reason) => new(source.SnapshotId, OrthoAnnotationRepresentation.None, string.Empty, [reason]); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationSettingsLoader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationSettingsLoader.cs new file mode 100644 index 0000000..09c0f9e --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/OrthoAnnotationSettingsLoader.cs @@ -0,0 +1,15 @@ +using System.Xml.Linq; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class OrthoAnnotationSettingsLoader +{ + public OrthoAnnotationAssets Load(string xml) + { + var value = XElement.Parse(xml).Element("Annotations") ?? throw Invalid("Annotations"); + return new(Required(value, "layer"), Required(value, "textstyle"), Required(value, "multileaderstyle")); + } + private static string Required(XElement value, string name) => value.Attribute(name) is { Value: var result } && !string.IsNullOrWhiteSpace(result) ? result : throw Invalid(name); + private static InvalidDataException Invalid(string value) => new("ORTHO_ANNOTATION_SETTINGS_INVALID:" + value); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/AutoAnnotateViewCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/AutoAnnotateViewCommand.cs index 5fdc9f6..a58440e 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/AutoAnnotateViewCommand.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/AutoAnnotateViewCommand.cs @@ -27,6 +27,8 @@ internal static class AutoAnnotateViewCommand viewport = matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "AUTO_ANNOTATE_VIEW_NOT_FOUND" : "AUTO_ANNOTATE_VIEW_AMBIGUOUS"); } var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None); + var classifications = objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal) + .Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray(); var permit = string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase) ? FirstLimitedApplyPermitReader.ReadFor(document) : null; var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest @@ -35,9 +37,24 @@ internal static class AutoAnnotateViewCommand LimitedApplyTargetSnapshotId = permit?.TargetSnapshotId, LimitedApplyRuleId = permit is null ? null : FirstLimitedApplyPlanner.RuleId }); - document.Editor.WriteMessage($"\nPXP-AAV preview: examined={plan.Summary.ObjectsExamined}; create={plan.Summary.CreateCount}; update={plan.Summary.UpdateCount}; skip={plan.Summary.SkipCount}; conflicts={plan.Summary.ConflictCount}; PlanId={plan.PlanId}. DWG не изменён."); - RuntimeDiagnostics.Write("AutoAnnotateViewPreviewCompleted", commandName: "PXP-AAV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary { ["PlanId"] = plan.PlanId, ["ObjectsExamined"] = plan.Summary.ObjectsExamined }); + var leaders = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader); + var bubbles = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock); + document.Editor.WriteMessage($"\nPXP-AAV preview: examined={plan.Summary.ObjectsExamined}; create={plan.Summary.CreateCount}; update={plan.Summary.UpdateCount}; leaders={leaders}; bubbles={bubbles}; skip={plan.Summary.SkipCount}; conflicts={plan.Summary.ConflictCount}; PlanId={plan.PlanId}. DWG не изменён."); + RuntimeDiagnostics.Write("AutoAnnotateViewPreviewCompleted", commandName: "PXP-AAV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary { ["PlanId"] = plan.PlanId, ["ObjectsExamined"] = plan.Summary.ObjectsExamined, ["MultiLeaderCandidates"] = leaders, ["BubbleCandidates"] = bubbles }); if (permit is null) return; + if (!string.Equals(permit.ExpectedPlanId, plan.PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("APPLY_PLAN_ID_MISMATCH"); + var selectedSource = objects.SingleOrDefault(x => string.Equals(x.SnapshotId, permit.TargetSnapshotId, StringComparison.Ordinal)) ?? throw new InvalidOperationException("APPLY_TARGET_NOT_FOUND"); + var selectedClassification = classifications.Single(x => string.Equals(x.StableId, permit.TargetSnapshotId, StringComparison.Ordinal)); + if (selectedClassification.Representation != OrthoAnnotationRepresentation.None) + { + var settingsPath = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH"); + if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath)) throw new InvalidOperationException("ORTHO_ANNOTATION_CONFIGURATION_REQUIRED"); + var assets = new OrthoAnnotationSettingsLoader().Load(File.ReadAllText(settingsPath)); + var result = new OrthoAnnotationWriter().Replace(document, viewport, selectedSource, selectedClassification, assets); + document.Editor.WriteMessage("\nPXP-AAV Apply: " + result + "; AutoCAD UNDO is available."); + RuntimeDiagnostics.Write("AutoAnnotateViewApplyCompleted", commandName: "PXP-AAV", resultStatus: result, document: document, details: new Dictionary { ["PlanId"] = plan.PlanId, ["Representation"] = selectedClassification.Representation.ToString() }); + return; + } var corePermit = new FirstLimitedApplyPermit { FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker, AllowApply = permit.AllowApply, TargetSnapshotId = permit.TargetSnapshotId, ExpectedPlanId = permit.ExpectedPlanId }; var preflight = new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, plan, viewport, objects, corePermit); var outcome = new FirstLimitedPipeAnnotationWriter().Write(document, preflight.Operation, permit.VisibilityState, permit.AnnotationTemplateDwgPath); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 0696927..88a9a34 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -228,6 +228,8 @@ public sealed class EntryPoint : IExtensionApplication var membershipPartial = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.PartiallyResolved); var membershipUnresolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.NotResolved); var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView); + var orthoClassifications = objectsByViewport.SelectMany(x => x).OrderBy(x => x.SnapshotId, StringComparer.Ordinal) + .Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray(); var planId = plans.Count == 1 ? plans[0].PlanId : null; details["ObjectsExamined"] = summary.ObjectsExamined; details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion; @@ -236,6 +238,8 @@ public sealed class EntryPoint : IExtensionApplication details["MembershipPartial"] = membershipPartial; details["MembershipUnresolved"] = membershipUnresolved; details["MembershipOutsideView"] = membershipOutside; + details["MultiLeaderCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader); + details["BubbleCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock); details["PlanCreateCount"] = summary.CreateCount; details["PlanUpdateCount"] = summary.UpdateCount; details["PlanSkipCount"] = summary.SkipCount; diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoAnnotationWriter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoAnnotationWriter.cs new file mode 100644 index 0000000..6354eb4 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/OrthoAnnotationWriter.cs @@ -0,0 +1,63 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Geometry; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Plant3D2026; + +internal sealed class OrthoAnnotationWriter +{ + private const string ApplicationName = "PXP-OA"; + public string Replace(Document document, OrthoViewportInfo viewport, PlantObjectSnapshot source, OrthoAnnotationClassification classification, OrthoAnnotationAssets assets) + { + if (source.Geometry.PaperPosition is not { } point || classification.Representation == OrthoAnnotationRepresentation.None) throw new InvalidOperationException("ORTHO_ANNOTATION_OPERATION_INVALID"); + using var documentLock = document.LockDocument(); using var transaction = document.Database.TransactionManager.StartTransaction(); + var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead); + var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead); + var styles = (TextStyleTable)transaction.GetObject(document.Database.TextStyleTableId, OpenMode.ForRead); + var leaders = (DBDictionary)transaction.GetObject(document.Database.MLeaderStyleDictionaryId, OpenMode.ForRead); + if (!layers.Has(assets.Layer)) throw new InvalidOperationException("ORTHO_ANNOTATION_LAYER_MISSING:" + assets.Layer); + if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_TEXT_STYLE_MISSING:" + assets.TextStyle); + if (!leaders.Contains(assets.MultiLeaderStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_MLEADER_STYLE_MISSING:" + assets.MultiLeaderStyle); + if (classification.Representation == OrthoAnnotationRepresentation.BubbleBlock && !blocks.Has(assets.BubbleBlock)) throw new InvalidOperationException("ORTHO_ANNOTATION_BUBBLE_BLOCK_MISSING:" + assets.BubbleBlock); + EnsureRegApp(transaction, document.Database); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite); + var deleted = 0; + foreach (ObjectId id in paper) + { + var entity = transaction.GetObject(id, OpenMode.ForRead) as Entity; if (entity is null || !OwnedBy(entity, source.SnapshotId)) continue; + entity.UpgradeOpen(); entity.Erase(); deleted++; + } + var start = new Point3d(point.X, point.Y, 0); var right = point.X >= viewport.CenterX; + var land = new Point3d(viewport.CenterX + (right ? viewport.Width / 2d + 5d : -viewport.Width / 2d - 5d), point.Y, 0); + if (classification.Representation == OrthoAnnotationRepresentation.MultiLeader) + { + using var leader = new MLeader { LayerId = layers[assets.Layer], MLeaderStyle = leaders.GetAt(assets.MultiLeaderStyle), ContentType = ContentType.MTextContent, LeaderLineType = LeaderType.StraightLeader }; + var leaderId = leader.AddLeader(); var lineId = leader.AddLeaderLine(leaderId); leader.AddFirstVertex(lineId, start); leader.AddLastVertex(lineId, land); + using var text = new MText { Contents = classification.Text, TextStyleId = styles[assets.TextStyle], Location = land, BackgroundFill = true, UseBackgroundColor = true, BackgroundScaleFactor = 1.1 }; + leader.MText = text; leader.TextLocation = land; leader.XData = Ownership(viewport.Id, source.SnapshotId, classification); + paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true); + } + else + { + var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BubbleBlock], OpenMode.ForRead); + using var reference = new BlockReference(start, definition.ObjectId) { LayerId = layers[assets.Layer], XData = Ownership(viewport.Id, source.SnapshotId, classification) }; + paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true); + foreach (ObjectId id in definition) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition definitionAttribute || definitionAttribute.Constant) continue; + using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(definitionAttribute, reference.BlockTransform); attribute.TextString = classification.Text; attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); + } + if (reference.IsDynamicBlock) foreach (DynamicBlockReferenceProperty property in reference.DynamicBlockReferencePropertyCollection) + { + if (property.ReadOnly) continue; + if (string.Equals(property.PropertyName, "P1 X", StringComparison.OrdinalIgnoreCase)) property.Value = land.X - start.X; + else if (string.Equals(property.PropertyName, "P1 Y", StringComparison.OrdinalIgnoreCase)) property.Value = land.Y - start.Y; + } + } + transaction.Commit(); return deleted == 0 ? "Created" : "Updated"; + } + private static bool OwnedBy(Entity entity, string source) => entity.GetXDataForApplication(ApplicationName)?.AsArray().Any(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(x.Value?.ToString(), source, StringComparison.Ordinal)) == true; + private static ResultBuffer Ownership(string viewport, string source, OrthoAnnotationClassification value) => new(new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "VIEWPORT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "ANNOTATIONTEXT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Text), new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Representation.ToString())); + private static void EnsureRegApp(Transaction transaction, Database database) { var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead); if (table.Has(ApplicationName)) return; table.UpgradeOpen(); using var record = new RegAppTableRecord { Name = ApplicationName }; table.Add(record); transaction.AddNewlyCreatedDBObject(record, true); } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationClassifierTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationClassifierTests.cs new file mode 100644 index 0000000..a89acf2 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationClassifierTests.cs @@ -0,0 +1,23 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class OrthoAnnotationClassifierTests +{ + private static readonly OrthoAnnotationOptions All = new(true, true, true, true, true); + [Theory] + [InlineData("Valve", "Annotation", "V-1", OrthoAnnotationRepresentation.MultiLeader, "V-1")] + [InlineData("Valve", "Annotation", "PIC$101", OrthoAnnotationRepresentation.BubbleBlock, "PIC\\P101")] + [InlineData("InlineInstrument", "Tag", "TI$12", OrthoAnnotationRepresentation.BubbleBlock, "TI\\P12")] + [InlineData("Plug", "TieInNumber", "T-4", OrthoAnnotationRepresentation.MultiLeader, "Для подключения\\Pпоз. T-4")] + public void PreservesLegacyClassAndDelimiterRules(string type, string field, string value, OrthoAnnotationRepresentation representation, string text) + { + var result = new OrthoAnnotationClassifier().Classify(Source(type, new Dictionary { [field] = value }), All); + Assert.Equal(representation, result.Representation); Assert.Equal(text, result.Text); + } + + [Fact] + public void RequiresNonstandardSupportTitle() => Assert.Equal(OrthoAnnotationRepresentation.None, new OrthoAnnotationClassifier().Classify(Source("Support", new Dictionary { ["Title"] = "STANDARD", ["Annotation"] = "S1" }), All).Representation); + private static PlantObjectSnapshot Source(string type, IReadOnlyDictionary properties) => new() { SnapshotId = "x", ObjectClass = type, Geometry = new(), Properties = properties }; +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationSettingsLoaderTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationSettingsLoaderTests.cs new file mode 100644 index 0000000..e63b60a --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/OrthoAnnotationSettingsLoaderTests.cs @@ -0,0 +1,7 @@ +using PRecAnnot.Core; +namespace PRecAnnot.Core.Tests; +public sealed class OrthoAnnotationSettingsLoaderTests +{ + [Fact] public void ReadsLegacyAnnotationAssets() { var value = new OrthoAnnotationSettingsLoader().Load("
"); Assert.Equal("A", value.Layer); Assert.Equal("M", value.MultiLeaderStyle); } + [Fact] public void RejectsMissingStyle() => Assert.Throws(() => new OrthoAnnotationSettingsLoader().Load("
")); +}