diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs
index 9297ce2..cf769b6 100644
--- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs
@@ -52,6 +52,7 @@ public sealed class EntryPoint : IExtensionApplication
[CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport();
[CommandMethod("PXP-UOB")] public void UpdateOrthoViewBlocksCommand() => PRecAnnot.Plant3D2026.UpdateOrthoViewBlocksCommand.Run();
[CommandMethod("PXP-AC", CommandFlags.UsePickSet)] public void SwapAttributesCommand() => PRecAnnot.Plant3D2026.SwapAttributesCommand.Run();
+ [CommandMethod("PXP-IOD", CommandFlags.Session)] public void UpdateOrthoDrawingsCommand() => PRecAnnot.Plant3D2026.UpdateOrthoDrawingsCommand.Run();
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj
index 3c844df..ba967fd 100644
--- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj
@@ -20,5 +20,6 @@
$(Plant3D2026ManagedDir)\PnP3dOrthoDrawingsUI.dllfalsefalse
$(Plant3D2026ManagedDir)\PnPDwg2dDef.dllfalsefalse
$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dllfalsefalse
+ $(Plant3D2026ManagedDir)\PnPDwg2dGen.dllfalsefalse
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoDrawingsCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoDrawingsCommand.cs
new file mode 100644
index 0000000..0f12d53
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/UpdateOrthoDrawingsCommand.cs
@@ -0,0 +1,148 @@
+using System.Collections;
+using System.Collections.Specialized;
+using Autodesk.AutoCAD.ApplicationServices;
+using Autodesk.AutoCAD.DatabaseServices;
+using Autodesk.AutoCAD.EditorInput;
+using Autodesk.AutoCAD.Geometry;
+using Autodesk.ProcessPower.Drawings2d;
+using Autodesk.ProcessPower.PlantInstance;
+using Autodesk.ProcessPower.P3dProjectParts;
+using Autodesk.ProcessPower.PnP3dOrthoDrawingsUI;
+using Autodesk.ProcessPower.ProjectManager;
+using PRecAnnot.Abstractions;
+using PRecAnnot.Core;
+using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
+
+namespace PRecAnnot.Plant3D2026;
+
+internal static class UpdateOrthoDrawingsCommand
+{
+ public static void Run()
+ {
+ var owner = AcadApplication.DocumentManager.MdiActiveDocument;
+ if (owner is null) return;
+ try
+ {
+ var project = PlantApplication.CurrentProject ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_REQUIRED");
+ var ortho = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_PART_REQUIRED");
+ var drawings = ReadDrawings(project, ortho);
+ var selections = PromptSelections(owner.Editor, drawings);
+ if (selections.Count == 0) return;
+ var plan = new OrthoBatchPlanner().Build(drawings, selections);
+ foreach (var diagnostic in plan.Diagnostics) owner.Editor.WriteMessage("\nPXP-IOD: " + diagnostic);
+ owner.Editor.WriteMessage($"\nPXP-IOD preview: drawings={plan.Operations.Select(value => value.RelativePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}; views={plan.Operations.Count}; no drawings modified.");
+ RuntimeDiagnostics.Write("UpdateOrthoDrawingsPreviewCompleted", commandName: "PXP-IOD", resultStatus: "PreviewCompleted", document: owner, details: new Dictionary { ["DrawingCount"] = plan.Operations.Select(value => value.RelativePath).Distinct(StringComparer.OrdinalIgnoreCase).Count(), ["ViewCount"] = plan.Operations.Count });
+ if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
+ var fixtureRoot = ValidateFixture();
+ var failures = Apply(project, ortho, plan, fixtureRoot);
+ owner.Editor.WriteMessage($"\nPXP-IOD Apply completed: views={plan.Operations.Count}; failures={failures.Count}.");
+ RuntimeDiagnostics.Write("UpdateOrthoDrawingsApplyCompleted", commandName: "PXP-IOD", resultStatus: failures.Count == 0 ? "Applied" : "PartiallyApplied", document: owner, details: new Dictionary { ["FailureCount"] = failures.Count, ["Failures"] = failures.ToArray() });
+ }
+ catch (Exception exception)
+ {
+ RuntimeDiagnostics.Write("UpdateOrthoDrawingsFailed", "ERROR", commandName: "PXP-IOD", resultStatus: exception.Message, exception: exception, document: owner);
+ owner.Editor.WriteMessage("\nPXP-IOD: " + exception.Message);
+ }
+ }
+
+ private static IReadOnlyList ReadDrawings(PlantProject project, OrthoProject ortho)
+ {
+ var root = Path.GetDirectoryName(project.FileName) ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_ROOT_UNAVAILABLE");
+ return ortho.DwgSheetDefinitions.AsArray().Select(sheet =>
+ {
+ var path = UIUtils.GetDwgSheetDefResolvedPath(sheet);
+ var relative = Path.GetRelativePath(root, path);
+ return new OrthoBatchDrawing(sheet.DwgGuid.ToString(), Path.GetFileName(path), relative, sheet.DwgViewDefs.AsArray().Select(view => view.ViewName).ToArray());
+ }).OrderBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray();
+ }
+
+ private static IReadOnlyList PromptSelections(Editor editor, IReadOnlyList drawings)
+ {
+ var result = new List();
+ foreach (var drawing in drawings)
+ {
+ var prompt = new PromptKeywordOptions($"\nUpdate Ortho drawing {drawing.RelativePath}?") { AllowNone = true };
+ prompt.Keywords.Add("Yes"); prompt.Keywords.Add("No"); prompt.Keywords.Default = "No";
+ var answer = editor.GetKeywords(prompt);
+ if (answer.Status == PromptStatus.Cancel) return Array.Empty();
+ if (string.Equals(answer.StringResult, "Yes", StringComparison.OrdinalIgnoreCase)) result.Add(new(drawing.RelativePath, drawing.Views));
+ }
+ return result;
+ }
+
+ private static IReadOnlyList Apply(PlantProject project, OrthoProject ortho, OrthoBatchPlan plan, string fixtureRoot)
+ {
+ var projectRoot = Path.GetDirectoryName(project.FileName)!;
+ var failures = new List();
+ foreach (var group in plan.Operations.GroupBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase))
+ {
+ var path = Path.GetFullPath(Path.Combine(projectRoot, group.Key));
+ if (!path.StartsWith(fixtureRoot, StringComparison.OrdinalIgnoreCase)) { failures.Add(group.Key + ":OUTSIDE_FIXTURE"); continue; }
+ Document? document = AcadApplication.DocumentManager.Cast().FirstOrDefault(value => string.Equals(Path.GetFullPath(value.Name), path, StringComparison.OrdinalIgnoreCase));
+ var wasOpen = document is not null;
+ try
+ {
+ if (document is null) { UIUtils.OpenDocument(path, out document, false); document.Database.CloseInput(true); }
+ AcadApplication.DocumentManager.MdiActiveDocument = document;
+ using (document.LockDocument()) foreach (var operation in group) UpdateView(document, ortho, operation.ViewName);
+ if (wasOpen) document.Database.SaveAs(document.Name, true, DwgVersion.Current, document.Database.SecurityParameters); else document.CloseAndSave(path);
+ }
+ catch (Exception exception)
+ {
+ failures.Add(group.Key + ":" + exception.GetType().Name);
+ if (!wasOpen && document is not null) try { document.CloseAndDiscard(); } catch { }
+ }
+ }
+ return failures;
+ }
+
+ private static void UpdateView(Document document, OrthoProject ortho, string viewName)
+ {
+ if (!UIUtils.GetDwgSheetFromOrthoDrawingName(document.Database.Filename, out IDwgSheetDef sheet) || !UIUtils.GetViewDefByViewName(viewName, sheet, out IDwgViewDef definition))
+ throw new InvalidOperationException("ORTHO_BATCH_VIEW_NOT_FOUND:" + viewName);
+ var viewportId = FindViewport(document.Database, definition);
+ var frozen = ReadFrozenLayers(document.Database, viewportId); var off = new StringCollection();
+ if (!UIUtils.IsValidViewpport(viewportId, out double width, out double height, out double scale, out Point3d center) || !definition.SetLayout(width, height, ref center))
+ throw new InvalidOperationException("ORTHO_BATCH_VIEW_LAYOUT_INVALID:" + viewName);
+ definition.ViewScale = scale;
+ var resolved = new StringCollection(); resolved.AddRange(UIUtils.GetResolvedRefModels(definition, false).Select(value => value.ModelFileName).ToArray());
+ var models = new ArrayList(); UIUtils.UpdateFileCollectionWithXrefs(resolved, ref models, out Extents3d extents);
+ definition.DwgQuery.ModelFiles = models.ToArray(typeof(ModelFile)) as ModelFile[];
+ definition.DwgQuery.Execute(frozen, off);
+ UIUtils.CreateOrUpdateOrthoView(null!, sheet, viewName, ortho, false, true, Dwg2dGen.NewDwgSetClipper(definition, 0), 0, frozen, off, new Vector3d(), 0, new Vector3d(), null!);
+ definition.ViewCreationTime = DateTime.UtcNow.ToString("O");
+ }
+
+ private static ObjectId FindViewport(Database database, IDwgViewDef definition)
+ {
+ using var transaction = database.TransactionManager.StartOpenCloseTransaction();
+ var layouts = (DBDictionary)transaction.GetObject(database.LayoutDictionaryId, OpenMode.ForRead);
+ foreach (DBDictionaryEntry entry in layouts)
+ {
+ var layout = (Layout)transaction.GetObject(entry.Value, OpenMode.ForRead);
+ if (layout.ModelType) continue;
+ var paper = (BlockTableRecord)transaction.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
+ foreach (ObjectId id in paper)
+ if (transaction.GetObject(id, OpenMode.ForRead) is Viewport viewport && viewport.Number > 1 &&
+ Math.Abs(viewport.CenterPoint.X - definition.CenterPoint.X) < .001 && Math.Abs(viewport.CenterPoint.Y - definition.CenterPoint.Y) < .001 &&
+ Math.Abs(viewport.Width - definition.ViewWidth) < .001 && Math.Abs(viewport.Height - definition.ViewHeight) < .001) return id;
+ }
+ throw new InvalidOperationException("ORTHO_BATCH_VIEWPORT_NOT_FOUND:" + definition.ViewName);
+ }
+
+ private static StringCollection ReadFrozenLayers(Database database, ObjectId viewportId)
+ {
+ var result = new StringCollection();
+ using var transaction = database.TransactionManager.StartOpenCloseTransaction();
+ var viewport = (Viewport)transaction.GetObject(viewportId, OpenMode.ForRead);
+ foreach (ObjectId id in viewport.GetFrozenLayers()) if (transaction.GetObject(id, OpenMode.ForRead) is LayerTableRecord layer) result.Add(layer.Name);
+ return result;
+ }
+
+ private static string ValidateFixture()
+ {
+ var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_BATCH_FIXTURE_MARKER");
+ if (string.IsNullOrWhiteSpace(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_BATCH_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_BATCH_TEST_FIXTURE_REQUIRED");
+ return Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar;
+ }
+}