feat: resolve Ortho viewport membership from confirmed evidence
This commit is contained in:
@@ -64,6 +64,15 @@ public sealed record PlantObjectSnapshot
|
||||
|
||||
public enum ViewMembershipStatus { NotResolved, Resolved, PartiallyResolved, ObjectOutsideView, Unsupported, Failed }
|
||||
|
||||
public sealed record SourceObjectViewportMembership
|
||||
{
|
||||
public required string SourceObjectStableKey { get; init; }
|
||||
public required string ViewStableKey { get; init; }
|
||||
public required ViewMembershipStatus Status { get; init; }
|
||||
public IReadOnlyList<string> EvidenceCodes { get; init; } = Array.Empty<string>();
|
||||
public string? ReasonCode { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PlantRuntimeContext
|
||||
{
|
||||
public bool HasActiveDocument { get; init; }
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
|
||||
if (item.MembershipStatus is not ViewMembershipStatus.Resolved and not ViewMembershipStatus.PartiallyResolved)
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "VIEW_MEMBERSHIP_NOT_RESOLVED" } });
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { MembershipReason(item.MembershipStatus) } });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -127,4 +127,12 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
}
|
||||
|
||||
private static string Normalize(string? value) => (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormC);
|
||||
|
||||
private static string MembershipReason(ViewMembershipStatus status) => status switch
|
||||
{
|
||||
ViewMembershipStatus.ObjectOutsideView => "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP",
|
||||
ViewMembershipStatus.Unsupported => "VIEW_MEMBERSHIP_API_UNSUPPORTED",
|
||||
ViewMembershipStatus.Failed => "VIEW_MEMBERSHIP_TRANSFORM_FAILED",
|
||||
_ => "VIEW_MEMBERSHIP_NOT_RESOLVED"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
var membershipResolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved);
|
||||
var membershipPartial = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.PartiallyResolved);
|
||||
var membershipUnresolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.NotResolved);
|
||||
var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||
@@ -106,6 +107,7 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
details["MembershipResolved"] = membershipResolved;
|
||||
details["MembershipPartial"] = membershipPartial;
|
||||
details["MembershipUnresolved"] = membershipUnresolved;
|
||||
details["MembershipOutsideView"] = membershipOutside;
|
||||
details["PlanCreateCount"] = summary.CreateCount;
|
||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||
details["PlanSkipCount"] = summary.SkipCount;
|
||||
@@ -114,7 +116,7 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
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 не изменён.");
|
||||
WriteEditor(document, $"\nPlan schema: {AnnotationPlanBuilder.CurrentPlanSchemaVersion}; PlanId: {planId ?? "multiple"}; membership resolved/partial/unresolved: {membershipResolved}/{membershipPartial}/{membershipUnresolved}.");
|
||||
WriteEditor(document, $"\nPlan schema: {AnnotationPlanBuilder.CurrentPlanSchemaVersion}; PlanId: {planId ?? "multiple"}; membership resolved/partial/outside/unresolved: {membershipResolved}/{membershipPartial}/{membershipOutside}/{membershipUnresolved}.");
|
||||
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Collections.Specialized;
|
||||
using System.Text;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
@@ -279,6 +280,8 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
|
||||
var membership = PlantViewportMembershipReader.Read(viewport, dataLinks, rowIds);
|
||||
|
||||
foreach (var rowId in rowIds)
|
||||
{
|
||||
try
|
||||
@@ -299,7 +302,9 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
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" }
|
||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" },
|
||||
MembershipStatus = membership.StatusFor(rowId),
|
||||
MembershipEvidenceCodes = membership.EvidenceFor(rowId)
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -381,6 +386,88 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -135,6 +135,31 @@ public sealed class FoundationTests
|
||||
Assert.Contains("VIEW_MEMBERSHIP_NOT_RESOLVED", plan.SkipReasons);
|
||||
}
|
||||
|
||||
[Fact] public void ViewportMembership_OutsideClip_IsSkippedWithStableReason()
|
||||
{
|
||||
var plan = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||
Assert.Contains("VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP", plan.SkipReasons);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_ResolvedMembership_AffectsPlanId()
|
||||
{
|
||||
var resolved = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||
var outside = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||
Assert.NotEqual(resolved.PlanId, outside.PlanId);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_ReorderedMembership_HasSamePlanId()
|
||||
{
|
||||
var first = Build(
|
||||
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved },
|
||||
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||
var second = Build(
|
||||
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView },
|
||||
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||
Assert.Equal(first.PlanId, second.PlanId);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||
{
|
||||
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||
@@ -233,6 +258,22 @@ public sealed class FoundationTests
|
||||
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
||||
}
|
||||
|
||||
[Fact] public void ViewportMembership_UsesConfirmedPnpIdentityContract()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.Contains("view.DwgQuery.Execute", source);
|
||||
Assert.Contains("sourceId.PrjObjectId", source);
|
||||
Assert.Contains("dataLinks.FindAcPpRowId", source);
|
||||
Assert.Contains("VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", source);
|
||||
}
|
||||
|
||||
[Fact] public void ViewportMembership_DoesNotSelectFirstViewport()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.DoesNotContain("DwgViewDefs.AsArray().First", source);
|
||||
Assert.Contains("candidates.Length != 1", source);
|
||||
}
|
||||
|
||||
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
||||
|
||||
Reference in New Issue
Block a user