479 lines
22 KiB
C#
479 lines
22 KiB
C#
using System.Security.Cryptography;
|
|
using System.Collections.Specialized;
|
|
using System.Text;
|
|
using Autodesk.AutoCAD.ApplicationServices;
|
|
using Autodesk.AutoCAD.DatabaseServices;
|
|
using Autodesk.ProcessPower.DataLinks;
|
|
using Autodesk.ProcessPower.Drawings2d;
|
|
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-проект не найден.");
|
|
}
|
|
|
|
var displayName = string.IsNullOrWhiteSpace(project.Name) ? null : project.Name.Trim();
|
|
var projectStatus = displayName is null ? PlantProjectDetectionStatus.FoundWithoutDisplayName : PlantProjectDetectionStatus.FoundWithName;
|
|
|
|
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 = displayName ?? "display name unavailable",
|
|
ProjectDetectionStatus = projectStatus,
|
|
Viewports = viewports,
|
|
Diagnostics = displayName is null ? new[] { new RuntimeDiagnostic { Code = "PLANT_PROJECT_DISPLAY_NAME_EMPTY", Message = "Plant project is open but no display name is available.", Severity = "Warning" } } : 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,
|
|
ProjectDisplayName = hasProject ? "display name unavailable" : null,
|
|
ProjectDetectionStatus = hasProject ? PlantProjectDetectionStatus.Ambiguous : PlantProjectDetectionStatus.NotFound,
|
|
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 = ReadSourceDrawingPath(view),
|
|
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];
|
|
}
|
|
|
|
private static string? ReadSourceDrawingPath(IDwgViewDef view)
|
|
{
|
|
// Confirmed Plant 3D 2026 contract (PnPDwg2dDef/PnPDwg2dUtil):
|
|
// IDwgViewDef.DwgQuery -> IDwgQuery.ModelFiles -> ModelFile.ModelFileName.
|
|
// The Ortho sheet is never used as a Piping DataLinks source.
|
|
try
|
|
{
|
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
|
{
|
|
RuntimeDiagnostics.Write("OrthoModelFilesContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
|
return null;
|
|
}
|
|
|
|
if (RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled)
|
|
{
|
|
OrthoRuntimeStructureInspector.Inspect(view);
|
|
}
|
|
|
|
return view.DwgQuery?.ModelFiles
|
|
.Select(file => file.ModelFileName)
|
|
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
RuntimeDiagnostics.Write("OrthoModelFilesReadFailed", "WARNING", resultStatus: "ORTHO_MODEL_FILES_API_UNSUPPORTED", exception: exception);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal static class OrthoModelFilesContract
|
|
{
|
|
private const string ExpectedViewType = "Autodesk.ProcessPower.Drawings2d.DwgViewDef";
|
|
private const string ExpectedQueryType = "Autodesk.ProcessPower.Drawings2d.DwgQueryByBox";
|
|
private const string ExpectedModelFileType = "Autodesk.ProcessPower.Drawings2d.ModelFile";
|
|
private const string ExpectedDefAssembly = "PnPDwg2dDef";
|
|
private const string ExpectedUtilAssembly = "PnPDwg2dUtil";
|
|
private const string ExpectedVersion = "0.0.0.0";
|
|
|
|
public static bool IsSupported(IDwgViewDef view, out OrthoModelFilesContractDiagnostic diagnostic)
|
|
{
|
|
var query = view.DwgQuery;
|
|
var files = query?.ModelFiles;
|
|
var valid = HasExpectedType(view, ExpectedViewType, ExpectedDefAssembly) &&
|
|
query is not null && HasExpectedType(query, ExpectedQueryType, ExpectedDefAssembly) &&
|
|
files is not null && files.GetType() == typeof(ModelFile[]) &&
|
|
files.All(file => file is not null && HasExpectedType(file, ExpectedModelFileType, ExpectedUtilAssembly));
|
|
diagnostic = new OrthoModelFilesContractDiagnostic(
|
|
valid ? "ORTHO_MODEL_FILES_API_SUPPORTED" : "ORTHO_MODEL_FILES_API_UNSUPPORTED",
|
|
new Dictionary<string, object?>
|
|
{
|
|
["ExpectedViewType"] = ExpectedViewType,
|
|
["ActualViewType"] = TypeName(view),
|
|
["ExpectedQueryType"] = ExpectedQueryType,
|
|
["ActualQueryType"] = query is null ? null : TypeName(query),
|
|
["ExpectedModelFileType"] = ExpectedModelFileType,
|
|
["ActualModelFilesType"] = files?.GetType().AssemblyQualifiedName,
|
|
["ExpectedAssemblyVersion"] = ExpectedVersion,
|
|
["ActualViewAssemblyVersion"] = view.GetType().Assembly.GetName().Version?.ToString(),
|
|
["ActualQueryAssemblyVersion"] = query?.GetType().Assembly.GetName().Version?.ToString(),
|
|
["ActualModelFilesAssemblyVersion"] = files?.GetType().Assembly.GetName().Version?.ToString()
|
|
});
|
|
return valid;
|
|
}
|
|
|
|
private static bool HasExpectedType(object value, string expectedType, string expectedAssembly) =>
|
|
string.Equals(value.GetType().FullName, expectedType, StringComparison.Ordinal) &&
|
|
string.Equals(value.GetType().Assembly.GetName().Name, expectedAssembly, StringComparison.Ordinal) &&
|
|
string.Equals(value.GetType().Assembly.GetName().Version?.ToString(), ExpectedVersion, StringComparison.Ordinal);
|
|
|
|
private static string TypeName(object value) => $"{value.GetType().FullName}, {value.GetType().Assembly.GetName().Name}, {value.GetType().Assembly.GetName().Version}";
|
|
}
|
|
|
|
internal sealed record OrthoModelFilesContractDiagnostic(string Code, IReadOnlyDictionary<string, object?> Details);
|
|
|
|
internal sealed class PlantObjectReader : IPlantObjectReader
|
|
{
|
|
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName))
|
|
{
|
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", viewport);
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
|
|
PipingProject? pipingProject;
|
|
try { pipingProject = PlantApplication.ProjectPart("Piping") as PipingProject; }
|
|
catch (Exception exception)
|
|
{
|
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport, exception);
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
if (pipingProject is null)
|
|
{
|
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport);
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
|
|
var sourceDrawing = ResolveProjectDrawing(pipingProject, viewport.SourceDrawingName);
|
|
if (sourceDrawing is null) return Array.Empty<PlantObjectSnapshot>();
|
|
|
|
DataLinksManager? dataLinks;
|
|
try { dataLinks = pipingProject.DataLinksManager; }
|
|
catch (Exception exception)
|
|
{
|
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
if (dataLinks is null)
|
|
{
|
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport);
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
|
|
var snapshots = new List<PlantObjectSnapshot>();
|
|
try
|
|
{
|
|
// Confirmed Plant 3D 2026 contract: resolve the source model to the
|
|
// Piping project drawing record, then read its DataLinks by drawing Id.
|
|
// The active Ortho database is deliberately never passed to DataLinks.
|
|
var rowIds = dataLinks.SelectAcPpRowIds(sourceDrawing.Id);
|
|
if (rowIds.Count == 0)
|
|
{
|
|
WriteOutcome("SOURCE_DATALINKS_NO_ROWS", viewport, details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
|
return Array.Empty<PlantObjectSnapshot>();
|
|
}
|
|
|
|
var membership = PlantViewportMembershipReader.Read(viewport, dataLinks, rowIds);
|
|
|
|
foreach (var rowId in rowIds)
|
|
{
|
|
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(viewport.SourceDrawingName, 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" },
|
|
MembershipStatus = membership.StatusFor(rowId),
|
|
MembershipEvidenceCodes = membership.EvidenceFor(rowId)
|
|
});
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
RuntimeDiagnostics.Write("SourceDataLinksRowReadFailed", "WARNING", resultStatus: "SOURCE_DATABASE_READ_FAILED", exception: exception);
|
|
}
|
|
}
|
|
}
|
|
catch (DLException exception)
|
|
{
|
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
WriteOutcome("SOURCE_DATABASE_READ_FAILED", viewport, exception);
|
|
}
|
|
|
|
WriteOutcome(snapshots.Count == 0 ? "SOURCE_PIPE_ROWS_NOT_FOUND" : "SOURCE_DATALINKS_READ", viewport,
|
|
details: new Dictionary<string, object?> { ["SourcePlantObjectsRead"] = snapshots.Count, ["PipeObjectsRead"] = snapshots.Count });
|
|
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
|
}
|
|
|
|
private static PnPProjectDrawing? ResolveProjectDrawing(PipingProject pipingProject, string sourceDrawingPath)
|
|
{
|
|
if (!Path.IsPathRooted(sourceDrawingPath))
|
|
{
|
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", null);
|
|
return null;
|
|
}
|
|
|
|
string normalizedSource;
|
|
try { normalizedSource = Path.GetFullPath(sourceDrawingPath); }
|
|
catch (Exception exception)
|
|
{
|
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", null, exception);
|
|
return null;
|
|
}
|
|
|
|
var matches = pipingProject.GetPnPDrawingFiles()
|
|
.Where(drawing => MatchesSourcePath(drawing, normalizedSource))
|
|
.OrderBy(drawing => drawing.Id)
|
|
.ToArray();
|
|
if (matches.Length == 1)
|
|
{
|
|
RuntimeDiagnostics.Write("SourceProjectDrawingResolved", resultStatus: "SOURCE_FROM_PROJECT_DRAWING_ROW", details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
|
return matches[0];
|
|
}
|
|
|
|
WriteOutcome(matches.Length == 0 ? "ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT" : "ORTHO_SOURCE_DRAWING_AMBIGUOUS", null,
|
|
details: new Dictionary<string, object?> { ["CandidateCount"] = matches.Length });
|
|
return null;
|
|
}
|
|
|
|
private static bool MatchesSourcePath(PnPProjectDrawing drawing, string normalizedSource) =>
|
|
EquivalentPath(drawing.AbsoluteFileName, normalizedSource) ||
|
|
EquivalentPath(drawing.ResolvedFilePath, normalizedSource);
|
|
|
|
private static bool EquivalentPath(string? candidate, string normalizedSource)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(candidate)) return false;
|
|
try { return string.Equals(Path.GetFullPath(candidate), normalizedSource, StringComparison.OrdinalIgnoreCase); }
|
|
catch { return false; }
|
|
}
|
|
|
|
private static void WriteOutcome(string code, OrthoViewportInfo? viewport, Exception? exception = null, IReadOnlyDictionary<string, object?>? details = null)
|
|
{
|
|
var sanitizedDetails = new Dictionary<string, object?>
|
|
{
|
|
["SourceResolved"] = viewport?.SourceDrawingName is not null
|
|
};
|
|
if (details is not null) foreach (var detail in details) sanitizedDetails[detail.Key] = detail.Value;
|
|
RuntimeDiagnostics.Write("SourceDataLinksReadCompleted", code == "SOURCE_DATALINKS_READ" ? "INFO" : "WARNING", resultStatus: code, exception: exception, details: sanitizedDetails);
|
|
}
|
|
|
|
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 PlantViewportMembershipReader
|
|
{
|
|
internal readonly record struct MembershipReadResult(
|
|
IReadOnlySet<int> SelectedRows,
|
|
ViewMembershipStatus DefaultStatus,
|
|
string DefaultCode)
|
|
{
|
|
public ViewMembershipStatus StatusFor(int rowId) =>
|
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
|
? ViewMembershipStatus.ObjectOutsideView
|
|
: DefaultStatus;
|
|
|
|
public IReadOnlyList<string> EvidenceFor(int rowId) =>
|
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
|
? new[] { "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP" }
|
|
: new[] { DefaultCode };
|
|
}
|
|
|
|
internal static MembershipReadResult Read(OrthoViewportInfo viewport, DataLinksManager dataLinks, Autodesk.ProcessPower.DataObjects.PnPRowIdArray sourceRows)
|
|
{
|
|
try
|
|
{
|
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject;
|
|
if (project is null) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
|
|
|
var candidates = project.DwgSheetDefinitions.AsArray()
|
|
.SelectMany(sheet => sheet.DwgViewDefs.AsArray()
|
|
.Where(view => string.Equals(StableId(sheet.DwgGuid.ToString(), view.ViewName), viewport.Id, StringComparison.Ordinal))
|
|
.Select(view => (sheet, view)))
|
|
.ToArray();
|
|
if (candidates.Length != 1) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
|
|
|
var view = candidates[0].view;
|
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
|
{
|
|
RuntimeDiagnostics.Write("ViewportMembershipContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
|
}
|
|
|
|
var selectedRows = new HashSet<int>();
|
|
var selected = view.DwgQuery.Execute(new StringCollection(), new StringCollection());
|
|
for (var index = 0; index < selected.Count; index++)
|
|
{
|
|
Autodesk.ProcessPower.Drawings2d.MdObjectId sourceId = default;
|
|
if (!selected.GetItem(index, ref sourceId) || sourceId.IsNull || sourceId.PrjObjectId.IsNull) continue;
|
|
try
|
|
{
|
|
var rowId = dataLinks.FindAcPpRowId(sourceId.PrjObjectId);
|
|
if (sourceRows.Contains(rowId)) selectedRows.Add(rowId);
|
|
}
|
|
catch (DLException)
|
|
{
|
|
// A generated entity without a Piping DataLinks row is outside this reader's source set.
|
|
}
|
|
}
|
|
|
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "INFO", resultStatus: "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", details: new Dictionary<string, object?>
|
|
{
|
|
["SelectedSourceRows"] = selectedRows.Count,
|
|
["SourceRows"] = sourceRows.Count
|
|
});
|
|
return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "WARNING", resultStatus: "VIEW_MEMBERSHIP_API_UNSUPPORTED", exception: exception);
|
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
|
}
|
|
}
|
|
|
|
private static MembershipReadResult Unsupported(string code) => new(new HashSet<int>(), ViewMembershipStatus.NotResolved, code);
|
|
|
|
// Ortho viewport IDs are generated by PlantOrthoViewportReader from exactly this pair.
|
|
// Only an exact regenerated ID is accepted; the reader never chooses an arbitrary view.
|
|
private static string StableId(string sheetId, string? viewName)
|
|
{
|
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + (viewName ?? string.Empty)));
|
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
|
}
|
|
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|