From e707456ab63c5983359fbd07d8e2fd8f68cf6b09 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 4 Aug 2026 14:25:44 +0300 Subject: [PATCH] fix: retain prepared Ortho context for script PXP-OA --- .../src/PRecAnnot.Plant3D2026/EntryPoint.cs | 7 +++-- .../ScriptDrivenFirstApplyAutomation.cs | 31 ++++++++++++++++++- .../PxpOaRegistrationTests.cs | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index 0037c93..5e70872 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -77,7 +77,9 @@ public sealed class EntryPoint : IExtensionApplication // document lags behind MDI activation. Bind the planner to the // command document captured above, which is also the observer's // fixture identity. - var context = new PlantRuntimeContextReader().ReadCurrentContext(document); + var context = ScriptDrivenFirstApplyAutomation.TryGetPreparedContext(document, out var scriptContext) + ? scriptContext + : new PlantRuntimeContextReader().ReadCurrentContext(document); var details = new Dictionary { ["ExecutionMode"] = mode.ToString(), @@ -113,7 +115,8 @@ public sealed class EntryPoint : IExtensionApplication var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null ? FirstLimitedApplyPermitReader.ReadFor(document) : null; - var automationTarget = FirstLimitedApplyAutomationCoordinator.SelectedTargetSnapshotId; + var automationTarget = ScriptDrivenFirstApplyAutomation.SelectedTargetSnapshotId + ?? FirstLimitedApplyAutomationCoordinator.SelectedTargetSnapshotId; var limitedTarget = applyPermit?.TargetSnapshotId ?? automationTarget; if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1) throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT"); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ScriptDrivenFirstApplyAutomation.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ScriptDrivenFirstApplyAutomation.cs index c8a68ba..b617ce0 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ScriptDrivenFirstApplyAutomation.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/ScriptDrivenFirstApplyAutomation.cs @@ -6,6 +6,7 @@ using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; using Autodesk.ProcessPower.PlantInstance; using Autodesk.ProcessPower.ProjectManager; +using PRecAnnot.Abstractions; using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application; namespace PRecAnnot.Plant3D2026; @@ -26,6 +27,11 @@ internal static class ScriptDrivenFirstApplyAutomation private static readonly List sentinels = []; private static readonly List correlations = []; private static PxpOaDiscoveryEvidence? discovery; + // Plant's implicit current-document lookup can lag between script commands. + // PREPARE has already proved this immutable context belongs to the live fixture; + // retain it for the two immediately following public PXP-OA commands only. + private static PlantRuntimeContext? preparedContext; + private static string? selectedTargetSnapshotId; private static bool prepared; private static bool terminal; @@ -53,6 +59,12 @@ internal static class ScriptDrivenFirstApplyAutomation var context = new PlantRuntimeContextReader().ReadCurrentContext(fixture); if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1) throw new InvalidOperationException("AUTOMATION_ORTHO_CONTEXT_INVALID"); + var candidates = new PlantObjectReader().ReadObjects(context.Viewports.Single(), CancellationToken.None) + .Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null) + .Select(x => x.SnapshotId).OrderBy(x => x, StringComparer.Ordinal).ToArray(); + if (candidates.Length == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE"); + preparedContext = context; + selectedTargetSnapshotId = candidates[0]; dbmodBaseline = Dbmod(); baseline = Counts(database); prepared = true; @@ -81,6 +93,18 @@ internal static class ScriptDrivenFirstApplyAutomation runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, SourceRows = sourceRows, MembershipResolved = membershipResolved }; } + internal static bool TryGetPreparedContext(Document? commandDocument, out PlantRuntimeContext context) + { + context = null!; + if (!prepared || terminal || fixture is null || preparedContext is null || commandDocument is null || config is null) return false; + if (!SamePath(commandDocument.Name, config.OrthoDrawingPath) || !SamePath(fixture.Name, config.OrthoDrawingPath)) return false; + if (commandDocument.Database is null || database is null || !SamePath(commandDocument.Database.Filename, database.Filename)) return false; + context = preparedContext; + return true; + } + + internal static string? SelectedTargetSnapshotId => prepared && !terminal ? selectedTargetSnapshotId : null; + internal static void FinalizePreview() { try @@ -149,7 +173,12 @@ internal static class ScriptDrivenFirstApplyAutomation } private static void Guard(Action action) { try { if (!terminal) action(); } catch (System.Exception ex) { Fail("AUTOMATION_EVENT_HANDLER_FAILED", ex); } } private static void Fail(string code, System.Exception exception) { if (terminal) return; terminal = true; transitions.Add("Failed:" + code); try { Write(false, code + ":" + exception); } finally { Unsubscribe(); } } - private static void Unsubscribe() { if (fixture is null) return; try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { } } + private static void Unsubscribe() + { + if (fixture is not null) try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { } + preparedContext = null; + selectedTargetSnapshotId = null; + } private static int? Dbmod() { try { return Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD")); } catch { return null; } } private static AutomationEntityCounts Counts(Database db) { diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PxpOaRegistrationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PxpOaRegistrationTests.cs index fac4c70..3a1ee1c 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PxpOaRegistrationTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PxpOaRegistrationTests.cs @@ -34,4 +34,6 @@ public sealed class PxpOaRegistrationTests [Fact] public void Observer_RecordsExactlyTwoSequentialRuns() => Assert.Contains("runs.Count < 2", Observer); [Fact] public void Finalize_ReportsMissingPlanSeparatelyFromMissingRuns() => Assert.True(Observer.IndexOf("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA", StringComparison.Ordinal) < Observer.IndexOf("AUTOMATION_PLAN_IDS_DIFFER", StringComparison.Ordinal)); [Fact] public void ScriptDrivenPxpOa_BindsPlantContextToCommandDocument() => Assert.Contains("ReadCurrentContext(document)", Entry); + [Fact] public void ScriptDrivenPxpOa_UsesPreparedFixtureContextBeforeImplicitLookup() => Assert.Contains("TryGetPreparedContext(document, out var scriptContext)", Entry); + [Fact] public void ScriptDrivenPrepare_SelectsCanonicalTargetAndRetainsReadOnlyContext() { Assert.Contains("preparedContext = context", Observer); Assert.Contains("selectedTargetSnapshotId = candidates[0]", Observer); } }