feat: read Plant 3D Ortho context and viewports
This commit is contained in:
@@ -1,5 +1,117 @@
|
|||||||
namespace PRecAnnot.Abstractions;
|
namespace PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
public sealed record RuntimeDiagnostic
|
||||||
|
{
|
||||||
|
public required string Code { get; init; }
|
||||||
|
public required string Message { get; init; }
|
||||||
|
public string Severity { get; init; } = "Warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
|
||||||
|
|
||||||
|
public sealed record OrthoDetectionResult
|
||||||
|
{
|
||||||
|
public required OrthoDetectionStatus Status { get; init; }
|
||||||
|
public required IReadOnlyList<string> Evidence { get; init; }
|
||||||
|
public required IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct Point2Dto(double X, double Y);
|
||||||
|
public readonly record struct Point3Dto(double X, double Y, double Z);
|
||||||
|
public readonly record struct Vector2Dto(double X, double Y);
|
||||||
|
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
|
||||||
|
|
||||||
|
public sealed record GeometrySnapshot
|
||||||
|
{
|
||||||
|
public Point3Dto? WorldPosition { get; init; }
|
||||||
|
public Point2Dto? PaperPosition { get; init; }
|
||||||
|
public Bounds2Dto? ProjectedBounds { get; init; }
|
||||||
|
public double? Rotation { get; init; }
|
||||||
|
public double? Elevation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoViewportInfo
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
public double CenterX { get; init; }
|
||||||
|
public double CenterY { get; init; }
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { get; init; }
|
||||||
|
public double Scale { get; init; }
|
||||||
|
public double TwistAngle { get; init; }
|
||||||
|
public bool IsOn { get; init; }
|
||||||
|
public bool IsLocked { get; init; }
|
||||||
|
public string? SourceDrawingName { get; init; }
|
||||||
|
public string? ViewName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlantObjectSnapshot
|
||||||
|
{
|
||||||
|
public required string SnapshotId { get; init; }
|
||||||
|
public required string ObjectClass { get; init; }
|
||||||
|
public string? Tag { get; init; }
|
||||||
|
public string? LineNumber { get; init; }
|
||||||
|
public string? SupportNumber { get; init; }
|
||||||
|
public string? EquipmentNumber { get; init; }
|
||||||
|
public required GeometrySnapshot Geometry { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||||
|
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlantRuntimeContext
|
||||||
|
{
|
||||||
|
public bool HasActiveDocument { get; init; }
|
||||||
|
public bool HasPlantProject { get; init; }
|
||||||
|
public bool IsOrthoDrawing { get; init; }
|
||||||
|
public string? DrawingName { get; init; }
|
||||||
|
public string? ProjectDisplayName { get; init; }
|
||||||
|
public IReadOnlyList<OrthoViewportInfo> Viewports { get; init; } = Array.Empty<OrthoViewportInfo>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty<string>(), Diagnostics = Array.Empty<RuntimeDiagnostic>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
||||||
|
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
||||||
|
|
||||||
|
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
||||||
|
|
||||||
|
public sealed record AnnotationOperationPlan
|
||||||
|
{
|
||||||
|
public required string SourceSnapshotId { get; init; }
|
||||||
|
public required AnnotationOperationKind Kind { get; init; }
|
||||||
|
public string? AnnotationType { get; init; }
|
||||||
|
public string? Text { get; init; }
|
||||||
|
public Point2Dto? Position { get; init; }
|
||||||
|
public double? Rotation { get; init; }
|
||||||
|
public string? ExistingAnnotationId { get; init; }
|
||||||
|
public required IReadOnlyList<string> Reasons { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AnnotationPlanSummary
|
||||||
|
{
|
||||||
|
public int ObjectsExamined { get; init; }
|
||||||
|
public int CreateCount { get; init; }
|
||||||
|
public int UpdateCount { get; init; }
|
||||||
|
public int SkipCount { get; init; }
|
||||||
|
public int ConflictCount { get; init; }
|
||||||
|
public int UnsupportedCount { get; init; }
|
||||||
|
public int WarningCount { get; init; }
|
||||||
|
public int ErrorCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
public required string CommandName { get; init; }
|
||||||
|
public required OrthoViewportInfo Viewport { get; init; }
|
||||||
|
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
||||||
|
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
||||||
|
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
||||||
|
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
||||||
|
|
||||||
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
||||||
|
|
||||||
public sealed record PlantObjectData
|
public sealed record PlantObjectData
|
||||||
@@ -36,6 +148,11 @@ public sealed record AnnotationIntent
|
|||||||
|
|
||||||
public sealed record AnnotationPlan
|
public sealed record AnnotationPlan
|
||||||
{
|
{
|
||||||
|
public string PlanId { get; init; } = string.Empty;
|
||||||
|
public string CommandName { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
public AnnotationPlanSummary Summary { get; init; } = new();
|
||||||
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
|
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
|
||||||
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
||||||
public required IReadOnlyList<string> Delete { get; init; }
|
public required IReadOnlyList<string> Delete { get; init; }
|
||||||
@@ -58,7 +175,7 @@ public interface IOrthoDrawingContext
|
|||||||
|
|
||||||
public interface IPlantObjectReader
|
public interface IPlantObjectReader
|
||||||
{
|
{
|
||||||
IReadOnlyList<PlantObjectData> ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken);
|
IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAnnotationRepository
|
public interface IAnnotationRepository
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||||
|
{
|
||||||
|
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||||
|
{
|
||||||
|
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
|
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var operations = new List<AnnotationOperationPlan>(objects.Length);
|
||||||
|
var creates = new List<AnnotationIntent>();
|
||||||
|
var updates = new List<AnnotationIntent>();
|
||||||
|
var diagnostics = new List<RuntimeDiagnostic>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var item in objects)
|
||||||
|
{
|
||||||
|
if (!seen.Add(item.SnapshotId))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Conflict, Reasons = new[] { "duplicate-snapshot-id" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(item.ObjectClass))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "missing-object-class" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "missing-line-number-or-tag" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = item.Geometry.PaperPosition;
|
||||||
|
if (position is null)
|
||||||
|
{
|
||||||
|
diagnostics.Add(new RuntimeDiagnostic { Code = "MISSING_GEOMETRY", Message = $"No paper position for {item.SnapshotId}." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var stableKey = $"{request.Viewport.SourceDrawingName ?? string.Empty}:{request.Viewport.Id}:{item.SnapshotId}";
|
||||||
|
var existingAnnotation = existing.TryGetValue(stableKey, out var match) ? match : existing.TryGetValue(item.SnapshotId, out var directMatch) ? directMatch : null;
|
||||||
|
var kind = existingAnnotation is null ? AnnotationOperationKind.Create : AnnotationOperationKind.Update;
|
||||||
|
operations.Add(new AnnotationOperationPlan
|
||||||
|
{
|
||||||
|
SourceSnapshotId = item.SnapshotId,
|
||||||
|
Kind = kind,
|
||||||
|
AnnotationType = "PlantObject",
|
||||||
|
Text = text,
|
||||||
|
Position = position,
|
||||||
|
Rotation = item.Geometry.Rotation,
|
||||||
|
ExistingAnnotationId = existingAnnotation?.StableKey,
|
||||||
|
Reasons = Array.Empty<string>()
|
||||||
|
});
|
||||||
|
|
||||||
|
var intent = new AnnotationIntent
|
||||||
|
{
|
||||||
|
StableKey = stableKey,
|
||||||
|
Text = text,
|
||||||
|
Position = position is { } point ? new AnnotationPoint(point.X, point.Y) : new AnnotationPoint(0, 0),
|
||||||
|
Rotation = item.Geometry.Rotation ?? 0,
|
||||||
|
ClassName = item.ObjectClass,
|
||||||
|
Layer = "PXP_ANNOTATION"
|
||||||
|
};
|
||||||
|
if (kind == AnnotationOperationKind.Create) creates.Add(intent); else updates.Add(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = new AnnotationPlanSummary
|
||||||
|
{
|
||||||
|
ObjectsExamined = objects.Length,
|
||||||
|
CreateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Create),
|
||||||
|
UpdateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Update),
|
||||||
|
SkipCount = operations.Count(x => x.Kind == AnnotationOperationKind.Skip),
|
||||||
|
ConflictCount = operations.Count(x => x.Kind == AnnotationOperationKind.Conflict),
|
||||||
|
UnsupportedCount = operations.Count(x => x.Kind == AnnotationOperationKind.Unsupported),
|
||||||
|
WarningCount = diagnostics.Count(x => !string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
ErrorCount = diagnostics.Count(x => string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase))
|
||||||
|
};
|
||||||
|
|
||||||
|
return new AnnotationPlan
|
||||||
|
{
|
||||||
|
PlanId = BuildPlanId(request, objects),
|
||||||
|
CommandName = request.CommandName,
|
||||||
|
Operations = operations,
|
||||||
|
Diagnostics = diagnostics,
|
||||||
|
Summary = summary,
|
||||||
|
Create = creates,
|
||||||
|
Update = updates,
|
||||||
|
Delete = Array.Empty<string>(),
|
||||||
|
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
|
||||||
|
ExaminedCount = objects.Length,
|
||||||
|
IsDeterministic = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
|
||||||
|
{
|
||||||
|
var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id);
|
||||||
|
foreach (var item in objects)
|
||||||
|
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||||
|
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,43 +6,21 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
|
|||||||
{
|
{
|
||||||
public AnnotationPlan BuildPlan(AnnotationRequest request)
|
public AnnotationPlan BuildPlan(AnnotationRequest request)
|
||||||
{
|
{
|
||||||
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
|
var objects = request.Objects.Select(item => new PlantObjectSnapshot
|
||||||
var create = new List<AnnotationIntent>();
|
|
||||||
var update = new List<AnnotationIntent>();
|
|
||||||
var skipped = new List<string>();
|
|
||||||
|
|
||||||
foreach (var item in request.Objects.OrderBy(x => x.ObjectId, StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag))
|
SnapshotId = item.ObjectId,
|
||||||
|
ObjectClass = item.ClassName,
|
||||||
|
Tag = item.Tag,
|
||||||
|
LineNumber = item.LineNumber,
|
||||||
|
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
{
|
{
|
||||||
skipped.Add($"{item.ObjectId}:missing-line-number-or-tag");
|
CommandName = "PHP_REC_ANNOT",
|
||||||
continue;
|
Viewport = new OrthoViewportInfo { Id = request.Viewport.ViewportId, DisplayName = request.Viewport.ViewportId, SourceDrawingName = request.Viewport.DrawingId },
|
||||||
|
Objects = objects,
|
||||||
|
Existing = request.Existing
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = BuildText(item);
|
|
||||||
var intent = new AnnotationIntent
|
|
||||||
{
|
|
||||||
StableKey = $"{request.Viewport.DrawingId}:{request.Viewport.ViewportId}:{item.ObjectId}",
|
|
||||||
Text = text,
|
|
||||||
Position = item.Position,
|
|
||||||
Rotation = item.Rotation,
|
|
||||||
ClassName = item.ClassName,
|
|
||||||
Layer = "PXP_ANNOTATION"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existing.ContainsKey(intent.StableKey)) update.Add(intent); else create.Add(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AnnotationPlan
|
|
||||||
{
|
|
||||||
Create = create,
|
|
||||||
Update = update,
|
|
||||||
Delete = Array.Empty<string>(),
|
|
||||||
SkipReasons = skipped,
|
|
||||||
ExaminedCount = request.Objects.Count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildText(PlantObjectData item) =>
|
|
||||||
string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class ExecutionModeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||||
|
{
|
||||||
|
public void EnsureWriteAllowed(string operation) => throw new InvalidOperationException(mode == PRecAnnotExecutionMode.Apply ? "APPLY_MODE_NOT_IMPLEMENTED" : "SMOKE_WRITE_BLOCKED");
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class OrthoDetectionClassifier
|
||||||
|
{
|
||||||
|
public static OrthoDetectionResult Classify(bool hasPlantProject, bool hasOrthoPart, bool apiConfirmed, bool metadataAmbiguous = false)
|
||||||
|
{
|
||||||
|
if (!hasPlantProject) return Result(OrthoDetectionStatus.NotOrtho, "NO_PLANT_PROJECT", "Active Plant project is absent.");
|
||||||
|
if (metadataAmbiguous) return Result(OrthoDetectionStatus.Ambiguous, "AMBIGUOUS_ORTHO_METADATA", "Plant metadata is insufficient to confirm Ortho.");
|
||||||
|
if (!hasOrthoPart) return Result(OrthoDetectionStatus.Unsupported, "ORTHO_PART_UNAVAILABLE", "Ortho project part is unavailable.");
|
||||||
|
return apiConfirmed
|
||||||
|
? Result(OrthoDetectionStatus.Confirmed, "ORTHO_API_CONFIRMED", "OrthoDocUtils confirmed the active drawing.")
|
||||||
|
: Result(OrthoDetectionStatus.NotOrtho, "ORTHO_API_NEGATIVE", "OrthoDocUtils did not confirm the active drawing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoDetectionResult Result(OrthoDetectionStatus status, string code, string message) => new()
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
Evidence = new[] { code },
|
||||||
|
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
using Autodesk.AutoCAD.EditorInput;
|
|
||||||
using Autodesk.AutoCAD.Runtime;
|
using Autodesk.AutoCAD.Runtime;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
|
||||||
using Autodesk.Windows;
|
using Autodesk.Windows;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
namespace PRecAnnot.Plant3D2026;
|
namespace PRecAnnot.Plant3D2026;
|
||||||
@@ -39,20 +39,24 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
Document? document = null;
|
Document? document = null;
|
||||||
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
|
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
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?>
|
var details = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["IsPlantProjectOpen"] = project is not null,
|
["ExecutionMode"] = mode.ToString(),
|
||||||
["IsOrthoDrawing"] = false,
|
["IsPlantProjectDetected"] = context.HasPlantProject,
|
||||||
["ViewportCount"] = null,
|
["IsPlantProjectOpen"] = context.HasPlantProject,
|
||||||
["DataLinksAvailable"] = DataLinksAvailable(project)
|
["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);
|
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
|
||||||
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
|
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
|
||||||
@@ -61,47 +65,75 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightAccepted", document: document, details: details);
|
if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing)
|
||||||
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);
|
var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho";
|
||||||
WriteUsage(operationId, commandName, "SMOKE_WRITE_BLOCKED", document, null, stopwatch.ElapsedMilliseconds, exception);
|
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
|
||||||
WriteEditor(document, "\nP_Rec_Annot: SMOKE_WRITE_BLOCKED; запись запрещена.");
|
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)
|
catch (System.Exception exception)
|
||||||
{
|
{
|
||||||
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, "Failed", exception, document);
|
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
|
||||||
WriteUsage(operationId, commandName, "Failed", document, null, stopwatch.ElapsedMilliseconds, exception);
|
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
|
||||||
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
|
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; }
|
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?>
|
var eventDetails = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["ObjectsExamined"] = 0,
|
["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0,
|
||||||
["AnnotationsCreated"] = 0,
|
["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0,
|
||||||
["AnnotationsUpdated"] = 0,
|
["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration
|
||||||
["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;
|
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)
|
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 TabId = "PRecAnnot.Plant3D2026";
|
||||||
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
|
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
|
||||||
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
|
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
|
||||||
private const string Command = "PHP_REC_ANNOT ";
|
|
||||||
private static bool deferred;
|
private static bool deferred;
|
||||||
|
|
||||||
public static void Register()
|
public static void Register()
|
||||||
@@ -125,23 +156,16 @@ internal static class RibbonIntegration
|
|||||||
{
|
{
|
||||||
if (ComponentManager.Ribbon is null)
|
if (ComponentManager.Ribbon is null)
|
||||||
{
|
{
|
||||||
if (!deferred)
|
if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; }
|
||||||
{
|
|
||||||
ComponentManager.ItemInitialized += OnItemInitialized;
|
|
||||||
deferred = true;
|
|
||||||
}
|
|
||||||
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
|
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RegisterNow();
|
RegisterNow();
|
||||||
}
|
}
|
||||||
catch (System.Exception exception)
|
catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); }
|
||||||
{
|
|
||||||
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;
|
if (ComponentManager.Ribbon is null) return;
|
||||||
ComponentManager.ItemInitialized -= OnItemInitialized;
|
ComponentManager.ItemInitialized -= OnItemInitialized;
|
||||||
@@ -151,24 +175,13 @@ internal static class RibbonIntegration
|
|||||||
|
|
||||||
private static void RegisterNow()
|
private static void RegisterNow()
|
||||||
{
|
{
|
||||||
RibbonControl ribbon = ComponentManager.Ribbon!;
|
var ribbon = ComponentManager.Ribbon!;
|
||||||
RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase));
|
var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase));
|
||||||
if (tab is null)
|
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));
|
||||||
tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" };
|
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
|
||||||
ribbon.Tabs.Add(tab);
|
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
|
||||||
}
|
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
|
||||||
|
|
||||||
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 });
|
|
||||||
tab.Panels.Add(new RibbonPanel { Source = source });
|
tab.Panels.Add(new RibbonPanel { Source = source });
|
||||||
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
|
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="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="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="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="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="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>
|
</ItemGroup>
|
||||||
</Project>
|
</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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user