feat: read Plant 3D Ortho context and viewports

This commit is contained in:
codex
2026-07-31 13:08:16 +03:00
parent 142b5f8b93
commit 837ac1d994
8 changed files with 556 additions and 99 deletions
@@ -1,9 +1,9 @@
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.Windows;
using PRecAnnot.Abstractions;
using PRecAnnot.Core;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
@@ -39,20 +39,24 @@ public sealed class EntryPoint : IExtensionApplication
var stopwatch = Stopwatch.StartNew();
Document? document = null;
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
try
{
document = AcadApplication.DocumentManager.MdiActiveDocument;
var project = PlantApplication.CurrentProject;
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
var context = new PlantRuntimeContextReader().ReadCurrentContext();
var details = new Dictionary<string, object?>
{
["IsPlantProjectOpen"] = project is not null,
["IsOrthoDrawing"] = false,
["ViewportCount"] = null,
["DataLinksAvailable"] = DataLinksAvailable(project)
["ExecutionMode"] = mode.ToString(),
["IsPlantProjectDetected"] = context.HasPlantProject,
["IsPlantProjectOpen"] = context.HasPlantProject,
["IsOrthoDrawing"] = context.IsOrthoDrawing,
["ViewportCount"] = context.Viewports.Count,
["DataLinksAvailable"] = DataLinksAvailable(context.HasPlantProject)
};
if (document is null || project is null)
if (document is null || !context.HasPlantProject)
{
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
@@ -61,47 +65,75 @@ public sealed class EntryPoint : IExtensionApplication
return;
}
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details);
if (SmokeGuard.IsSmoke) SmokeGuard.EnsureReadOnly();
WriteEditor(document, $"\nP_Rec_Annot {commandName}: Plant 3D-проект '{project.Name}' найден; Smoke/diagnostic-only, DWG не изменён.");
WriteUsage(operationId, commandName, "PreflightAccepted", document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details);
}
catch (SmokeWriteBlockedException exception)
{
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "SMOKE_WRITE_BLOCKED", exception, document);
WriteUsage(operationId, commandName, "SMOKE_WRITE_BLOCKED", document, null, stopwatch.ElapsedMilliseconds, exception);
WriteEditor(document, "\nP_Rec_Annot: SMOKE_WRITE_BLOCKED; запись запрещена.");
if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing)
{
var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho";
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
WriteEditor(document, $"\nP_Rec_Annot {commandName}: проект '{context.ProjectDisplayName}' найден; тип чертежа: {(context.IsOrthoDrawing ? "Ortho" : "NotOrtho")}; DWG не изменён.");
WriteUsage(operationId, commandName, status, document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
return;
}
var builder = new AnnotationPlanBuilder();
var plans = new List<AnnotationPlan>();
foreach (var viewport in context.Viewports)
{
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
}
var summary = new
{
ObjectsExamined = plans.Sum(x => x.Summary.ObjectsExamined),
CreateCount = plans.Sum(x => x.Summary.CreateCount),
UpdateCount = plans.Sum(x => x.Summary.UpdateCount),
SkipCount = plans.Sum(x => x.Summary.SkipCount),
ConflictCount = plans.Sum(x => x.Summary.ConflictCount),
UnsupportedCount = plans.Sum(x => x.Summary.UnsupportedCount),
WarningCount = plans.Sum(x => x.Summary.WarningCount),
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
};
details["ObjectsExamined"] = summary.ObjectsExamined;
details["PlanCreateCount"] = summary.CreateCount;
details["PlanUpdateCount"] = summary.UpdateCount;
details["PlanSkipCount"] = summary.SkipCount;
details["PlanConflictCount"] = summary.ConflictCount;
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
details["ErrorCount"] = summary.ErrorCount;
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
WriteEditor(document, $"\nP_Rec_Annot — диагностический план\nПроект: {context.ProjectDisplayName}\nТип чертежа: Ortho\nViewport: {context.Viewports.Count}\nОбъектов найдено: {summary.ObjectsExamined}\nБудет создано: {summary.CreateCount}\nБудет обновлено: {summary.UpdateCount}\nБудет пропущено: {summary.SkipCount}\nКонфликтов: {summary.ConflictCount}\nОшибок: {summary.ErrorCount}\nDWG не изменён.");
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
}
catch (System.Exception exception)
{
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "Failed", exception, document);
WriteUsage(operationId, commandName, "Failed", document, null, stopwatch.ElapsedMilliseconds, exception);
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
}
}
private static bool DataLinksAvailable(Autodesk.ProcessPower.ProjectManager.PlantProject? project)
private static PRecAnnotExecutionMode ParseMode(string value) =>
value.Equals("Smoke", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Smoke :
value.Equals("Apply", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Apply : PRecAnnotExecutionMode.DryRun;
private static bool DataLinksAvailable(bool hasPlantProject)
{
try { return PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; }
try { return hasPlantProject && Autodesk.ProcessPower.PlantInstance.PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; }
catch { return false; }
}
private static void WriteUsage(string operationId, string commandName, string resultStatus, Document? document, IReadOnlyDictionary<string, object?>? details, long duration, System.Exception? exception = null)
private static void WriteUsage(string operationId, string commandName, string status, Document? document, IReadOnlyDictionary<string, object?>? details, long duration, System.Exception? exception = null)
{
var eventDetails = new Dictionary<string, object?>
{
["ObjectsExamined"] = 0,
["AnnotationsCreated"] = 0,
["AnnotationsUpdated"] = 0,
["AnnotationsDeleted"] = 0,
["AnnotationsSkipped"] = 0,
["WarningCount"] = 0,
["ErrorCount"] = exception is null ? 0 : 1,
["DurationMilliseconds"] = duration
["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0,
["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0,
["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration
};
if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value;
RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, resultStatus, exception, document, eventDetails);
RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, status, exception, document, eventDetails);
}
private static void WriteEditor(Document? document, string message)
@@ -115,7 +147,6 @@ internal static class RibbonIntegration
private const string TabId = "PRecAnnot.Plant3D2026";
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
private const string Command = "PHP_REC_ANNOT ";
private static bool deferred;
public static void Register()
@@ -125,23 +156,16 @@ internal static class RibbonIntegration
{
if (ComponentManager.Ribbon is null)
{
if (!deferred)
{
ComponentManager.ItemInitialized += OnItemInitialized;
deferred = true;
}
if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; }
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
return;
}
RegisterNow();
}
catch (System.Exception exception)
{
RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed");
}
catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); }
}
private static void OnItemInitialized(object? sender, System.EventArgs args)
private static void OnItemInitialized(object? sender, EventArgs args)
{
if (ComponentManager.Ribbon is null) return;
ComponentManager.ItemInitialized -= OnItemInitialized;
@@ -151,24 +175,13 @@ internal static class RibbonIntegration
private static void RegisterNow()
{
RibbonControl ribbon = ComponentManager.Ribbon!;
RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase));
if (tab is null)
{
tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" };
ribbon.Tabs.Add(tab);
}
RibbonPanel? panel = tab.Panels.FirstOrDefault(item => string.Equals(item.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
if (panel is not null)
{
RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists");
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists");
return;
}
RibbonPanelSource source = new() { Id = PanelId, Title = "Аннотации" };
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = Command });
var ribbon = ComponentManager.Ribbon!;
var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase));
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
tab.Panels.Add(new RibbonPanel { Source = source });
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
}
@@ -11,7 +11,10 @@
<Reference Include="AcCoreMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcCoreMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AdWindows"><HintPath>$(AutoCAD2026ManagedDir)\AdWindows.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDataLinks"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataLinks.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,206 @@
using System.Security.Cryptography;
using System.Text;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.ProcessPower.DataLinks;
using Autodesk.ProcessPower.P3dProjectParts;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.ProcessPower.ProjectManager;
using PRecAnnot.Abstractions;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
{
public PlantRuntimeContext ReadCurrentContext()
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null)
{
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
}
PlantProject? project;
try { project = PlantApplication.CurrentProject; }
catch (Exception exception)
{
return Empty(true, false, "PLANT_PROJECT_READ_FAILED", exception.GetType().Name);
}
if (project is null)
{
return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден.");
}
OrthoProject? orthoProject;
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
catch (Exception exception)
{
return Empty(true, true, "ORTHO_PROJECT_PART_READ_FAILED", exception.GetType().Name);
}
if (orthoProject is null)
{
return Empty(true, true, "ORTHO_PROJECT_PART_UNAVAILABLE", "Ortho project part недоступен.", OrthoDetectionStatus.Unsupported, "ProjectPart(Ortho)");
}
try
{
string resolvedPath;
var isOrtho = OrthoDocUtils.IsValidOrthoDrawing(document.Name, orthoProject, out resolvedPath);
if (!isOrtho)
{
return Empty(true, true, "NOT_ORTHO_DRAWING", "OrthoDocUtils.IsValidOrthoDrawing вернул false.", OrthoDetectionStatus.NotOrtho, "Ortho project part", "OrthoDocUtils.IsValidOrthoDrawing=false");
}
var viewports = new PlantOrthoViewportReader(orthoProject, document.Name).ReadViewports(CancellationToken.None);
return new PlantRuntimeContext
{
HasActiveDocument = true,
HasPlantProject = true,
IsOrthoDrawing = true,
DrawingName = Path.GetFileName(document.Name),
ProjectDisplayName = project.Name,
Viewports = viewports,
Diagnostics = Array.Empty<RuntimeDiagnostic>(),
Detection = new OrthoDetectionResult
{
Status = OrthoDetectionStatus.Confirmed,
Evidence = new[] { "ProjectPart(Ortho)", "OrthoDocUtils.IsValidOrthoDrawing=true", "DwgSheetDefinitions" },
Diagnostics = Array.Empty<RuntimeDiagnostic>()
}
};
}
catch (Exception exception)
{
return Empty(true, true, "ORTHO_DETECTION_FAILED", exception.GetType().Name, OrthoDetectionStatus.Failed, "ProjectPart(Ortho)");
}
}
private static PlantRuntimeContext Empty(bool hasDocument, bool hasProject, string code, string message, OrthoDetectionStatus status = OrthoDetectionStatus.NotOrtho, params string[] evidence) => new()
{
HasActiveDocument = hasDocument,
HasPlantProject = hasProject,
IsOrthoDrawing = false,
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
};
}
internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
{
private readonly OrthoProject project;
private readonly string drawingName;
public PlantOrthoViewportReader(OrthoProject project, string drawingName)
{
this.project = project;
this.drawingName = Path.GetFileName(drawingName);
}
public IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken)
{
var result = new List<OrthoViewportInfo>();
foreach (var sheet in project.DwgSheetDefinitions.AsArray())
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(Path.GetFileName(sheet.DwgName), drawingName, StringComparison.OrdinalIgnoreCase)) continue;
foreach (var view in sheet.DwgViewDefs.AsArray())
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var id = StableId(sheet.DwgGuid.ToString(), view.ViewName);
result.Add(new OrthoViewportInfo
{
Id = id,
DisplayName = view.ViewName,
CenterX = view.CenterPoint.X,
CenterY = view.CenterPoint.Y,
Width = view.ViewWidth,
Height = view.ViewHeight,
Scale = view.ViewScale,
TwistAngle = 0,
IsOn = true,
IsLocked = false,
SourceDrawingName = sheet.DwgName,
ViewName = view.ViewName
});
}
catch
{
// One malformed view must not abort the remaining read-only analysis.
}
}
}
return result.OrderByDescending(x => x.CenterY).ThenBy(x => x.CenterX).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
}
private static string StableId(string sheetId, string viewName)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
return "vp-" + Convert.ToHexString(hash)[..20];
}
}
internal sealed class PlantObjectReader : IPlantObjectReader
{
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null) return Array.Empty<PlantObjectSnapshot>();
DataLinksManager? dataLinks;
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
catch { return Array.Empty<PlantObjectSnapshot>(); }
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
var snapshots = new List<PlantObjectSnapshot>();
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var className = dataLinks.GetObjectClassname(rowId);
var properties = dataLinks.GetAllProperties(rowId, true)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
snapshots.Add(new PlantObjectSnapshot
{
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
ObjectClass = className ?? string.Empty,
Tag = Value("Tag", "TAG"),
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
SupportNumber = Value("Support Number", "SupportNumber"),
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
Geometry = new GeometrySnapshot(),
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
});
}
catch
{
// Keep the read path best-effort; no write operation is attempted.
}
}
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
}
private static string StableId(string drawingName, int rowId, string? className)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
return "obj-" + Convert.ToHexString(hash)[..24];
}
}
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
{
public void EnsureWriteAllowed(string operation)
{
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
}
}