feat: port p-rec-annot ortho viewport maintenance foundation

This commit is contained in:
codex
2026-08-07 12:43:08 +03:00
parent ba39a649be
commit 7d8cc570df
8 changed files with 116 additions and 6 deletions
@@ -85,7 +85,7 @@ public sealed record LineConnectionBoundaryCandidate(
double Rotation,
string? FirstLineGroupStableId = null,
string? SecondLineGroupStableId = null);
public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons, string? FirstText = null, string? SecondText = null);
public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons, string? FirstText = null, string? SecondText = null, Point3Dto? ConnectorPosition = null);
public sealed record LineConnectionAssets(string PolylineLayer, string TextLayer, string TextStyle, string BlockName = "BLOCK_LINENUMBER_CONNECTION");
public sealed record LineConnectionApplyResult(int Deleted, int Created);
// The geometry adapter supplies the paper-space connector point and orientation.
@@ -154,6 +154,7 @@ public sealed record ComponentGeometryDefinition(
Bounds3Dto Bounds,
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 interface IPlantComponentGeometryReader
{
ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric);
@@ -15,7 +15,7 @@ public sealed class LineConnectionBoundaryPlanner
{
if (!seen.Add(candidate.StableId))
{
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"]));
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"], ConnectorPosition: candidate.FirstEndpoint));
continue;
}
var first = candidate.FirstLineNumber?.Trim() ?? string.Empty;
@@ -27,21 +27,21 @@ public sealed class LineConnectionBoundaryPlanner
if (!string.IsNullOrWhiteSpace(candidate.FirstLineGroupStableId) &&
string.Equals(candidate.FirstLineGroupStableId, candidate.SecondLineGroupStableId, StringComparison.OrdinalIgnoreCase))
{
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"]));
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint));
continue;
}
if (first.Length == 0 || second.Length == 0)
{
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"]));
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"], ConnectorPosition: candidate.FirstEndpoint));
continue;
}
if (string.Equals(first, second, StringComparison.OrdinalIgnoreCase))
{
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"]));
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint));
continue;
}
var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray();
result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty<string>(), first, second));
result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty<string>(), first, second, candidate.FirstEndpoint));
}
return result;
}
@@ -0,0 +1,13 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class ViewportLayerPlanner
{
public ViewportLayerPlan Build(string? currentLayer, bool targetLayerExists, string targetLayer = "Defpoints")
{
if (string.IsNullOrWhiteSpace(targetLayer)) return new(false, string.Empty, "VIEWPORT_TARGET_LAYER_INVALID");
if (!targetLayerExists) return new(false, targetLayer, "VIEWPORT_TARGET_LAYER_MISSING");
return new(!string.Equals(currentLayer, targetLayer, StringComparison.OrdinalIgnoreCase), targetLayer);
}
}
@@ -47,6 +47,7 @@ public sealed class EntryPoint : IExtensionApplication
[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("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport();
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
@@ -49,6 +49,16 @@ internal sealed class LineConnectionAnnotationWriter
attribute.LayerId = layers[assets.TextLayer]; attribute.TextStyleId = styles[assets.TextStyle];
attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
}
if (plan.ConnectorPosition is { } connector)
{
using var leader = new Polyline(3) { LayerId = layers[assets.PolylineLayer], XData = Ownership(viewportStableId, plan) };
leader.AddVertexAt(0, new Point2d(connector.X, connector.Y), 0, 0, 0);
// Legacy PXP-ALC uses an orthogonal two-segment leader. Its
// elbow shares the annotation Y coordinate for a stable route.
leader.AddVertexAt(1, new Point2d(connector.X, plan.Position.Y), 0, 0, 0);
leader.AddVertexAt(2, new Point2d(plan.Position.X, plan.Position.Y), 0, 0, 0);
paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true);
}
}
transaction.Commit();
return new(deleted, insertable.Length);
@@ -0,0 +1,67 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
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 OrthoMaintenanceCommands
{
public static void CheckViewport()
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null) return;
try
{
var options = new PromptEntityOptions("\nВыберите видовой экран для проверки: ");
options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
var selected = document.Editor.GetEntity(options); if (selected.Status != PromptStatus.OK) return;
OrthoViewportInfo view;
ViewportLayerPlan layerPlan;
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
{
var viewport = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
layerPlan = new ViewportLayerPlanner().Build(viewport.Layer, layers.Has("Defpoints"));
view = Resolve(document, viewport);
}
if (layerPlan.FailureCode is not null) throw new InvalidOperationException(layerPlan.FailureCode);
var objects = new PlantObjectReader().ReadObjects(view, CancellationToken.None);
var resolved = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved);
var outside = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
var unresolved = objects.Count - resolved - outside;
document.Editor.WriteMessage($"\nPXP-COV preview: resolved={resolved}; outside={outside}; unresolved={unresolved}; viewport-layer-change={layerPlan.RequiresChange}. DWG не изменён.");
RuntimeDiagnostics.Write("OrthoViewportCheckCompleted", commandName: "PXP-COV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["Resolved"] = resolved, ["Outside"] = outside, ["Unresolved"] = unresolved, ["LayerChange"] = layerPlan.RequiresChange });
if (!layerPlan.RequiresChange || !string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
ValidateFixture(document);
using var documentLock = document.LockDocument();
using var write = document.Database.TransactionManager.StartTransaction();
var target = (Viewport)write.GetObject(selected.ObjectId, OpenMode.ForWrite);
var layerTable = (LayerTable)write.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
target.LayerId = layerTable[layerPlan.TargetLayer];
write.Commit();
document.Editor.WriteMessage("\nPXP-COV Apply: viewport moved to Defpoints; AutoCAD UNDO is available.");
}
catch (Exception exception) { RuntimeDiagnostics.Write("OrthoViewportCheckFailed", "ERROR", commandName: "PXP-COV", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-COV: " + exception.Message); }
}
private static OrthoViewportInfo Resolve(Document document, Viewport selected)
{
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_PROJECT_UNAVAILABLE");
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
.Where(x => Math.Abs(x.CenterX - selected.CenterPoint.X) < .001 && Math.Abs(x.CenterY - selected.CenterPoint.Y) < .001 && Math.Abs(x.Width - selected.Width) < .001 && Math.Abs(x.Height - selected.Height) < .001).ToArray();
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ORTHO_VIEW_NOT_FOUND" : "ORTHO_VIEW_AMBIGUOUS");
}
private static void ValidateFixture(Document document)
{
var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER");
if (string.IsNullOrWhiteSpace(marker) || !Path.IsPathFullyQualified(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_MAINTENANCE_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
var root = Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar;
if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
}
}
@@ -13,6 +13,7 @@ public sealed class LineConnectionBoundaryPlannerTests
Assert.Equal("a", result[0].StableId);
Assert.Contains("SAME_LINE_GROUP", result[0].Reasons);
Assert.Equal("L-100 / L-200", result[1].Text);
Assert.Equal(new Point3Dto(), result[1].ConnectorPosition);
Assert.Empty(result[1].Reasons);
}
@@ -0,0 +1,17 @@
using PRecAnnot.Core;
namespace PRecAnnot.Core.Tests;
public sealed class ViewportLayerPlannerTests
{
[Theory]
[InlineData("0", true)]
[InlineData("VIEWPORTS", true)]
public void PlansLegacyDefpointsAssignment(string current, bool exists) => Assert.True(new ViewportLayerPlanner().Build(current, exists).RequiresChange);
[Fact]
public void PreservesAlreadyAssignedViewport() => Assert.False(new ViewportLayerPlanner().Build("defpoints", true).RequiresChange);
[Fact]
public void RejectsMissingTargetLayer() => Assert.Equal("VIEWPORT_TARGET_LAYER_MISSING", new ViewportLayerPlanner().Build("0", false).FailureCode);
}