// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Windows.Media.Imaging; using Autodesk.Revit.DB; using Autodesk.Revit.DB.Events; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; using Microsoft.Win32; namespace DatasmithRevitExporter { // Add-in external application Datasmith Revit Exporter. public class DatasmithRevitApplication : IExternalApplication { // Updater notifying us when 3D views got updated/added/deleted. public class View3DUpdater : IUpdater { static AddInId AppId; static UpdaterId AppUpdaterId; public View3DUpdater(AddInId InId) { AppId = InId; AppUpdaterId = new UpdaterId(AppId, new Guid("BE5DA5A4-73C2-42BB-9346-E54F632E2B54")); } public void Execute(UpdaterData InData) { if (InData.GetAddedElementIds().Count > 0 || InData.GetDeletedElementIds().Count > 0 || InData.GetModifiedElementIds().Count > 0) { Instance.SetViewList(ElementId.InvalidElementId); } } public string GetAdditionalInformation() { return ""; } public ChangePriority GetChangePriority() { return ChangePriority.FloorsRoofsStructuralWalls; } public UpdaterId GetUpdaterId() { return AppUpdaterId; } public string GetUpdaterName() { return "View3DUpdater"; } } private static DatasmithRevitExportMessages ExportMessagesDialog = null; private static string ExportMessages; private EventHandler DocumentClosingHandler; private EventHandler IdlingEventHandler; private EventHandler DocumentOpenedHandler; private EventHandler DocumentCreatedHandler; private EventHandler ViewActivatedHandler; private PushButton AutoSyncPushButton; private PushButton SyncPushButton; private View3DUpdater ViewsUpdater; private ElementId SelectedViewId = ElementId.InvalidElementId; private ComboBox ComboViews; private List All3DViews; BitmapImage AutoSyncIconOn_Small; BitmapImage AutoSyncIconOn_Large; BitmapImage AutoSyncIconOff_Small; BitmapImage AutoSyncIconOff_Large; private FDebugLog DebugLog; public DatasmithRevitApplication() { CreateDebugLog(); } public object Properties { get; private set; } public static DatasmithRevitApplication Instance { get; private set; } public void SetAutoSyncButtonToggled(bool bToggled) { if (bToggled) { AutoSyncPushButton.Image = AutoSyncIconOff_Small; AutoSyncPushButton.LargeImage = AutoSyncIconOff_Large; } else { AutoSyncPushButton.Image = AutoSyncIconOn_Small; AutoSyncPushButton.LargeImage = AutoSyncIconOn_Large; } SyncPushButton.Enabled = !bToggled; } private void OnComboBoxChanged(object s, Autodesk.Revit.UI.Events.ComboBoxCurrentChangedEventArgs e) { if (e.NewValue != null) { View3D SelectedView = All3DViews.Find(View => View.Name == e.NewValue.ItemText); if (SelectedView != null) { SelectedViewId = SelectedView.Id; FDocument.ActiveDocument?.SetActiveDirectLinkInstance(SelectedView); } } } private void ClearViews() { All3DViews.Clear(); foreach (ComboBoxMember Item in ComboViews.GetItems()) { Item.Visible = false; } SelectedViewId = ElementId.InvalidElementId; } private void SetViewList(ElementId InActiveViewId) { if (FDocument.ActiveDocument == null) { return; } // Cache prev selected view All3DViews = new FilteredElementCollector(FDocument.ActiveDocument.RevitDoc).OfClass(typeof(View3D)).Cast().ToList(); All3DViews.RemoveAll(view => (view.IsTemplate || !view.CanBePrinted)); // Combo box does not allow removal of items :). This is a workaround to always only add items and // reuse them. int ViewIndex = 0; for (; ViewIndex < All3DViews.Count; ++ViewIndex) { View3D View = All3DViews[ViewIndex]; var Items = ComboViews.GetItems(); if (Items.Count == ViewIndex) { ComboViews.AddItem(new ComboBoxMemberData($"View_{ViewIndex}", "ViewName")); } Items = ComboViews.GetItems(); ComboBoxMember Item = Items[ViewIndex]; Item.ItemText = View.Name; Item.Visible = true; } // Hide the excessive items for (; ViewIndex < ComboViews.GetItems().Count; ++ViewIndex) { ComboViews.GetItems()[ViewIndex].Visible = false; } if (InActiveViewId != ElementId.InvalidElementId) { SelectedViewId = InActiveViewId; } // Set the active view View3D ComboActiveView = null; if (SelectedViewId != ElementId.InvalidElementId) { ComboActiveView = All3DViews.Find(View => View.Id == SelectedViewId); } if (ComboActiveView == null) { ComboActiveView = All3DViews.Count > 0 ? All3DViews.First() : null; } if (ComboActiveView != null) { foreach (ComboBoxMember Item in ComboViews.GetItems()) { if (Item.ItemText == ComboActiveView.Name) { ComboViews.Current = Item; SelectedViewId = ComboActiveView.Id; FDocument.ActiveDocument?.SetActiveDirectLinkInstance(ComboActiveView); break; } } } } // Implement the interface to execute some tasks when Revit starts. public Result OnStartup( UIControlledApplication InApplication // handle to the application being started ) { Instance = this; // Create a custom ribbon tab string TabName = DatasmithRevitResources.Strings.DatasmithTabName; InApplication.CreateRibbonTab(TabName); // Add a new ribbon panel RibbonPanel DirectLinkRibbonPanel = InApplication.CreateRibbonPanel(TabName, DatasmithRevitResources.Strings.RibbonSection_DirectLink); RibbonPanel FileExportRibbonPanel = InApplication.CreateRibbonPanel(TabName, DatasmithRevitResources.Strings.RibbonSection_FileExport); RibbonPanel DatasmithRibbonPanel = InApplication.CreateRibbonPanel(TabName, DatasmithRevitResources.Strings.RibbonSection_Datasmith); string AssemblyPath = Assembly.GetExecutingAssembly().Location; PushButtonData ExportButtonData = new PushButtonData("Export3DView", DatasmithRevitResources.Strings.ButtonExport3DView, AssemblyPath, "DatasmithRevitExporter.DatasmithExportRevitCommand"); PushButtonData SyncButtonData = new PushButtonData("Sync3DView", DatasmithRevitResources.Strings.ButtonSync, AssemblyPath, "DatasmithRevitExporter.DatasmithSyncRevitCommand"); PushButtonData AutoSyncButtonData = new PushButtonData("AutoSync3DView", DatasmithRevitResources.Strings.ButtonAutoSync, AssemblyPath, "DatasmithRevitExporter.DatasmithAutoSyncRevitCommand"); PushButtonData ManageConnectionsButtonData = new PushButtonData("Connections", DatasmithRevitResources.Strings.ButtonConnections, AssemblyPath, "DatasmithRevitExporter.DatasmithManageConnectionsRevitCommand"); PushButtonData SettingsButtonData = new PushButtonData("Settings", DatasmithRevitResources.Strings.ButtonSettings, AssemblyPath, "DatasmithRevitExporter.DatasmithShowSettingsRevitCommand"); PushButtonData LogButtonData = new PushButtonData("Messages", DatasmithRevitResources.Strings.ButtonMessages, AssemblyPath, "DatasmithRevitExporter.DatasmithShowMessagesRevitCommand"); SyncPushButton = DirectLinkRibbonPanel.AddItem(SyncButtonData) as PushButton; AutoSyncPushButton = DirectLinkRibbonPanel.AddItem(AutoSyncButtonData) as PushButton; PushButton ManageConnectionsButton = DirectLinkRibbonPanel.AddItem(ManageConnectionsButtonData) as PushButton; PushButton ExportPushButton = FileExportRibbonPanel.AddItem(ExportButtonData) as PushButton; // Create the view sync combo ComboBoxData CbData = new ComboBoxData("ComboBoxViews"); PushButtonData LabelData = new PushButtonData("ComboLabel", "Select 3D view to sync", Assembly.GetExecutingAssembly().Location, "DatasmithRevitExporter.DatasmithSyncRevitCommand"); IList StackedItems = DatasmithRibbonPanel.AddStackedItems(LabelData, CbData); if (StackedItems.Count > 1) { // We emulate a label on the ribbon with disabled push button (the Revit way to do things) PushButton LabelButton = StackedItems[0] as PushButton; if (LabelButton != null) { LabelButton.ToolTip = "Select 3D view to sync"; LabelButton.Enabled = false; } ComboViews = StackedItems[1] as ComboBox; if (ComboViews != null) { ComboViews.CurrentChanged += OnComboBoxChanged; ComboViews.ItemText = "3D Views"; ComboViews.ToolTip = "Select 3D View to Sync"; } } PushButton ShowLogButton = DatasmithRibbonPanel.AddItem(LogButtonData) as PushButton; PushButton SettingsButton = DatasmithRibbonPanel.AddItem(SettingsButtonData) as PushButton; string DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithIcon"); ExportPushButton.Image = new BitmapImage(new Uri(DatasmithIconBase + "16.png")); ExportPushButton.LargeImage = new BitmapImage(new Uri(DatasmithIconBase + "32.png")); ExportPushButton.ToolTip = DatasmithRevitResources.Strings.ButtonExport3DViewHint; DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithSyncIcon"); SyncPushButton.Image = new BitmapImage(new Uri(DatasmithIconBase + "16.png")); SyncPushButton.LargeImage = new BitmapImage(new Uri(DatasmithIconBase + "32.png")); SyncPushButton.ToolTip = DatasmithRevitResources.Strings.ButtonSyncHint; DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithAutoSyncIcon"); AutoSyncIconOn_Small = new BitmapImage(new Uri(DatasmithIconBase + "On16.png")); AutoSyncIconOn_Large = new BitmapImage(new Uri(DatasmithIconBase + "On32.png")); AutoSyncIconOff_Small = new BitmapImage(new Uri(DatasmithIconBase + "Off16.png")); AutoSyncIconOff_Large = new BitmapImage(new Uri(DatasmithIconBase + "Off32.png")); AutoSyncPushButton.Image = AutoSyncIconOn_Small; AutoSyncPushButton.LargeImage = AutoSyncIconOn_Large; AutoSyncPushButton.ToolTip = DatasmithRevitResources.Strings.ButtonAutoSyncHint; DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithManageConnectionsIcon"); ManageConnectionsButton.Image = new BitmapImage(new Uri(DatasmithIconBase + "16.png")); ManageConnectionsButton.LargeImage = new BitmapImage(new Uri(DatasmithIconBase + "32.png")); ManageConnectionsButton.ToolTip = DatasmithRevitResources.Strings.ButtonConnectionsHint; DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithSettingsIcon"); SettingsButton.Image = new BitmapImage(new Uri(DatasmithIconBase + "16.png")); SettingsButton.LargeImage = new BitmapImage(new Uri(DatasmithIconBase + "32.png")); SettingsButton.ToolTip = DatasmithRevitResources.Strings.ButtonSettingsHint; DatasmithIconBase = Path.Combine(Path.GetDirectoryName(AssemblyPath), "DatasmithLogIcon"); ShowLogButton.Image = new BitmapImage(new Uri(DatasmithIconBase + "16.png")); ShowLogButton.LargeImage = new BitmapImage(new Uri(DatasmithIconBase + "32.png")); ShowLogButton.ToolTip = DatasmithRevitResources.Strings.ButtonMessagesHint; DocumentOpenedHandler = new EventHandler(OnDocumentOpened); InApplication.ControlledApplication.DocumentOpened += DocumentOpenedHandler; DocumentCreatedHandler = new EventHandler(OnDocumentCreated); InApplication.ControlledApplication.DocumentCreated += DocumentCreatedHandler; DocumentClosingHandler = new EventHandler(OnDocumentClosing); InApplication.ControlledApplication.DocumentClosing += DocumentClosingHandler; IdlingEventHandler = new EventHandler(OnIdling); InApplication.Idling += IdlingEventHandler; ViewActivatedHandler = new EventHandler(OnViewActivated); InApplication.ViewActivated += ViewActivatedHandler; // Setup Direct Link string RevitEngineDir = null; try { using (RegistryKey Key = Registry.LocalMachine.OpenSubKey("Software\\Wow6432Node\\EpicGames\\Unreal Engine")) { RevitEngineDir = Key?.GetValue("RevitEngineDir") as string; } } finally { if (RevitEngineDir == null) { // If we could not read the registry, fallback to hardcoded engine dir RevitEngineDir = "C:\\ProgramData\\Epic\\Exporter\\RevitEngine\\"; } } bool bDirectLinkInitOk = FDatasmithFacadeDirectLink.Init(true, RevitEngineDir); Debug.Assert(bDirectLinkInitOk); // Register updater to react to view modification ViewsUpdater = new View3DUpdater(InApplication.ControlledApplication.ActiveAddInId); UpdaterRegistry.RegisterUpdater(ViewsUpdater); ElementCategoryFilter Filter = new ElementCategoryFilter( BuiltInCategory.OST_Views); UpdaterRegistry.AddTrigger(ViewsUpdater.GetUpdaterId(), Filter, Element.GetChangeTypeAny()); UpdaterRegistry.AddTrigger(ViewsUpdater.GetUpdaterId(), Filter, Element.GetChangeTypeElementAddition()); UpdaterRegistry.AddTrigger(ViewsUpdater.GetUpdaterId(), Filter, Element.GetChangeTypeElementDeletion()); return Result.Succeeded; } void OnIdling(object Sender, IdlingEventArgs Args) { FDirectLink.OnApplicationIdle(); } static void OnDocumentOpened(object sender, DocumentOpenedEventArgs e) { FDocument.SetActiveDocument(e.Document); Instance.SetViewList(FDocument.ActiveDocument?.Settings.SyncViewId ?? ElementId.InvalidElementId); } static void OnDocumentCreated(object sender, DocumentCreatedEventArgs e) { FDocument.SetActiveDocument(e.Document); Instance.SetViewList(ElementId.InvalidElementId); } static void OnDocumentClosing(object sender, DocumentClosingEventArgs e) { Instance.ClearViews(); FDocument.Destroy(e.Document); } static void OnViewActivated(object sender, ViewActivatedEventArgs e) { View Previous = e.PreviousActiveView; View Current = e.CurrentActiveView; if (Previous != null && !Previous.Document.Equals(Current.Document)) { FDocument.SetActiveDocument(e.Document); Instance.SetViewList(FDocument.ActiveDocument?.Settings.SyncViewId ?? ElementId.InvalidElementId); } } // Implement the interface to execute some tasks when Revit shuts down. public Result OnShutdown( UIControlledApplication InApplication // handle to the application being shut down ) { FDocument.DestroyAll(); InApplication.ControlledApplication.DocumentClosing -= DocumentClosingHandler; InApplication.ControlledApplication.DocumentOpened -= DocumentOpenedHandler; InApplication.ControlledApplication.DocumentCreated -= DocumentCreatedHandler; InApplication.ViewActivated -= ViewActivatedHandler; DocumentClosingHandler = null; DocumentOpenedHandler = null; DocumentCreatedHandler = null; ViewActivatedHandler = null; UpdaterRegistry.UnregisterUpdater(ViewsUpdater.GetUpdaterId()); ViewsUpdater = null; if (ExportMessagesDialog != null && !ExportMessagesDialog.IsDisposed) { ExportMessagesDialog.Close(); } FDatasmithFacadeDirectLink.Shutdown(); return Result.Succeeded; } public static void SetExportMessages(string InMessages) { ExportMessages = InMessages; if (ExportMessagesDialog != null) { ExportMessagesDialog.Messages = ExportMessages; } } public static void ShowExportMessages(ExternalCommandData InCommandData) { if (ExportMessagesDialog == null || ExportMessagesDialog.IsDisposed) { int CenterX = (InCommandData.Application.MainWindowExtents.Left + InCommandData.Application.MainWindowExtents.Right) / 2; int CenterY = (InCommandData.Application.MainWindowExtents.Top + InCommandData.Application.MainWindowExtents.Bottom) / 2; ExportMessagesDialog = new DatasmithRevitExportMessages(new System.Drawing.Point(CenterX, CenterY), () => ExportMessages = ""); ExportMessagesDialog.Messages = ExportMessages; ExportMessagesDialog.Show(); } else { ExportMessagesDialog.Focus(); } } public static bool IsPreHandshakeRevitBuild(string VersionBuild) { #if REVIT_API_2023 return Version.Parse(VersionBuild) < Version.Parse("23.1"); #else return true; #endif } [Conditional("DatasmithRevitDebugOutput")] private void CreateDebugLog() { DebugLog = new FDebugLog(); } [Conditional("DatasmithRevitDebugOutput")] public void LogDebug(string Message) { DebugLog.LogDebug(Message); } } class FDebugLog { private ConcurrentQueue MessagesQueue = new ConcurrentQueue(); private Thread LogWriterThread; public FDebugLog() { LogWriterThread = new Thread(() => { LogWriterProc(); }); LogWriterThread.Start(); } private void LogWriterProc() { string LogPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UnrealDatasmithExporter/Saved/Logs/UnrealDatasmithRevitExporterDebug.log"); StreamWriter LogFile = new StreamWriter(LogPath); while (true) { while (MessagesQueue.TryDequeue(out string Message)) { LogFile.WriteLine(Message); } LogFile.Flush(); Thread.Sleep(10); } } public void LogDebug(string Message) { MessagesQueue.Enqueue(Message); } }; }