From b69a858f8e5e94d822e4109252769567c4aba538 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 11:58:13 +0300 Subject: [PATCH] feat: port p-rec-annot elevation subsystem --- .../src/PRecAnnot.Abstractions/Contracts.cs | 2 + .../PRecAnnot.Core/ElevationSettingsLoader.cs | 19 +++ .../LegacyElevationFormatter.cs | 8 +- .../ElevationAnnotationWriter.cs | 104 +++++++++++++++ .../ElevationApplyPermit.cs | 29 +++++ .../ElevationCommands.cs | 122 ++++++++++++++++++ .../ElevationRuntimeConfiguration.cs | 15 +++ .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 2 + .../PRecAnnot.Plant3D2026/PlantReadPath.cs | 2 + .../ElevationAnnotationPlannerTests.cs | 4 +- .../ElevationSettingsLoaderTests.cs | 22 ++++ .../LegacyElevationFormatterTests.cs | 8 +- 12 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ElevationSettingsLoader.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationAnnotationWriter.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationApplyPermit.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationCommands.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationRuntimeConfiguration.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationSettingsLoaderTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index e2a741c..3b8da97 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -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 Reasons); public sealed record ElevationAnnotationPlan(IReadOnlyList Operations); +public sealed record ElevationAssets(string Layer, string TextStyle, string BlockName = "BLOCK_ELEVATION"); +public sealed record ElevationApplyResult(int Created, int Updated, int Unchanged); 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 Members, AnnotationPoint Placement, double Rotation); // A legacy PXP-ALC annotation belongs to a connector between two *different* diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ElevationSettingsLoader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ElevationSettingsLoader.cs new file mode 100644 index 0000000..4a6beeb --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ElevationSettingsLoader.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class ElevationSettingsLoader +{ + public ElevationAssets Load(string xml) + { + var element = XElement.Parse(xml).Element("Elevations") + ?? throw new InvalidDataException("ELEVATION_SETTINGS_INVALID:Elevations"); + return new ElevationAssets(Required(element, "layer"), Required(element, "textstyle")); + } + + private static string Required(XElement element, string name) => + element.Attribute(name) is { Value: var value } && !string.IsNullOrWhiteSpace(value) + ? value + : throw new InvalidDataException("ELEVATION_SETTINGS_INVALID:" + name); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LegacyElevationFormatter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LegacyElevationFormatter.cs index 954772c..a034a77 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LegacyElevationFormatter.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LegacyElevationFormatter.cs @@ -7,10 +7,10 @@ public static class LegacyElevationFormatter public static int RoundModelElevation(double modelZ, double datumOffset) { // InsertElevation.BlockJig divides the model-space ordinate by five, - // rounds that quotient away from zero, then formats the resulting - // integer as a signed metres/millimetres value. It does not multiply - // the rounded quotient back by five. - return checked((int)Math.Round((modelZ + datumOffset) / 5d, MidpointRounding.AwayFromZero)); + // rounds that quotient away from zero and restores the five-unit grid + // before formatting the signed metres/millimetres value. + var grid = Math.Round((modelZ + datumOffset) / 5d, MidpointRounding.AwayFromZero) * 5d; + return checked((int)Math.Round(grid, MidpointRounding.AwayFromZero)); } public static string FormatMillimetres(int millimetres) diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationAnnotationWriter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationAnnotationWriter.cs new file mode 100644 index 0000000..b7691b6 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationAnnotationWriter.cs @@ -0,0 +1,104 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Geometry; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Plant3D2026; + +internal sealed class ElevationAnnotationWriter +{ + private const string ApplicationName = "PXP-AE"; + + public IReadOnlyList ReadExisting(Document document) + { + var result = new List(); + using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction(); + var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead); + foreach (ObjectId id in paper) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue; + var data = reference.GetXDataForApplication(ApplicationName)?.AsArray(); + if (data is null) continue; + var strings = data.Where(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(x => x.Value?.ToString() ?? string.Empty).ToArray(); + var source = Value(strings, "SOURCE"); var text = Value(strings, "ANNOTATIONTEXT"); + if (source is null || text is null) continue; + result.Add(new ExistingElevationAnnotation(reference.Handle.ToString(), source, text, new AnnotationPoint(reference.Position.X, reference.Position.Y, reference.Position.Z))); + } + return result.OrderBy(x => x.SourceStableId, StringComparer.Ordinal).ToArray(); + } + + public ElevationApplyResult Apply(Document document, IReadOnlyList operations, ElevationAssets assets, bool manual = false) + { + 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); + if (!blocks.Has(assets.BlockName)) throw new InvalidOperationException("ELEVATION_BLOCK_MISSING:" + assets.BlockName); + if (!layers.Has(assets.Layer)) throw new InvalidOperationException("ELEVATION_LAYER_MISSING:" + assets.Layer); + if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("ELEVATION_TEXT_STYLE_MISSING:" + assets.TextStyle); + EnsureRegApp(transaction, document.Database); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite); + var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BlockName], OpenMode.ForRead); + var owned = paper.Cast().Select(id => transaction.GetObject(id, OpenMode.ForRead)).OfType() + .Select(reference => (reference, source: Source(reference))).Where(x => x.source is not null) + .GroupBy(x => x.source!, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.ToArray(), StringComparer.OrdinalIgnoreCase); + var created = 0; var updated = 0; var unchanged = 0; + foreach (var operation in operations.Where(x => x.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update)) + { + if (operation.Text is null) continue; + var matches = owned.GetValueOrDefault(operation.StableId, []); + BlockReference reference; + if (matches.Length == 0) + { + reference = new BlockReference(ToPoint(operation.Position), definition.ObjectId) { LayerId = layers[assets.Layer], Rotation = operation.Rotation }; + paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true); created++; + AddAttributes(transaction, definition, reference, operation.Text, styles[assets.TextStyle]); + } + else + { + reference = matches[0].reference; reference.UpgradeOpen(); + reference.Position = ToPoint(operation.Position); reference.Rotation = operation.Rotation; reference.LayerId = layers[assets.Layer]; updated++; + foreach (ObjectId attributeId in reference.AttributeCollection) + { + var attribute = (AttributeReference)transaction.GetObject(attributeId, OpenMode.ForWrite); + attribute.TextString = operation.Text + (attribute.Tag.Contains('*') ? "*" : string.Empty); attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute(); + } + } + reference.XData = Ownership(operation.StableId, operation.Text, manual); + ApplyLegacyDynamicProperties(reference); + } + unchanged += operations.Count(x => x.Kind == AnnotationOperationKind.Skip && x.Reasons.Contains("ALREADY_CURRENT")); + transaction.Commit(); + return new(created, updated, unchanged); + } + + private static void AddAttributes(Transaction transaction, BlockTableRecord definition, BlockReference reference, string text, ObjectId style) + { + foreach (ObjectId id in definition) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue; + using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(source, reference.BlockTransform); + attribute.TextString = text + (attribute.Tag.Contains('*') ? "*" : string.Empty); attribute.TextStyleId = style; + attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); + } + } + + private static void ApplyLegacyDynamicProperties(BlockReference reference) + { + if (!reference.IsDynamicBlock) return; + foreach (DynamicBlockReferenceProperty property in reference.DynamicBlockReferencePropertyCollection) + if (string.Equals(property.PropertyName, "V1", StringComparison.OrdinalIgnoreCase) && !property.ReadOnly) property.Value = "От объекта"; + } + + private static string? Source(BlockReference reference) + { + var strings = reference.GetXDataForApplication(ApplicationName)?.AsArray().Where(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(x => x.Value?.ToString() ?? string.Empty).ToArray(); + return strings is null ? null : Value(strings, "SOURCE"); + } + private static string? Value(string[] values, string key) { var index = Array.FindIndex(values, x => string.Equals(x, key, StringComparison.Ordinal)); return index >= 0 && index + 1 < values.Length ? values[index + 1] : null; } + private static Point3d ToPoint(AnnotationPoint point) => new(point.X, point.Y, point.Z); + private static ResultBuffer Ownership(string source, string text, bool manual) => new(new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "ANNOTATIONTEXT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, text), new TypedValue((int)DxfCode.ExtendedDataAsciiString, manual ? "MANUAL" : "AUTO")); + 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/src/PRecAnnot.Plant3D2026/ElevationApplyPermit.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationApplyPermit.cs new file mode 100644 index 0000000..c92e58b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationApplyPermit.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using Autodesk.AutoCAD.ApplicationServices; + +namespace PRecAnnot.Plant3D2026; + +internal sealed record ElevationApplyPermit +{ + public required bool AllowApply { get; init; } + public required string FixtureMarkerPath { get; init; } +} + +internal static class ElevationApplyPermitReader +{ + private const string Marker = "P_REC_ANNOT_TEST_FIXTURE=ELEVATION_APPLY"; + + public static ElevationApplyPermit ReadAndValidate(Document document) + { + var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_ELEVATION_APPLY_PERMIT_PATH"); + if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path)) + throw new InvalidOperationException("ELEVATION_APPLY_PERMIT_REQUIRED"); + var permit = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? throw new InvalidOperationException("ELEVATION_APPLY_PERMIT_INVALID"); + if (!permit.AllowApply || !Path.IsPathFullyQualified(permit.FixtureMarkerPath) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), Marker, StringComparison.Ordinal)) + throw new InvalidOperationException("ELEVATION_APPLY_TEST_FIXTURE_REQUIRED"); + var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar; + if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ELEVATION_APPLY_TEST_FIXTURE_REQUIRED"); + return permit; + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationCommands.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationCommands.cs new file mode 100644 index 0000000..5c1cd1a --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationCommands.cs @@ -0,0 +1,122 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.ProcessPower.PlantInstance; +using Autodesk.ProcessPower.P3dProjectParts; +using PRecAnnot.Abstractions; +using PRecAnnot.Core; +using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application; + +namespace PRecAnnot.Plant3D2026; + +internal static class ElevationCommands +{ + public static void RunAutomatic() => Run("PXP-AE", manual: false); + public static void RunManual() => Run("PXP-IE", manual: true); + + private static void Run(string command, bool manual) + { + var document = AcadApplication.DocumentManager.MdiActiveDocument; + if (document is null) return; + try + { + var assets = ElevationRuntimeConfiguration.Read(); + var datum = PromptDatum(document.Editor); + if (datum is null) return; + if (manual) RunManual(document, assets, datum.Value); + else RunAutomatic(document, assets, datum.Value); + } + catch (Exception exception) + { + RuntimeDiagnostics.Write("ElevationCommandFailed", "ERROR", commandName: command, resultStatus: exception.Message, exception: exception, document: document); + document.Editor.WriteMessage($"\n{command}: {exception.Message}"); + } + } + + private static void RunAutomatic(Document document, ElevationAssets assets, double datum) + { + var selected = SelectViewport(document.Editor); + if (selected.Status != PromptStatus.OK) return; + OrthoViewportInfo viewport; + using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction()) + viewport = ResolveViewport(document, (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead)); + var candidates = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None) + .Where(value => value.MembershipStatus == ViewMembershipStatus.Resolved && value.Geometry.PaperPosition is not null && value.Geometry.Elevation.HasValue) + .Select(value => new ElevationCandidate(value.SnapshotId, value.Geometry.Elevation!.Value, datum, + new AnnotationPoint(value.Geometry.PaperPosition!.Value.X, value.Geometry.PaperPosition.Value.Y), value.Geometry.Rotation ?? 0, assets.Layer, assets.TextStyle)) + .ToArray(); + var writer = new ElevationAnnotationWriter(); + var plan = new ElevationAnnotationPlanner().Build(candidates, writer.ReadExisting(document)); + WritePreview(document, "PXP-AE", plan); + if (!IsApply()) return; + ElevationApplyPermitReader.ReadAndValidate(document); + var result = writer.Apply(document, plan.Operations, assets); + WriteApply(document, "PXP-AE", result); + } + + private static void RunManual(Document document, ElevationAssets assets, double datum) + { + while (true) + { + var point = document.Editor.GetPoint("\nУкажите положение высотной отметки: "); + if (point.Status != PromptStatus.OK) return; + Viewport? viewport = null; + Point3d modelPoint = default; + using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction()) + { + var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead); + viewport = paper.Cast().Select(id => transaction.GetObject(id, OpenMode.ForRead)).OfType() + .Where(value => value.Number > 1 && !value.PerspectiveOn && Contains(value, point.Value)) + .OrderBy(value => value.Number).FirstOrDefault(); + if (viewport is not null) modelPoint = point.Value.TransformBy(ModelToPaper(viewport).Inverse()); + } + if (viewport is null) { document.Editor.WriteMessage("\nТочка должна находиться внутри ортографического видового экрана."); continue; } + var stableId = $"manual:{viewport.Handle}:{point.Value.X:R}:{point.Value.Y:R}"; + var candidate = new ElevationCandidate(stableId, modelPoint.Z, datum, new AnnotationPoint(point.Value.X, point.Value.Y), 0, assets.Layer, assets.TextStyle); + var plan = new ElevationAnnotationPlanner().Build([candidate], Array.Empty()); + WritePreview(document, "PXP-IE", plan); + if (!IsApply()) return; + ElevationApplyPermitReader.ReadAndValidate(document); + var result = new ElevationAnnotationWriter().Apply(document, plan.Operations, assets, manual: true); + WriteApply(document, "PXP-IE", result); + // Legacy PXP-IE repeats until Escape; every committed iteration is + // a native AutoCAD transaction and therefore participates in UNDO. + } + } + + private static double? PromptDatum(Editor editor) + { + var options = new PromptDoubleOptions("\nСмещение нуля МСК (в мм) <0>: ") { AllowNone = true, DefaultValue = 0, UseDefaultValue = true }; + var result = editor.GetDouble(options); + return result.Status == PromptStatus.OK ? result.Value : result.Status == PromptStatus.None ? 0 : null; + } + + private static PromptEntityResult SelectViewport(Editor editor) + { + var options = new PromptEntityOptions("\nВыберите видовой экран: "); options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true); + return editor.GetEntity(options); + } + + private static OrthoViewportInfo ResolveViewport(Document document, Viewport selected) + { + var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ELEVATION_ORTHO_PROJECT_UNAVAILABLE"); + var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None) + .Where(value => Math.Abs(value.CenterX - selected.CenterPoint.X) < 0.001 && Math.Abs(value.CenterY - selected.CenterPoint.Y) < 0.001 && Math.Abs(value.Width - selected.Width) < 0.001 && Math.Abs(value.Height - selected.Height) < 0.001).ToArray(); + return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ELEVATION_ORTHO_VIEW_NOT_FOUND" : "ELEVATION_ORTHO_VIEW_AMBIGUOUS"); + } + + private static Matrix3d ModelToPaper(Viewport viewport) + { + var center = new Point3d(viewport.ViewCenter.X, viewport.ViewCenter.Y, 0); + return Matrix3d.Displacement(new Vector3d(viewport.CenterPoint.X - center.X, viewport.CenterPoint.Y - center.Y, 0)) + * Matrix3d.Scaling(viewport.CustomScale, center) + * Matrix3d.Rotation(viewport.TwistAngle, Vector3d.ZAxis, Point3d.Origin) + * Matrix3d.WorldToPlane(new Plane(viewport.ViewTarget, viewport.ViewDirection)); + } + private static bool Contains(Viewport viewport, Point3d point) => viewport.GeometricExtents.MinPoint.X <= point.X && viewport.GeometricExtents.MaxPoint.X >= point.X && viewport.GeometricExtents.MinPoint.Y <= point.Y && viewport.GeometricExtents.MaxPoint.Y >= point.Y; + private static bool IsApply() => string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase); + private static void WritePreview(Document document, string command, ElevationAnnotationPlan plan) { document.Editor.WriteMessage($"\n{command} preview: create={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Create)}; update={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Update)}; skip={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Skip)}; conflict={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Conflict)}. DWG не изменён."); RuntimeDiagnostics.Write("ElevationPreviewCompleted", commandName: command, resultStatus: "PreviewCompleted", document: document, details: new Dictionary { ["OperationCount"] = plan.Operations.Count }); } + private static void WriteApply(Document document, string command, ElevationApplyResult result) { document.Editor.WriteMessage($"\n{command} Apply: created={result.Created}; updated={result.Updated}; already-current={result.Unchanged}."); RuntimeDiagnostics.Write("ElevationApplyCompleted", commandName: command, resultStatus: "Applied", document: document, details: new Dictionary { ["Created"] = result.Created, ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged }); } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationRuntimeConfiguration.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationRuntimeConfiguration.cs new file mode 100644 index 0000000..c5d8c82 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ElevationRuntimeConfiguration.cs @@ -0,0 +1,15 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Core; + +namespace PRecAnnot.Plant3D2026; + +internal static class ElevationRuntimeConfiguration +{ + public static ElevationAssets Read() + { + var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH"); + if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path)) + throw new InvalidOperationException("ELEVATION_CONFIGURATION_REQUIRED:P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH"); + return new ElevationSettingsLoader().Load(File.ReadAllText(path)); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 2c2b10f..07d7d93 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -45,6 +45,8 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText(); [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition(); [CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run(); + [CommandMethod("PXP-AE")] public void AutoElevationCommand() => ElevationCommands.RunAutomatic(); + [CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual(); [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs index ebef83e..82b60aa 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs @@ -511,7 +511,9 @@ internal sealed class PlantViewportMembershipReader var direction = modelDirection?.TransformBy(sourceId.Xform); geometry = new GeometrySnapshot { + WorldPosition = new Point3Dto(modelPoint.X, modelPoint.Y, modelPoint.Z), PaperPosition = new Point2Dto(point.X, point.Y), + Elevation = modelPoint.Z, Rotation = 0, PaperDirection = direction is null ? null : new Vector2Dto(direction.Value.X, direction.Value.Y) }; diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationAnnotationPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationAnnotationPlannerTests.cs index 1c2a092..ce80fce 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationAnnotationPlannerTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationAnnotationPlannerTests.cs @@ -10,7 +10,7 @@ public sealed class ElevationAnnotationPlannerTests { 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,001", new(0, 0)), new("old-b1", "b", "+0,002", new()), new("old-b2", "b", "+0,002", new())]); + 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), @@ -23,6 +23,6 @@ public sealed class ElevationAnnotationPlannerTests { 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,003", plan.Operations.Single().Text); + Assert.Equal("+0,015", plan.Operations.Single().Text); } } diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationSettingsLoaderTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationSettingsLoaderTests.cs new file mode 100644 index 0000000..621f5d4 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ElevationSettingsLoaderTests.cs @@ -0,0 +1,22 @@ +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class ElevationSettingsLoaderTests +{ + [Fact] + public void ReadsLegacyElevationAssets() + { + var result = new ElevationSettingsLoader().Load("
"); + Assert.Equal("ELEV", result.Layer); + Assert.Equal("GOST", result.TextStyle); + Assert.Equal("BLOCK_ELEVATION", result.BlockName); + } + + [Theory] + [InlineData("
")] + [InlineData("
")] + [InlineData("
")] + public void RejectsIncompleteLegacyAssets(string xml) => + Assert.Throws(() => new ElevationSettingsLoader().Load(xml)); +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LegacyElevationFormatterTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LegacyElevationFormatterTests.cs index 5cb057b..65be66c 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LegacyElevationFormatterTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LegacyElevationFormatterTests.cs @@ -7,10 +7,10 @@ public sealed class LegacyElevationFormatterTests [Theory] [InlineData(0, 0, "0,000")] [InlineData(2.49, 0, "0,000")] - [InlineData(2.5, 0, "+0,001")] - [InlineData(-2.5, 0, "-0,001")] - [InlineData(1233, 2, "+0,247")] - [InlineData(-1233, -2, "-0,247")] + [InlineData(2.5, 0, "+0,005")] + [InlineData(-2.5, 0, "-0,005")] + [InlineData(1233, 2, "+1,235")] + [InlineData(-1233, -2, "-1,235")] public void PreservesFiveMillimetreAwayFromZeroLegacyRule(double modelZ, double datum, string expected) => Assert.Equal(expected, LegacyElevationFormatter.Calculate(modelZ, datum));