feat: add ortho annotation source navigation

This commit is contained in:
codex
2026-08-07 15:26:37 +03:00
parent 79f6124885
commit 14c244657d
5 changed files with 125 additions and 5 deletions
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.2.4" ProductCode="b455ee6eaed79d76f084846a60ce3284" Author="NBIM">
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.2.5" ProductCode="b455ee6eaed79d76f084846a60ce3284" Author="NBIM">
<Components>
<!-- SeriesMin/SeriesMax and platform token are pending official/runtime verification. -->
<RuntimeRequirements OS="Win64" />
@@ -54,6 +54,7 @@ public sealed class EntryPoint : IExtensionApplication
[CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport();
[CommandMethod("PXP-UOB")] public void UpdateOrthoViewBlocksCommand() => PRecAnnot.Plant3D2026.UpdateOrthoViewBlocksCommand.Run();
[CommandMethod("PXP-RA", CommandFlags.Session)] public void RemoveOrthoAnnotationsCommand() => OrthoAnnotationRemovalCommand.Run();
[CommandMethod("PXP-OSI", CommandFlags.Session)] public void ShowOrthoSourceInModelCommand() => OrthoSourceNavigationCommand.Run();
[CommandMethod("PXP-AC", CommandFlags.UsePickSet)] public void SwapAttributesCommand() => PRecAnnot.Plant3D2026.SwapAttributesCommand.Run();
[CommandMethod("PXP-IOD", CommandFlags.Session)] public void UpdateOrthoDrawingsCommand() => PRecAnnot.Plant3D2026.UpdateOrthoDrawingsCommand.Run();
[CommandMethod("mto-ma")] public void ShowManualAnnotationPaletteCommand() => ManualAnnotationPalette.Show();
@@ -35,13 +35,13 @@ internal sealed class OrthoAnnotationWriter
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);
leader.MText = text; leader.TextLocation = land; leader.XData = Ownership(viewport, source, 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) };
using var reference = new BlockReference(start, definition.ObjectId) { LayerId = layers[assets.Layer], XData = Ownership(viewport, source, classification) };
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
foreach (ObjectId id in definition)
{
@@ -58,6 +58,17 @@ internal sealed class OrthoAnnotationWriter
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 ResultBuffer Ownership(OrthoViewportInfo viewport, PlantObjectSnapshot source, OrthoAnnotationClassification value)
{
source.Properties.TryGetValue("PRecAnnot.RowId", out var rowId);
return new ResultBuffer(
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "VIEWPORT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport.Id),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source.SnapshotId),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE_DRAWING"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport.SourceDrawingName ?? string.Empty),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE_ROW"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, rowId ?? string.Empty),
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); }
}
@@ -0,0 +1,108 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.ProcessPower.DataLinks;
using Autodesk.ProcessPower.PlantInstance;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
// Source-backed replacement for OrthoAutoAnnotate.HighlightAndZoomPnPId.
// New PXP-OA annotations persist their project source drawing and DataLinks row
// in XData. Legacy annotations without that information remain untouched and
// fail safely rather than being migrated destructively.
internal static class OrthoSourceNavigationCommand
{
private const string ApplicationName = "PXP-OA";
public static void Run()
{
var ortho = AcadApplication.DocumentManager.MdiActiveDocument;
if (ortho is null || PlantApplication.CurrentProject is null) return;
var pick = ortho.Editor.GetEntity(new PromptEntityOptions("\nSelect an Ortho annotation to show its model source: ") { AllowNone = false });
if (pick.Status != PromptStatus.OK) return;
if (!TryReadSource(ortho.Database, pick.ObjectId, out var sourceDrawing, out var rowId))
{
ortho.Editor.WriteMessage("\nThe selected annotation has no modern source identity; no drawing was changed.");
return;
}
var target = AcadApplication.DocumentManager.Cast<Document>()
.FirstOrDefault(document => SamePath(document.Name, sourceDrawing));
if (target is null)
{
var answer = ortho.Editor.GetKeywords(new PromptKeywordOptions($"\nOpen model drawing '{Path.GetFileName(sourceDrawing)}'? [Open/Cancel] <Open>: ") { Keywords = { "Open", "Cancel" }, AllowNone = true });
if (answer.Status == PromptStatus.Cancel || string.Equals(answer.StringResult, "Cancel", StringComparison.OrdinalIgnoreCase)) return;
if (!File.Exists(sourceDrawing)) { ortho.Editor.WriteMessage("\nThe source drawing is unavailable in the active project."); return; }
try { target = AcadApplication.DocumentManager.Open(sourceDrawing, false); }
catch (Exception exception) { RuntimeDiagnostics.Write("OrthoSourceNavigation", "ERROR", resultStatus: "SOURCE_DRAWING_OPEN_FAILED", exception: exception); ortho.Editor.WriteMessage("\nUnable to open the source drawing."); return; }
}
AcadApplication.DocumentManager.MdiActiveDocument = target;
var links = PlantApplication.ProjectPart("Piping")?.DataLinksManager;
if (links is null) { target.Editor.WriteMessage("\nPiping DataLinks are unavailable."); return; }
var id = FindEntityForRow(target.Database, links, rowId);
if (id.IsNull) { target.Editor.WriteMessage("\nThe source component no longer exists."); return; }
HighlightAndZoom(target, id);
RuntimeDiagnostics.Write("OrthoSourceNavigation", resultStatus: "SOURCE_OBJECT_HIGHLIGHTED", document: target);
}
private static bool TryReadSource(Database database, ObjectId id, out string drawing, out int rowId)
{
drawing = string.Empty; rowId = 0;
try
{
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) return false;
using var data = entity.GetXDataForApplication(ApplicationName);
var values = data?.AsArray().Where(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(value => value.Value?.ToString() ?? string.Empty).ToArray();
if (values is null) return false;
for (var index = 0; index + 1 < values.Length; index++)
{
if (string.Equals(values[index], "SOURCE_DRAWING", StringComparison.Ordinal)) drawing = values[index + 1];
if (string.Equals(values[index], "SOURCE_ROW", StringComparison.Ordinal)) int.TryParse(values[index + 1], out rowId);
}
return Path.IsPathRooted(drawing) && rowId > 0;
}
catch { return false; }
}
private static ObjectId FindEntityForRow(Database database, DataLinksManager links, int rowId)
{
try
{
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
var model = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.ModelSpace], OpenMode.ForRead);
foreach (ObjectId id in model)
{
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity) continue;
try { if (links.FindAcPpRowId(id) == rowId) return id; }
catch (DLException) { }
}
}
catch { }
return ObjectId.Null;
}
private static void HighlightAndZoom(Document document, ObjectId id)
{
using var transaction = document.Database.TransactionManager.StartTransaction();
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) return;
entity.Highlight();
var extents = entity.GeometricExtents;
using var view = document.Editor.GetCurrentView();
var transform = Matrix3d.PlaneToWorld(view.ViewDirection);
transform = Matrix3d.Displacement(view.Target - Point3d.Origin) * transform;
transform = Matrix3d.Rotation(-view.ViewTwist, view.ViewDirection, view.Target) * transform;
extents.TransformBy(transform.Inverse());
view.Width = Math.Max(extents.MaxPoint.X - extents.MinPoint.X, 1d);
view.Height = Math.Max(extents.MaxPoint.Y - extents.MinPoint.Y, 1d);
view.CenterPoint = new Point2d((extents.MaxPoint.X + extents.MinPoint.X) / 2d, (extents.MaxPoint.Y + extents.MinPoint.Y) / 2d);
document.Editor.SetCurrentView(view);
transaction.Commit();
}
private static bool SamePath(string left, string right) => string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase);
}
@@ -16,6 +16,6 @@ $moduleManifestPath=Join-Path $bundleRoot 'Contents\module-manifest.json'
$files=Get-ChildItem $bundleRoot -Recurse -File | Where-Object { $_.FullName -ne $moduleManifestPath } | Sort-Object FullName | ForEach-Object {
[ordered]@{ relativePath=$_.FullName.Substring($bundleRoot.Length + 1).Replace('\','/'); sha256=(Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant(); required=$true }
}
$manifest=[ordered]@{ schemaVersion=2; package='P_Rec_Annot'; version='0.2.4'; modules=$modules; assemblies=$assemblies; files=$files }
$manifest=[ordered]@{ schemaVersion=2; package='P_Rec_Annot'; version='0.2.5'; modules=$modules; assemblies=$assemblies; files=$files }
$manifest | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $bundleRoot 'Contents\module-manifest.json') -Encoding utf8
Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.'