Files
2025-05-18 13:04:45 +08:00

505 lines
17 KiB
C#

// 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<DocumentClosingEventArgs> DocumentClosingHandler;
private EventHandler<IdlingEventArgs> IdlingEventHandler;
private EventHandler<DocumentOpenedEventArgs> DocumentOpenedHandler;
private EventHandler<DocumentCreatedEventArgs> DocumentCreatedHandler;
private EventHandler<ViewActivatedEventArgs> ViewActivatedHandler;
private PushButton AutoSyncPushButton;
private PushButton SyncPushButton;
private View3DUpdater ViewsUpdater;
private ElementId SelectedViewId = ElementId.InvalidElementId;
private ComboBox ComboViews;
private List<View3D> 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<View3D>().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<RibbonItem> 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<DocumentOpenedEventArgs>(OnDocumentOpened);
InApplication.ControlledApplication.DocumentOpened += DocumentOpenedHandler;
DocumentCreatedHandler = new EventHandler<DocumentCreatedEventArgs>(OnDocumentCreated);
InApplication.ControlledApplication.DocumentCreated += DocumentCreatedHandler;
DocumentClosingHandler = new EventHandler<DocumentClosingEventArgs>(OnDocumentClosing);
InApplication.ControlledApplication.DocumentClosing += DocumentClosingHandler;
IdlingEventHandler = new EventHandler<IdlingEventArgs>(OnIdling);
InApplication.Idling += IdlingEventHandler;
ViewActivatedHandler = new EventHandler<ViewActivatedEventArgs>(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<string> MessagesQueue = new ConcurrentQueue<string>();
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);
}
};
}