初始提交: UE5.3项目基础框架

This commit is contained in:
2025-10-14 11:14:54 +08:00
commit 721d9fd98e
5334 changed files with 316782 additions and 0 deletions

View File

@ -0,0 +1,41 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "Cesium3DTilesetCustomization.h"
#include "Cesium3DTileset.h"
#include "CesiumCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
FName FCesium3DTilesetCustomization::RegisteredLayoutName;
void FCesium3DTilesetCustomization::Register(
FPropertyEditorModule& PropertyEditorModule) {
RegisteredLayoutName = ACesium3DTileset::StaticClass()->GetFName();
PropertyEditorModule.RegisterCustomClassLayout(
RegisteredLayoutName,
FOnGetDetailCustomizationInstance::CreateStatic(
&FCesium3DTilesetCustomization::MakeInstance));
}
void FCesium3DTilesetCustomization::Unregister(
FPropertyEditorModule& PropertyEditorModule) {
PropertyEditorModule.UnregisterCustomClassLayout(RegisteredLayoutName);
}
TSharedRef<IDetailCustomization> FCesium3DTilesetCustomization::MakeInstance() {
return MakeShareable(new FCesium3DTilesetCustomization);
}
void FCesium3DTilesetCustomization::CustomizeDetails(
IDetailLayoutBuilder& DetailBuilder) {
DetailBuilder.SortCategories(&SortCustomDetailsCategories);
}
void FCesium3DTilesetCustomization::SortCustomDetailsCategories(
const TMap<FName, IDetailCategoryBuilder*>& AllCategoryMap) {
(*AllCategoryMap.Find(FName("TransformCommon")))->SetSortOrder(0);
(*AllCategoryMap.Find(FName("Cesium")))->SetSortOrder(1);
(*AllCategoryMap.Find(FName("Rendering")))->SetSortOrder(2);
}

View File

@ -0,0 +1,28 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "IDetailCustomization.h"
#include "PropertyEditorModule.h"
class IDetailCategoryBuilder;
/**
* An implementation of the IDetailCustomization interface that customizes
* the Details View of a Cesium3DTileset. It is registered in
* FCesiumEditorModule::StartupModule.
*/
class FCesium3DTilesetCustomization : public IDetailCustomization {
public:
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
static void Register(FPropertyEditorModule& PropertyEditorModule);
static void Unregister(FPropertyEditorModule& PropertyEditorModule);
static TSharedRef<IDetailCustomization> MakeInstance();
static void SortCustomDetailsCategories(
const TMap<FName, IDetailCategoryBuilder*>& AllCategoryMap);
static FName RegisteredLayoutName;
};

View File

@ -0,0 +1,62 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumCommands.h"
#include "CesiumEditor.h"
#define LOCTEXT_NAMESPACE "CesiumCommands"
FCesiumCommands::FCesiumCommands()
: TCommands<FCesiumCommands>(
"Cesium.Common",
LOCTEXT("Common", "Common"),
NAME_None,
FCesiumEditorModule::GetStyleSetName()) {}
void FCesiumCommands::RegisterCommands() {
UI_COMMAND(
AddFromIon,
"Add",
"Add a tileset from Cesium ion to this level",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
UploadToIon,
"Upload",
"Upload a tileset to Cesium ion to process it for efficient streaming to Cesium for Unreal",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
SignOut,
"Sign Out",
"Sign out of Cesium ion",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
OpenDocumentation,
"Learn",
"Open Cesium for Unreal tutorials and learning resources",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
OpenSupport,
"Help",
"Search for existing questions or ask a new question on the Cesium Community Forum",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
OpenTokenSelector,
"Token",
"Select or create a token to use to access Cesium ion assets. "
"Not used for ion servers that don't need token authentication.",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(
OpenCesiumPanel,
"Cesium",
"Open the Cesium panel",
EUserInterfaceActionType::Button,
FInputChord());
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,22 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CoreMinimal.h"
#include "Framework/Commands/Commands.h"
class FCesiumCommands : public TCommands<FCesiumCommands> {
public:
FCesiumCommands();
TSharedPtr<FUICommandInfo> AddFromIon;
TSharedPtr<FUICommandInfo> UploadToIon;
TSharedPtr<FUICommandInfo> SignOut;
TSharedPtr<FUICommandInfo> OpenDocumentation;
TSharedPtr<FUICommandInfo> OpenSupport;
TSharedPtr<FUICommandInfo> OpenTokenSelector;
TSharedPtr<FUICommandInfo> OpenCesiumPanel;
virtual void RegisterCommands() override;
};

View File

@ -0,0 +1,110 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "IDetailGroup.h"
#include "ScopedTransaction.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SWrapBox.h"
#include "Widgets/Text/STextBlock.h"
struct CesiumButtonGroup::Impl {
TSharedPtr<SWrapBox> Container;
TArray<TWeakObjectPtr<UObject>> SelectedObjects;
FTextBuilder ButtonSearchText;
};
CesiumButtonGroup::CesiumButtonGroup() : pImpl(MakeUnique<Impl>()) {
this->pImpl->Container = SNew(SWrapBox).UseAllottedSize(true);
}
void CesiumButtonGroup::AddButtonForUFunction(
UFunction* Function,
const FText& Label) {
check(Function);
if (!Function)
return;
FText ButtonCaption =
Label.IsEmpty()
? FText::FromString(
FName::NameToDisplayString(*Function->GetName(), false))
: Label;
FText ButtonTooltip = Function->GetToolTipText();
this->pImpl->ButtonSearchText.AppendLine(ButtonCaption);
this->pImpl->ButtonSearchText.AppendLine(ButtonTooltip);
TWeakObjectPtr<UFunction> WeakFunctionPtr = Function;
pImpl->Container->AddSlot()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(
0.0f,
3.0f,
0.0f,
3.0f)[SNew(SButton)
.Text(ButtonCaption)
.OnClicked_Lambda([WeakFunctionPtr,
ButtonCaption,
Group = this->AsShared()]() {
if (UFunction* Function = WeakFunctionPtr.Get()) {
FScopedTransaction Transaction(ButtonCaption);
FEditorScriptExecutionGuard ScriptGuard;
for (TWeakObjectPtr<UObject> SelectedObjectPtr :
Group->pImpl->SelectedObjects) {
if (UObject* Object = SelectedObjectPtr.Get()) {
Object->Modify();
Object->ProcessEvent(Function, nullptr);
}
}
}
return FReply::Handled();
})
.ToolTipText(ButtonTooltip)];
}
void CesiumButtonGroup::Finish(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category) {
DetailBuilder.GetObjectsBeingCustomized(this->pImpl->SelectedObjects);
Category.AddCustomRow(this->pImpl->ButtonSearchText.ToText())
.RowTag("Actions")[this->pImpl->Container.ToSharedRef()];
}
IDetailGroup& CesiumCustomization::CreateGroup(
IDetailCategoryBuilder& Category,
FName GroupName,
const FText& LocalizedDisplayName,
bool bForAdvanced,
bool bStartExpanded) {
IDetailGroup& Group = Category.AddGroup(
GroupName,
LocalizedDisplayName,
bForAdvanced,
bStartExpanded);
Group.HeaderRow()
.WholeRowContent()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "NoBorder")
.ContentPadding(FMargin(0, 2, 0, 2))
.OnClicked_Lambda([&Group]() {
Group.ToggleExpansion(!Group.GetExpansionState());
return FReply::Handled();
})
.ForegroundColor(FSlateColor::UseForeground())
.Content()[SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LocalizedDisplayName)]
];
return Group;
}
TSharedPtr<CesiumButtonGroup> CesiumCustomization::CreateButtonGroup() {
return MakeShared<CesiumButtonGroup>();
}

View File

@ -0,0 +1,61 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Internationalization/Text.h"
#include "UObject/NameTypes.h"
class IDetailCategoryBuilder;
class IDetailGroup;
class IDetailLayoutBuilder;
class CesiumButtonGroup : public TSharedFromThis<CesiumButtonGroup> {
public:
CesiumButtonGroup();
/// <summary>
/// Adds a button to this group. When pressed, the button will invoke the
/// provided UFunction. If a name is not specified, the label on the button is
/// derived automatically from the name of the function.
/// </summary>
/// <param name="Function"></param>
void AddButtonForUFunction(
UFunction* Function,
const FText& Label = FText::GetEmpty());
void
Finish(IDetailLayoutBuilder& DetailBuilder, IDetailCategoryBuilder& Category);
private:
struct Impl;
TUniquePtr<Impl> pImpl;
};
class CesiumCustomization {
public:
/// <summary>
/// Adds a new group to the given category. This is equivalent to calling
/// `Category.AddGroup` except that it allows the label to span the entire row
/// rather than confining it to the "name" column for no apparent reason.
/// </summary>
/// <param name="Category">The category to which to add the group.</param>
/// <param name="GroupName">The name of the group.</param>
/// <param name="LocalizedDisplayName">The display name of the group.</param>
/// <param name="bForAdvanced">True if the group should appear in the advanced
/// section of the category.</param>
/// <param name="bStartExpanded">True if the group should start
/// expanded.</param>
/// <returns>The newly-created group.</returns>
static IDetailGroup& CreateGroup(
IDetailCategoryBuilder& Category,
FName GroupName,
const FText& LocalizedDisplayName,
bool bForAdvanced = false,
bool bStartExpanded = false);
/// <summary>
/// Creates a new group of action buttons. Be sure to call Finish on the
/// returned instance after the last button has been added.
/// </summary>
static TSharedPtr<CesiumButtonGroup> CreateButtonGroup();
};

View File

@ -0,0 +1,310 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumDegreesMinutesSecondsEditor.h"
#include "CesiumEditor.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Input/STextComboBox.h"
#include "Widgets/Text/STextBlock.h"
#include <glm/glm.hpp>
namespace {
/**
* @brief A structure describing cartographic coordinates in
* the DMS (Degree-Minute-Second) representation.
*/
struct DMS {
/**
* @brief The degrees.
*
* This is usually a value in [0,90] (for latitude) or
* in [0,180] (for longitude), although explicit
* clamping is not guaranteed.
*/
int32_t d;
/**
* @brief The minutes.
*
* This is a value in [0,60).
*/
int32_t m;
/**
* @brief The seconds.
*
* This is a value in [0,60).
*/
double s;
/**
* @brief Whether the coordinate is negative.
*
* When the coordinate is negative, it represents a latitude south
* of the equator, or a longitude west of the prime meridian.
*/
bool negative;
};
/**
* @brief Converts the given decimal degrees to a DMS representation.
*
* @param decimalDegrees The decimal degrees
* @return The DMS representation.
*/
DMS decimalDegreesToDms(double decimalDegrees) {
// Roughly based on
// https://en.wikiversity.org/wiki/Geographic_coordinate_conversion
// Section "Conversion_from_Decimal_Degree_to_DMS"
bool negative = decimalDegrees < 0;
double dd = negative ? -decimalDegrees : decimalDegrees;
double d = glm::floor(dd);
double min = (dd - d) * 60;
double m = glm::floor(min);
double sec = (min - m) * 60;
double s = sec;
if (s >= 60) {
m++;
s -= 60;
}
if (m == 60) {
d++;
m = 0;
}
return DMS{static_cast<int32_t>(d), static_cast<int32_t>(m), s, negative};
}
/**
* @brief Converts the given DMS into decimal degrees.
*
* @param dms The DMS
* @return The decimal degrees
*/
double dmsToDecimalDegrees(const DMS& dms) {
double dd = dms.d + (dms.m / 60.0) + (dms.s / 3600.0);
if (dms.negative) {
return -dd;
}
return dd;
}
} // namespace
CesiumDegreesMinutesSecondsEditor::CesiumDegreesMinutesSecondsEditor(
TSharedPtr<class IPropertyHandle> InputDecimalDegreesHandle,
bool InputIsLongitude) {
this->DecimalDegreesHandle = InputDecimalDegreesHandle;
this->IsLongitude = InputIsLongitude;
}
void CesiumDegreesMinutesSecondsEditor::PopulateRow(IDetailPropertyRow& Row) {
FSlateFontInfo FontInfo = IDetailLayoutBuilder::GetDetailFont();
// The default editing component for the property:
// A SpinBox for the decimal degrees
DecimalDegreesSpinBox =
SNew(SSpinBox<double>)
.Font(FontInfo)
.MinSliderValue(IsLongitude ? -180 : -90)
.MaxSliderValue(IsLongitude ? 180 : 90)
.OnValueChanged(
this,
&CesiumDegreesMinutesSecondsEditor::SetDecimalDegreesOnProperty)
.Value(
this,
&CesiumDegreesMinutesSecondsEditor::
GetDecimalDegreesFromProperty);
// Editing components for the DMS representation:
// Spin boxes for degrees, minutes and seconds
DegreesSpinBox =
SNew(SSpinBox<int32>)
.Font(FontInfo)
.ToolTipText(FText::FromString("Degrees"))
.MinSliderValue(0)
.MaxSliderValue(IsLongitude ? 179 : 89)
.OnValueChanged(this, &CesiumDegreesMinutesSecondsEditor::SetDegrees)
.Value(this, &CesiumDegreesMinutesSecondsEditor::GetDegrees);
MinutesSpinBox =
SNew(SSpinBox<int32>)
.Font(FontInfo)
.ToolTipText(FText::FromString("Minutes"))
.MinSliderValue(0)
.MaxSliderValue(59)
.OnValueChanged(this, &CesiumDegreesMinutesSecondsEditor::SetMinutes)
.Value(this, &CesiumDegreesMinutesSecondsEditor::GetMinutes);
SecondsSpinBox =
SNew(SSpinBox<double>)
.Font(FontInfo)
.ToolTipText(FText::FromString("Seconds"))
.MinSliderValue(0)
.MaxSliderValue(59.999999)
.OnValueChanged(this, &CesiumDegreesMinutesSecondsEditor::SetSeconds)
.Value(this, &CesiumDegreesMinutesSecondsEditor::GetSeconds);
// The combo box for selecting "Eeast" or "West",
// or "North" or "South", respectively.
FText signTooltip;
if (IsLongitude) {
PositiveIndicator = MakeShareable(new FString(TEXT("E")));
NegativeIndicator = MakeShareable(new FString(TEXT("W")));
signTooltip = FText::FromString("East or West");
} else {
PositiveIndicator = MakeShareable(new FString(TEXT("N")));
NegativeIndicator = MakeShareable(new FString(TEXT("S")));
signTooltip = FText::FromString("North or South");
}
SignComboBoxItems.Add(NegativeIndicator);
SignComboBoxItems.Emplace(PositiveIndicator);
SignComboBox = SNew(STextComboBox)
.Font(FontInfo)
.ToolTipText(signTooltip)
.OptionsSource(&SignComboBoxItems)
.OnSelectionChanged(
this,
&CesiumDegreesMinutesSecondsEditor::SignChanged);
SignComboBox->SetSelectedItem(
GetDecimalDegreesFromProperty() < 0 ? NegativeIndicator
: PositiveIndicator);
const float hPad = 3.0;
const float vPad = 2.0;
// clang-format off
Row.CustomWidget().NameContent()
[
DecimalDegreesHandle->CreatePropertyNameWidget()
]
.ValueContent().HAlign(EHorizontalAlignment::HAlign_Fill)
[
SNew( SVerticalBox )
+ SVerticalBox::Slot().Padding(0.0f, vPad)
[
DecimalDegreesSpinBox.ToSharedRef()
]
+ SVerticalBox::Slot().Padding(0.0f, vPad)
[
SNew( SHorizontalBox )
+ SHorizontalBox::Slot().FillWidth(1.0)
[
DegreesSpinBox.ToSharedRef()
]
+ SHorizontalBox::Slot().AutoWidth().Padding(hPad, 0.0f)
[
// The 'degrees' symbol
SNew(STextBlock)
.Text(FText::FromString(TEXT("\u00B0")))
.ToolTipText(FText::FromString("Degrees"))
]
+ SHorizontalBox::Slot().FillWidth(1.0)
[
MinutesSpinBox.ToSharedRef()
]
+ SHorizontalBox::Slot().AutoWidth().Padding(hPad, 0.0f)
[
// The 'minutes' symbol
SNew(STextBlock)
.Text(FText::FromString(TEXT("\u2032")))
.ToolTipText(FText::FromString("Minutes"))
]
+ SHorizontalBox::Slot().FillWidth(1.0)
[
SecondsSpinBox.ToSharedRef()
]
+ SHorizontalBox::Slot().AutoWidth().Padding(hPad, 0.0f)
[
// The 'seconds' symbol
SNew(STextBlock)
.Text(FText::FromString(TEXT("\u2033")))
.ToolTipText(FText::FromString("Seconds"))
]
+ SHorizontalBox::Slot().AutoWidth()
[
SignComboBox.ToSharedRef()
]
]
];
// clang-format on
}
double
CesiumDegreesMinutesSecondsEditor::GetDecimalDegreesFromProperty() const {
double decimalDegrees;
FPropertyAccess::Result AccessResult =
DecimalDegreesHandle->GetValue(decimalDegrees);
if (AccessResult == FPropertyAccess::Success) {
return decimalDegrees;
}
// In theory, this should never happen if the actual property is a double. But
// in practice it gets triggered when saving a level, for some reason. So, we
// ignore it.
return 0.0;
}
void CesiumDegreesMinutesSecondsEditor::SetDecimalDegreesOnProperty(
double NewValue) {
DecimalDegreesHandle->SetValue(NewValue);
SignComboBox->SetSelectedItem(
NewValue < 0 ? NegativeIndicator : PositiveIndicator);
}
int32 CesiumDegreesMinutesSecondsEditor::GetDegrees() const {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
return static_cast<int32>(dms.d);
}
void CesiumDegreesMinutesSecondsEditor::SetDegrees(int32 NewValue) {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
dms.d = NewValue;
double newDecimalDegreesValue = dmsToDecimalDegrees(dms);
SetDecimalDegreesOnProperty(newDecimalDegreesValue);
}
int32 CesiumDegreesMinutesSecondsEditor::GetMinutes() const {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
return static_cast<int32>(dms.m);
}
void CesiumDegreesMinutesSecondsEditor::SetMinutes(int32 NewValue) {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
dms.m = NewValue;
double newDecimalDegreesValue = dmsToDecimalDegrees(dms);
SetDecimalDegreesOnProperty(newDecimalDegreesValue);
}
double CesiumDegreesMinutesSecondsEditor::GetSeconds() const {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
return dms.s;
}
void CesiumDegreesMinutesSecondsEditor::SetSeconds(double NewValue) {
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
dms.s = NewValue;
double newDecimalDegreesValue = dmsToDecimalDegrees(dms);
SetDecimalDegreesOnProperty(newDecimalDegreesValue);
}
void CesiumDegreesMinutesSecondsEditor::SignChanged(
TSharedPtr<FString> StringItem,
ESelectInfo::Type SelectInfo) {
bool negative = false;
if (StringItem.IsValid()) {
negative = (StringItem == NegativeIndicator);
}
double decimalDegrees = GetDecimalDegreesFromProperty();
DMS dms = decimalDegreesToDms(decimalDegrees);
dms.negative = negative;
double newDecimalDegreesValue = dmsToDecimalDegrees(dms);
SetDecimalDegreesOnProperty(newDecimalDegreesValue);
}

View File

@ -0,0 +1,80 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "IDetailCustomization.h"
#include "IDetailPropertyRow.h"
#include "Types/SlateEnums.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Input/STextComboBox.h"
/**
* A class that allows configuring a Details View row for a
* latitude or longitude property.
*
* Latitude and longitude properties are often computed with doubles
* representing decimal-point degrees. This Details View row will show the
* property with an additional Degree-Minutes-Seconds (DMS) view for easier
* usability and editing.
*
* See FCesiumGeoreferenceCustomization::CustomizeDetails for
* an example of how to use this class.
*/
class CesiumDegreesMinutesSecondsEditor
: public TSharedFromThis<CesiumDegreesMinutesSecondsEditor> {
public:
/**
* Creates a new instance.
*
* The given property handle must be a handle to a 'double'
* property!
*
* @param InputDecimalDegreesHandle The property handle for the
* decimal degrees property
* @param InputIsLongitude Whether the edited property is a
* longitude (as opposed to a latitude) property
*/
CesiumDegreesMinutesSecondsEditor(
TSharedPtr<class IPropertyHandle> InputDecimalDegreesHandle,
bool InputIsLongitude);
/**
* Populates the given Details View row with the default
* editor (a SSpinBox for the value), as well as the
* spin boxes and dropdowns for the DMS editing.
*
* @param Row The Details View row
*/
void PopulateRow(IDetailPropertyRow& Row);
private:
TSharedPtr<class IPropertyHandle> DecimalDegreesHandle;
bool IsLongitude;
TSharedPtr<SSpinBox<double>> DecimalDegreesSpinBox;
TSharedPtr<SSpinBox<int32>> DegreesSpinBox;
TSharedPtr<SSpinBox<int32>> MinutesSpinBox;
TSharedPtr<SSpinBox<double>> SecondsSpinBox;
TSharedPtr<FString> NegativeIndicator;
TSharedPtr<FString> PositiveIndicator;
TArray<TSharedPtr<FString>> SignComboBoxItems;
TSharedPtr<STextComboBox> SignComboBox;
double GetDecimalDegreesFromProperty() const;
void SetDecimalDegreesOnProperty(double NewValue);
int32 GetDegrees() const;
void SetDegrees(int32 NewValue);
int32 GetMinutes() const;
void SetMinutes(int32 NewValue);
double GetSeconds() const;
void SetSeconds(double NewValue);
void
SignChanged(TSharedPtr<FString> StringItem, ESelectInfo::Type SelectInfo);
};

View File

@ -0,0 +1,737 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumEditor.h"
#include "Cesium3DTilesSelection/Tileset.h"
#include "Cesium3DTileset.h"
#include "Cesium3DTilesetCustomization.h"
#include "CesiumCartographicPolygon.h"
#include "CesiumCommands.h"
#include "CesiumGeoreferenceCustomization.h"
#include "CesiumGlobeAnchorCustomization.h"
#include "CesiumIonPanel.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServer.h"
#include "CesiumIonTokenTroubleshooting.h"
#include "CesiumPanel.h"
#include "CesiumRuntime.h"
#include "CesiumSunSky.h"
#include "Editor.h"
#include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructure.h"
#include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h"
#include "EngineUtils.h"
#include "Framework/Docking/LayoutExtender.h"
#include "Framework/Docking/TabManager.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Interfaces/IPluginManager.h"
#include "LevelEditor.h"
#include "PropertyEditorModule.h"
#include "Styling/SlateStyle.h"
#include "Styling/SlateStyleRegistry.h"
constexpr int MaximumOverlaysWithDefaultMaterial = 3;
IMPLEMENT_MODULE(FCesiumEditorModule, CesiumEditor)
DEFINE_LOG_CATEGORY(LogCesiumEditor);
#define IMAGE_BRUSH(RelativePath, ...) \
FSlateImageBrush( \
FCesiumEditorModule::InContent(RelativePath, ".png"), \
__VA_ARGS__)
#define BOX_BRUSH(RelativePath, ...) \
FSlateBoxBrush( \
FCesiumEditorModule::InContent(RelativePath, ".png"), \
__VA_ARGS__)
FString FCesiumEditorModule::InContent(
const FString& RelativePath,
const ANSICHAR* Extension) {
static FString ContentDir = IPluginManager::Get()
.FindPlugin(TEXT("CesiumForUnreal"))
->GetContentDir();
return (ContentDir / RelativePath) + Extension;
}
TSharedPtr<FSlateStyleSet> FCesiumEditorModule::StyleSet = nullptr;
FCesiumEditorModule* FCesiumEditorModule::_pModule = nullptr;
namespace {
AActor* SpawnActorWithClass(UClass* actorClass);
/**
* Register an icon in the StyleSet, using the given property
* name and relative resource path.
*
* This will register the icon once with a default size of
* 40x40, and once under the same name, extended by the
* suffix `".Small"`, with a size of 20x20, which will be
* used when the "useSmallToolbarIcons" editor preference
* was enabled.
*
* @param styleSet The style set
* @param The property name
* @param The resource path
*/
void registerIcon(
TSharedPtr<FSlateStyleSet>& styleSet,
const FString& propertyName,
const FString& relativePath) {
const FVector2D Icon40x40(40.0f, 40.0f);
const FVector2D Icon20x20(20.0f, 20.0f);
styleSet->Set(FName(propertyName), new IMAGE_BRUSH(relativePath, Icon40x40));
styleSet->Set(
FName(propertyName + ".Small"),
new IMAGE_BRUSH(relativePath, Icon20x20));
}
/**
* Create a slate box brush that can be used as the
* normal-, hovered-, or pressed-brush for a button,
* based on a resource with the given name, that
* contains a slate box image with a margin of 4 pixels.
*
* @param name The name of the image (without extension, PNG is assumed)
* @param color The color used for "dyeing" the image
* @return The box brush
*/
FSlateBoxBrush
createButtonBoxBrush(const FString& name, const FLinearColor& color) {
return BOX_BRUSH(name, FMargin(4 / 16.0f), color);
}
} // namespace
namespace {
/**
* Registers our details panel customizations with the property editor.
*/
void registerDetailCustomization() {
FPropertyEditorModule& PropertyEditorModule =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>(
"PropertyEditor");
FCesiumGeoreferenceCustomization::Register(PropertyEditorModule);
FCesiumGlobeAnchorCustomization::Register(PropertyEditorModule);
FCesium3DTilesetCustomization::Register(PropertyEditorModule);
PropertyEditorModule.NotifyCustomizationModuleChanged();
}
/**
* Undo the registration that was done in `registerDetailCustomization`
*/
void unregisterDetailCustomization() {
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) {
FPropertyEditorModule& PropertyEditorModule =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>(
"PropertyEditor");
FCesiumGeoreferenceCustomization::Unregister(PropertyEditorModule);
FCesiumGlobeAnchorCustomization::Unregister(PropertyEditorModule);
FCesium3DTilesetCustomization::Unregister(PropertyEditorModule);
}
}
} // namespace
namespace {
/**
* @brief Populate the given StyleSet with the Cesium icons and fonts.
*
* @param StyleSet The StyleSet
*/
void populateCesiumStyleSet(TSharedPtr<FSlateStyleSet>& StyleSet) {
if (!StyleSet.IsValid()) {
return;
}
const FVector2D Icon16x16(16.0f, 16.0f);
const FVector2D Icon40x40(40.0f, 40.0f);
const FVector2D Icon64x64(64.0f, 64.0f);
StyleSet->Set(
"Cesium.MenuIcon",
new IMAGE_BRUSH(TEXT("Cesium-icon-16x16"), Icon16x16));
// Give Cesium Actors a Cesium icon in the editor
StyleSet->Set(
"ClassIcon.Cesium3DTileset",
new IMAGE_BRUSH(TEXT("Cesium-icon-16x16"), Icon16x16));
StyleSet->Set(
"ClassThumbnail.Cesium3DTileset",
new IMAGE_BRUSH(TEXT("Cesium-64x64"), Icon64x64));
StyleSet->Set(
"ClassIcon.CesiumGeoreference",
new IMAGE_BRUSH(TEXT("Cesium-icon-16x16"), Icon16x16));
StyleSet->Set(
"ClassThumbnail.CesiumGeoreference",
new IMAGE_BRUSH(TEXT("Cesium-64x64"), Icon64x64));
// Icons for the toolbar. These will be registered with
// a default size, and a ".Small" suffix for the case
// that the useSmallToolbarIcons preference is enabled
registerIcon(StyleSet, "Cesium.Common.AddFromIon", "FontAwesome/plus-solid");
registerIcon(
StyleSet,
"Cesium.Common.UploadToIon",
"FontAwesome/cloud-upload-alt-solid");
registerIcon(
StyleSet,
"Cesium.Common.SignOut",
"FontAwesome/sign-out-alt-solid");
registerIcon(
StyleSet,
"Cesium.Common.OpenDocumentation",
"FontAwesome/book-reader-solid");
registerIcon(
StyleSet,
"Cesium.Common.OpenSupport",
"FontAwesome/hands-helping-solid");
registerIcon(
StyleSet,
"Cesium.Common.OpenTokenSelector",
"FontAwesome/key-solid");
StyleSet->Set(
"Cesium.Common.GreenTick",
new IMAGE_BRUSH(TEXT("FontAwesome/check-solid"), Icon16x16));
StyleSet->Set(
"Cesium.Common.RedX",
new IMAGE_BRUSH(TEXT("FontAwesome/times-solid"), Icon16x16));
registerIcon(StyleSet, "Cesium.Common.OpenCesiumPanel", "Cesium-64x64");
StyleSet->Set(
"Cesium.Common.Refresh",
new IMAGE_BRUSH(TEXT("FontAwesome/sync-alt-solid"), Icon16x16));
StyleSet->Set(
"Cesium.Logo",
new IMAGE_BRUSH(
"Cesium_for_Unreal_light_color_vertical-height150",
FVector2D(184.0f, 150.0f)));
StyleSet->Set(
"WelcomeText",
FTextBlockStyle()
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 14)));
StyleSet->Set(
"Heading",
FTextBlockStyle()
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 12)));
StyleSet->Set(
"BodyBold",
FTextBlockStyle()
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 9)));
StyleSet->Set(
"AssetDetailsFieldHeader",
FTextBlockStyle()
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 11)));
StyleSet->Set(
"AssetDetailsFieldValue",
FTextBlockStyle()
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 9)));
const FLinearColor CesiumButtonLighter(0.16863f, 0.52941f, 0.76863f, 1.0f);
const FLinearColor CesiumButton(0.07059f, 0.35686f, 0.59216f, 1.0f);
const FLinearColor CesiumButtonDarker(0.05490f, 0.29412f, 0.45882f, 1.0f);
const FButtonStyle CesiumButtonStyle =
FButtonStyle()
.SetNormalPadding(FMargin(10, 5, 10, 5))
.SetPressedPadding(FMargin(10, 5, 10, 5))
.SetNormal(createButtonBoxBrush("CesiumButton", CesiumButton))
.SetHovered(createButtonBoxBrush("CesiumButton", CesiumButtonLighter))
.SetPressed(createButtonBoxBrush("CesiumButton", CesiumButtonDarker));
StyleSet->Set("CesiumButton", CesiumButtonStyle);
const FTextBlockStyle CesiumButtonTextStyle =
FTextBlockStyle()
.SetColorAndOpacity(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))
.SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 12));
StyleSet->Set("CesiumButtonText", CesiumButtonTextStyle);
}
} // namespace
void FCesiumEditorModule::StartupModule() {
_pModule = this;
IModuleInterface::StartupModule();
registerDetailCustomization();
this->_serverManager.Initialize();
// Only register style once
if (!StyleSet.IsValid()) {
StyleSet = MakeShareable(new FSlateStyleSet("CesiumStyleSet"));
populateCesiumStyleSet(StyleSet);
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
}
FCesiumCommands::Register();
FGlobalTabmanager::Get()
->RegisterNomadTabSpawner(
TEXT("Cesium"),
FOnSpawnTab::CreateRaw(this, &FCesiumEditorModule::SpawnCesiumTab))
.SetGroup(WorkspaceMenu::GetMenuStructure().GetLevelEditorCategory())
.SetDisplayName(FText::FromString(TEXT("Cesium")))
.SetTooltipText(FText::FromString(TEXT("Cesium")))
.SetIcon(FSlateIcon(TEXT("CesiumStyleSet"), TEXT("Cesium.MenuIcon")));
FGlobalTabmanager::Get()
->RegisterNomadTabSpawner(
TEXT("CesiumIon"),
FOnSpawnTab::CreateRaw(
this,
&FCesiumEditorModule::SpawnCesiumIonAssetBrowserTab))
.SetGroup(WorkspaceMenu::GetMenuStructure().GetLevelEditorCategory())
.SetDisplayName(FText::FromString(TEXT("Cesium ion Assets")))
.SetTooltipText(FText::FromString(TEXT("Cesium ion Assets")))
.SetIcon(FSlateIcon(TEXT("CesiumStyleSet"), TEXT("Cesium.MenuIcon")));
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
if (pLevelEditorModule) {
pLevelEditorModule->OnRegisterLayoutExtensions().AddLambda(
[](FLayoutExtender& extender) {
extender.ExtendLayout(
FTabId("PlacementBrowser"),
ELayoutExtensionPosition::After,
FTabManager::FTab(FName("Cesium"), ETabState::OpenedTab));
extender.ExtendLayout(
FTabId("OutputLog"),
ELayoutExtensionPosition::Before,
FTabManager::FTab(FName("CesiumIon"), ETabState::ClosedTab));
});
TSharedRef<FUICommandList> toolbarCommandList =
MakeShared<FUICommandList>();
toolbarCommandList->MapAction(
FCesiumCommands::Get().OpenCesiumPanel,
FExecuteAction::CreateLambda([]() {
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
TSharedPtr<FTabManager> pTabManager =
pLevelEditorModule
? pLevelEditorModule->GetLevelEditorTabManager()
: FGlobalTabmanager::Get();
pTabManager->TryInvokeTab(FTabId(TEXT("Cesium")));
}));
TSharedPtr<FExtender> pToolbarExtender = MakeShared<FExtender>();
pToolbarExtender->AddToolBarExtension(
"Settings",
EExtensionHook::After,
toolbarCommandList,
FToolBarExtensionDelegate::CreateLambda([](FToolBarBuilder& builder) {
builder.BeginSection("Cesium");
builder.AddToolBarButton(FCesiumCommands::Get().OpenCesiumPanel);
builder.EndSection();
}));
pLevelEditorModule->GetToolBarExtensibilityManager()->AddExtender(
pToolbarExtender);
}
this->_tilesetLoadFailureSubscription = OnCesium3DTilesetLoadFailure.AddRaw(
this,
&FCesiumEditorModule::OnTilesetLoadFailure);
this->_rasterOverlayLoadFailureSubscription =
OnCesiumRasterOverlayLoadFailure.AddRaw(
this,
&FCesiumEditorModule::OnRasterOverlayLoadFailure);
this->_tilesetIonTroubleshootingSubscription =
OnCesium3DTilesetIonTroubleshooting.AddRaw(
this,
&FCesiumEditorModule::OnTilesetIonTroubleshooting);
this->_rasterOverlayIonTroubleshootingSubscription =
OnCesiumRasterOverlayIonTroubleshooting.AddRaw(
this,
&FCesiumEditorModule::OnRasterOverlayIonTroubleshooting);
}
void FCesiumEditorModule::ShutdownModule() {
if (this->_tilesetLoadFailureSubscription.IsValid()) {
OnCesium3DTilesetLoadFailure.Remove(this->_tilesetLoadFailureSubscription);
this->_tilesetLoadFailureSubscription.Reset();
}
if (this->_rasterOverlayLoadFailureSubscription.IsValid()) {
OnCesiumRasterOverlayLoadFailure.Remove(
this->_rasterOverlayLoadFailureSubscription);
this->_rasterOverlayLoadFailureSubscription.Reset();
}
if (this->_tilesetIonTroubleshootingSubscription.IsValid()) {
OnCesium3DTilesetIonTroubleshooting.Remove(
this->_tilesetIonTroubleshootingSubscription);
this->_tilesetIonTroubleshootingSubscription.Reset();
}
if (this->_rasterOverlayIonTroubleshootingSubscription.IsValid()) {
OnCesiumRasterOverlayIonTroubleshooting.Remove(
this->_rasterOverlayIonTroubleshootingSubscription);
this->_rasterOverlayIonTroubleshootingSubscription.Reset();
}
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TEXT("Cesium"));
FCesiumCommands::Unregister();
IModuleInterface::ShutdownModule();
unregisterDetailCustomization();
_pModule = nullptr;
}
TSharedRef<SDockTab>
FCesiumEditorModule::SpawnCesiumTab(const FSpawnTabArgs& TabSpawnArgs) {
TSharedRef<SDockTab> SpawnedTab =
SNew(SDockTab).TabRole(ETabRole::NomadTab)[SNew(CesiumPanel)];
return SpawnedTab;
}
TSharedRef<SDockTab> FCesiumEditorModule::SpawnCesiumIonAssetBrowserTab(
const FSpawnTabArgs& TabSpawnArgs) {
TSharedRef<SDockTab> SpawnedTab =
SNew(SDockTab).TabRole(ETabRole::NomadTab)[SNew(CesiumIonPanel)];
return SpawnedTab;
}
void FCesiumEditorModule::OnTilesetLoadFailure(
const FCesium3DTilesetLoadFailureDetails& details) {
if (!details.Tileset.IsValid()) {
return;
}
// Don't pop a troubleshooting panel over a game world (including
// Play-In-Editor).
if (details.Tileset->GetWorld()->IsGameWorld()) {
return;
}
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
if (pLevelEditorModule) {
pLevelEditorModule->GetLevelEditorTabManager()->TryInvokeTab(
FTabId("OutputLog"));
}
// Check for a 401 connecting to Cesium ion, which means the token is invalid
// (or perhaps the asset ID is). Also check for a 404, because ion returns 404
// when the token is valid but not authorized for the asset.
if (details.Type == ECesium3DTilesetLoadType::CesiumIon &&
(details.HttpStatusCode == 401 || details.HttpStatusCode == 404)) {
CesiumIonTokenTroubleshooting::Open(details.Tileset.Get(), true);
}
}
void FCesiumEditorModule::OnRasterOverlayLoadFailure(
const FCesiumRasterOverlayLoadFailureDetails& details) {
if (!details.Overlay.IsValid()) {
return;
}
// Don't pop a troubleshooting panel over a game world (including
// Play-In-Editor).
if (details.Overlay->GetWorld()->IsGameWorld()) {
return;
}
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
if (pLevelEditorModule) {
pLevelEditorModule->GetLevelEditorTabManager()->TryInvokeTab(
FTabId("OutputLog"));
}
// Check for a 401 connecting to Cesium ion, which means the token is invalid
// (or perhaps the asset ID is). Also check for a 404, because ion returns 404
// when the token is valid but not authorized for the asset.
if (details.Type == ECesiumRasterOverlayLoadType::CesiumIon &&
(details.HttpStatusCode == 401 || details.HttpStatusCode == 404)) {
CesiumIonTokenTroubleshooting::Open(details.Overlay.Get(), true);
}
}
void FCesiumEditorModule::OnTilesetIonTroubleshooting(
ACesium3DTileset* pTileset) {
CesiumIonTokenTroubleshooting::Open(pTileset, false);
}
void FCesiumEditorModule::OnRasterOverlayIonTroubleshooting(
UCesiumRasterOverlay* pOverlay) {
CesiumIonTokenTroubleshooting::Open(pOverlay, false);
}
TSharedPtr<FSlateStyleSet> FCesiumEditorModule::GetStyle() { return StyleSet; }
const FName& FCesiumEditorModule::GetStyleSetName() {
return StyleSet->GetStyleSetName();
}
ACesium3DTileset* FCesiumEditorModule::FindFirstTilesetSupportingOverlays() {
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
ULevel* pCurrentLevel = pCurrentWorld->GetCurrentLevel();
for (TActorIterator<ACesium3DTileset> it(pCurrentWorld); it; ++it) {
const Cesium3DTilesSelection::Tileset* pTileset = it->GetTileset();
if (pTileset) {
return *it;
}
}
return nullptr;
}
ACesium3DTileset*
FCesiumEditorModule::FindFirstTilesetWithAssetID(int64_t assetID) {
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
ULevel* pCurrentLevel = pCurrentWorld->GetCurrentLevel();
ACesium3DTileset* pTilesetActor = nullptr;
for (TActorIterator<ACesium3DTileset> it(pCurrentWorld); !pTilesetActor && it;
++it) {
ACesium3DTileset* pActor = *it;
// The existing Actor must be in the current level. Because it's sometimes
// useful to add the same tileset to multiple sub-levels.
if (!IsValid(pActor) || pActor->GetLevel() != pCurrentLevel)
continue;
const Cesium3DTilesSelection::Tileset* pTileset = it->GetTileset();
if (pTileset && it->GetIonAssetID() == assetID) {
return *it;
}
}
return nullptr;
}
ACesium3DTileset*
FCesiumEditorModule::CreateTileset(const std::string& name, int64_t assetID) {
AActor* pNewActor = SpawnActorWithClass(ACesium3DTileset::StaticClass());
ACesium3DTileset* pTilesetActor = Cast<ACesium3DTileset>(pNewActor);
if (pTilesetActor) {
pTilesetActor->SetActorLabel(UTF8_TO_TCHAR(name.c_str()));
if (assetID != -1) {
pTilesetActor->SetIonAssetID(assetID);
}
}
return pTilesetActor;
}
UCesiumIonRasterOverlay* FCesiumEditorModule::AddOverlay(
ACesium3DTileset* pTilesetActor,
const std::string& name,
int64_t assetID) {
// Remove an existing component with the same name but different types.
// This is necessary because UE will die immediately if we create two
// components with the same name.
FName newName = FName(name.c_str());
UObject* pExisting = static_cast<UObject*>(
FindObjectWithOuter(pTilesetActor, nullptr, newName));
if (pExisting) {
UCesiumRasterOverlay* pCesiumOverlay =
Cast<UCesiumRasterOverlay>(pExisting);
if (pCesiumOverlay) {
pCesiumOverlay->DestroyComponent();
} else {
// There's some object using our name, but it's not ours.
// We could do complicated things here, but this should be a very uncommon
// scenario so let's just log.
UE_LOG(
LogCesiumEditor,
Warning,
TEXT(
"Cannot create raster overlay component %s because the name is already in use."),
*newName.ToString());
}
}
// Find the first available `OverlayN` MaterialLayerKey.
TArray<UCesiumRasterOverlay*> rasterOverlays;
pTilesetActor->GetComponents<UCesiumRasterOverlay>(rasterOverlays);
FString overlayKey = TEXT("Overlay0");
auto materialLayerKeyMatches = [&newName,
&overlayKey](UCesiumRasterOverlay* pOverlay) {
return pOverlay->MaterialLayerKey == overlayKey;
};
int i = 0;
while (rasterOverlays.FindByPredicate(materialLayerKeyMatches)) {
++i;
overlayKey = FString(TEXT("Overlay")) + FString::FromInt(i);
}
UCesiumIonRasterOverlay* pOverlay = NewObject<UCesiumIonRasterOverlay>(
pTilesetActor,
FName(name.c_str()),
RF_Transactional);
pOverlay->MaterialLayerKey = overlayKey;
pOverlay->IonAssetID = assetID;
pOverlay->SetActive(true);
pOverlay->OnComponentCreated();
pTilesetActor->AddInstanceComponent(pOverlay);
if (i >= MaximumOverlaysWithDefaultMaterial) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT(
"The default material only supports up to %d raster overlays, and your tileset is now using %d, so the extra overlays will be ignored. Consider creating a custom Material Instance with support for more overlays."),
MaximumOverlaysWithDefaultMaterial,
i + 1);
}
return pOverlay;
}
UCesiumIonRasterOverlay* FCesiumEditorModule::AddBaseOverlay(
ACesium3DTileset* pTilesetActor,
const std::string& name,
int64_t assetID) {
// Remove Overlay0 (if it exists) and add the new one.
TArray<UCesiumRasterOverlay*> rasterOverlays;
pTilesetActor->GetComponents<UCesiumRasterOverlay>(rasterOverlays);
for (UCesiumRasterOverlay* pOverlay : rasterOverlays) {
if (pOverlay->MaterialLayerKey == TEXT("Overlay0")) {
pOverlay->DestroyComponent(false);
}
}
return FCesiumEditorModule::AddOverlay(pTilesetActor, name, assetID);
}
namespace {
AActor* GetFirstCurrentLevelActorWithClass(UClass* pActorClass) {
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
ULevel* pCurrentLevel = pCurrentWorld->GetCurrentLevel();
for (TActorIterator<AActor> it(pCurrentWorld); it; ++it) {
if (it->GetClass() == pActorClass && it->GetLevel() == pCurrentLevel) {
return *it;
}
}
return nullptr;
}
/**
* Returns whether the current level of the edited world contains
* any actor with the given class.
*
* @param actorClass The expected class
* @return Whether such an actor could be found
*/
bool CurrentLevelContainsActorWithClass(UClass* pActorClass) {
return GetFirstCurrentLevelActorWithClass(pActorClass) != nullptr;
}
/**
* Tries to spawn an actor with the given class, with all
* default parameters, in the current level of the edited world.
*
* @param actorClass The class
* @return The resulting actor, or `nullptr` if the actor
* could not be spawned.
*/
AActor* SpawnActorWithClass(UClass* actorClass) {
if (!actorClass) {
return nullptr;
}
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
ULevel* pCurrentLevel = pCurrentWorld->GetCurrentLevel();
ACesiumGeoreference* Georeference =
ACesiumGeoreference::GetDefaultGeoreference(pCurrentWorld);
// Spawn the new Actor with the same world transform as the
// CesiumGeoreference. This way it will match the existing globe. The user may
// transform it from there (e.g., to offset one tileset from another).
// When we're spawning this Actor in a sub-level, the transform specified here
// is a world transform relative to the _persistent level_. It's not relative
// to the sub-level's origin. Strange but true! But it's helpful in this case
// because we're able to correctly spawn things like tilesets into sub-levels
// where the sub-level origin and the persistent-level origin don't coincide
// due to a LevelTransform.
AActor* NewActor = GEditor->AddActor(
pCurrentLevel,
actorClass,
Georeference->GetActorTransform(),
false,
RF_Transactional);
// Make the new Actor a child of the CesiumGeoreference. Unless they're in
// different levels.
if (Georeference->GetLevel() == pCurrentLevel) {
NewActor->AttachToActor(
Georeference,
FAttachmentTransformRules::KeepWorldTransform);
}
return NewActor;
}
} // namespace
AActor* FCesiumEditorModule::GetCurrentLevelCesiumSunSky() {
return GetFirstCurrentLevelActorWithClass(GetCesiumSunSkyClass());
}
AActor* FCesiumEditorModule::GetCurrentLevelDynamicPawn() {
return GetFirstCurrentLevelActorWithClass(GetDynamicPawnBlueprintClass());
}
AActor* FCesiumEditorModule::SpawnCesiumSunSky() {
return SpawnActorWithClass(GetCesiumSunSkyClass());
}
AActor* FCesiumEditorModule::SpawnDynamicPawn() {
return SpawnActorWithClass(GetDynamicPawnBlueprintClass());
}
UClass* FCesiumEditorModule::GetCesiumSunSkyClass() {
return ACesiumSunSky::StaticClass();
}
AActor* FCesiumEditorModule::SpawnBlankTileset() {
return SpawnActorWithClass(ACesium3DTileset::StaticClass());
}
AActor* FCesiumEditorModule::SpawnCartographicPolygon() {
return SpawnActorWithClass(ACesiumCartographicPolygon::StaticClass());
}
UClass* FCesiumEditorModule::GetDynamicPawnBlueprintClass() {
static UClass* pResult = nullptr;
if (!pResult) {
pResult = LoadClass<AActor>(
nullptr,
TEXT("/CesiumForUnreal/DynamicPawn.DynamicPawn_C"));
if (!pResult) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Could not load /CesiumForUnreal/DynamicPawn.DynamicPawn_C"));
}
}
return pResult;
}

View File

@ -0,0 +1,137 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumEditorReparentHandler.h"
#include "CesiumEditorSubLevelMutex.h"
#include "CesiumIonServerManager.h"
#include "CesiumIonSession.h"
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "Styling/SlateStyle.h"
#include "Widgets/Docking/SDockTab.h"
#include <optional>
class FSpawnTabArgs;
class ACesium3DTileset;
class UCesiumRasterOverlay;
class UCesiumIonRasterOverlay;
struct FCesium3DTilesetLoadFailureDetails;
struct FCesiumRasterOverlayLoadFailureDetails;
class UCesiumIonServer;
DECLARE_LOG_CATEGORY_EXTERN(LogCesiumEditor, Log, All);
class FCesiumEditorModule : public IModuleInterface {
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static FString
InContent(const FString& RelativePath, const ANSICHAR* Extension);
static TSharedPtr<FSlateStyleSet> GetStyle();
static const FName& GetStyleSetName();
static FCesiumEditorModule* get() { return _pModule; }
static CesiumIonServerManager& serverManager() {
assert(_pModule);
return get()->_serverManager;
}
static ACesium3DTileset* FindFirstTilesetSupportingOverlays();
static ACesium3DTileset* FindFirstTilesetWithAssetID(int64_t assetID);
static ACesium3DTileset*
CreateTileset(const std::string& name, int64_t assetID);
/**
* Adds an overlay with the the MaterialLayerKey `OverlayN` where N is the
* next unused index.
*/
static UCesiumIonRasterOverlay* AddOverlay(
ACesium3DTileset* pTilesetActor,
const std::string& name,
int64_t assetID);
/**
* Adds a base overlay, replacing the existing overlay with MaterialLayerKey
* Overlay0, if any.
*/
static UCesiumIonRasterOverlay* AddBaseOverlay(
ACesium3DTileset* pTilesetActor,
const std::string& name,
int64_t assetID);
/**
* Gets the first CesiumSunSky in the current level if there is one, or
* nullptr if there is not.
*/
static AActor* GetCurrentLevelCesiumSunSky();
/**
* Gets the first DynamicPawn in the current level if there is one, or
* nullptr if there is not.
*/
static AActor* GetCurrentLevelDynamicPawn();
/**
* Spawns a new actor with the _cesiumSunSkyBlueprintClass
* in the current level of the edited world.
*/
static AActor* SpawnCesiumSunSky();
/**
* Spawns a new actor with the _dynamicPawnBlueprintClass
* in the current level of the edited world.
*/
static AActor* SpawnDynamicPawn();
/**
* Spawns a new Cesium3DTileset with default values in the current level of
* the edited world.
*/
static AActor* SpawnBlankTileset();
/**
* Spawns a new CesiumCartographicPolygon in the current level of the edited
* world.
*/
static AActor* SpawnCartographicPolygon();
private:
TSharedRef<SDockTab> SpawnCesiumTab(const FSpawnTabArgs& TabSpawnArgs);
TSharedRef<SDockTab>
SpawnCesiumIonAssetBrowserTab(const FSpawnTabArgs& TabSpawnArgs);
void OnTilesetLoadFailure(const FCesium3DTilesetLoadFailureDetails& details);
void OnRasterOverlayLoadFailure(
const FCesiumRasterOverlayLoadFailureDetails& details);
void OnTilesetIonTroubleshooting(ACesium3DTileset* pTileset);
void OnRasterOverlayIonTroubleshooting(UCesiumRasterOverlay* pOverlay);
CesiumIonServerManager _serverManager;
FDelegateHandle _tilesetLoadFailureSubscription;
FDelegateHandle _rasterOverlayLoadFailureSubscription;
FDelegateHandle _tilesetIonTroubleshootingSubscription;
FDelegateHandle _rasterOverlayIonTroubleshootingSubscription;
CesiumEditorSubLevelMutex _subLevelMutex;
CesiumEditorReparentHandler _reparentHandler;
static TSharedPtr<FSlateStyleSet> StyleSet;
static FCesiumEditorModule* _pModule;
/**
* Gets the class of the "Cesium Sun Sky", loading it if necessary.
* Used for spawning the CesiumSunSky.
*/
static UClass* GetCesiumSunSkyClass();
/**
* Gets the class of the "Dynamic Pawn" blueprint, loading it if necessary.
* Used for spawning the DynamicPawn.
*/
static UClass* GetDynamicPawnBlueprintClass();
};

View File

@ -0,0 +1,43 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumEditorReparentHandler.h"
#include "Cesium3DTileset.h"
#include "CesiumGlobeAnchorComponent.h"
#include "CesiumSubLevelComponent.h"
#include "Engine/Engine.h"
CesiumEditorReparentHandler::CesiumEditorReparentHandler() {
if (GEngine) {
this->_subscription = GEngine->OnLevelActorAttached().AddRaw(
this,
&CesiumEditorReparentHandler::OnLevelActorAttached);
}
}
CesiumEditorReparentHandler::~CesiumEditorReparentHandler() {
if (GEngine) {
GEngine->OnLevelActorAttached().Remove(this->_subscription);
this->_subscription.Reset();
}
}
void CesiumEditorReparentHandler::OnLevelActorAttached(
AActor* Actor,
const AActor* Parent) {
ACesium3DTileset* Tileset = Cast<ACesium3DTileset>(Actor);
if (IsValid(Tileset)) {
Tileset->InvalidateResolvedGeoreference();
}
UCesiumGlobeAnchorComponent* GlobeAnchor =
Actor->FindComponentByClass<UCesiumGlobeAnchorComponent>();
if (IsValid(GlobeAnchor)) {
GlobeAnchor->ResolveGeoreference(true);
}
UCesiumSubLevelComponent* SubLevel =
Actor->FindComponentByClass<UCesiumSubLevelComponent>();
if (IsValid(SubLevel)) {
SubLevel->ResolveGeoreference(true);
}
}

View File

@ -0,0 +1,24 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Delegates/IDelegateInstance.h"
class AActor;
/**
* Detects when Actors are reparented in the Editor by subscribing to
* GEngine::OnLevelActorAttached and handling it appropriately. For example,
* when a Cesium3DTileset's parent changes, we need to re-resolve its
* CesiumGeoreference.
*/
class CesiumEditorReparentHandler {
public:
CesiumEditorReparentHandler();
~CesiumEditorReparentHandler();
private:
void OnLevelActorAttached(AActor* Actor, const AActor* Parent);
FDelegateHandle _subscription;
};

View File

@ -0,0 +1,15 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumEditorSettings.h"
#include "CesiumSourceControl.h"
UCesiumEditorSettings::UCesiumEditorSettings(
const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer) {}
void UCesiumEditorSettings::Save() {
CesiumSourceControl::PromptToCheckoutConfigFile(
this->GetClass()->GetConfigName());
this->Modify();
this->SaveConfig();
}

View File

@ -0,0 +1,43 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumIonServer.h"
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "CesiumEditorSettings.generated.h"
/**
* Stores Editor settings for the Cesium plugin.
*/
UCLASS(Config = EditorPerProjectUserSettings, meta = (DisplayName = "Cesium"))
class UCesiumEditorSettings : public UDeveloperSettings {
GENERATED_UCLASS_BODY()
public:
UPROPERTY(
Config,
meta =
(DeprecatedProperty,
DeprecationMessage = "Set UserAccessTokenMap instead."))
FString UserAccessToken_DEPRECATED;
/**
* The Cesium ion server that is currently selected in the user interface.
*/
UPROPERTY(
Config,
EditAnywhere,
Category = "Cesium ion",
meta = (DisplayName = "Current Cesium ion Server"))
TSoftObjectPtr<UCesiumIonServer> CurrentCesiumIonServer;
UPROPERTY(
Config,
EditAnywhere,
Category = "Cesium ion",
meta = (DisplayName = "Token Map"))
TMap<TSoftObjectPtr<UCesiumIonServer>, FString> UserAccessTokenMap;
void Save();
};

View File

@ -0,0 +1,70 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumEditorSubLevelMutex.h"
#include "Async/Async.h"
#include "CesiumGeoreference.h"
#include "CesiumSubLevelComponent.h"
#include "CesiumSubLevelSwitcherComponent.h"
#include "Components/ActorComponent.h"
#include "Engine/World.h"
#include "LevelInstance/LevelInstanceActor.h"
CesiumEditorSubLevelMutex::CesiumEditorSubLevelMutex() {
this->_subscription = UActorComponent::MarkRenderStateDirtyEvent.AddRaw(
this,
&CesiumEditorSubLevelMutex::OnMarkRenderStateDirty);
}
CesiumEditorSubLevelMutex::~CesiumEditorSubLevelMutex() {
UActorComponent::MarkRenderStateDirtyEvent.Remove(this->_subscription);
this->_subscription.Reset();
}
void CesiumEditorSubLevelMutex::OnMarkRenderStateDirty(
UActorComponent& component) {
UCesiumSubLevelComponent* pSubLevel =
Cast<UCesiumSubLevelComponent>(&component);
if (pSubLevel == nullptr)
return;
ALevelInstance* pLevelInstance = Cast<ALevelInstance>(pSubLevel->GetOwner());
if (pLevelInstance == nullptr)
return;
ACesiumGeoreference* pGeoreference = pSubLevel->GetResolvedGeoreference();
if (pGeoreference == nullptr)
return;
UCesiumSubLevelSwitcherComponent* pSwitcher =
pGeoreference->FindComponentByClass<UCesiumSubLevelSwitcherComponent>();
if (pSwitcher == nullptr)
return;
bool needsTick = false;
if (!pLevelInstance->IsTemporarilyHiddenInEditor(true)) {
pSwitcher->SetTargetSubLevel(pLevelInstance);
needsTick = true;
} else if (pSwitcher->GetTargetSubLevel() == pLevelInstance) {
pSwitcher->SetTargetSubLevel(nullptr);
needsTick = true;
}
UWorld* pWorld = pGeoreference->GetWorld();
if (needsTick && pWorld && !pWorld->IsGameWorld()) {
// Other sub-levels won't be deactivated until
// UCesiumSubLevelSwitcherComponent next ticks. Normally that's no problem,
// but in some unusual cases it will never happen. For example, in UE 5.3,
// when running tests on CI with `-nullrhi`. Or if you close all your
// viewports in the Editor. So here we schedule a game thread task to ensure
// that _updateSubLevelStateEditor is called. It won't do any harm if we are
// ticking and it ends up being called multiple times.
TWeakObjectPtr<UCesiumSubLevelSwitcherComponent> pSwitcherWeak = pSwitcher;
AsyncTask(ENamedThreads::GameThread, [pSwitcherWeak]() {
UCesiumSubLevelSwitcherComponent* pSwitcher = pSwitcherWeak.Get();
if (pSwitcher) {
pSwitcher->_updateSubLevelStateEditor();
}
});
}
}

View File

@ -0,0 +1,24 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Delegates/IDelegateInstance.h"
class UActorComponent;
/**
* Ensures that only a single ALevelInstance with a UCesiumSubLevelComponent is
* visible in the Editor at any given time. It works by subscribing to the
* static MarkRenderStateDirtyEvent on UActorComponent, which is raised when the
* user toggles the visibility of an Actor in the Editor.
*/
class CesiumEditorSubLevelMutex {
public:
CesiumEditorSubLevelMutex();
~CesiumEditorSubLevelMutex();
private:
void OnMarkRenderStateDirty(UActorComponent& component);
FDelegateHandle _subscription;
};

View File

@ -0,0 +1,82 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumGeoreferenceCustomization.h"
#include "CesiumCustomization.h"
#include "CesiumDegreesMinutesSecondsEditor.h"
#include "CesiumGeoreference.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
FName FCesiumGeoreferenceCustomization::RegisteredLayoutName;
void FCesiumGeoreferenceCustomization::Register(
FPropertyEditorModule& PropertyEditorModule) {
RegisteredLayoutName = ACesiumGeoreference::StaticClass()->GetFName();
PropertyEditorModule.RegisterCustomClassLayout(
RegisteredLayoutName,
FOnGetDetailCustomizationInstance::CreateStatic(
&FCesiumGeoreferenceCustomization::MakeInstance));
}
void FCesiumGeoreferenceCustomization::Unregister(
FPropertyEditorModule& PropertyEditorModule) {
PropertyEditorModule.UnregisterCustomClassLayout(RegisteredLayoutName);
}
TSharedRef<IDetailCustomization>
FCesiumGeoreferenceCustomization::MakeInstance() {
return MakeShareable(new FCesiumGeoreferenceCustomization);
}
void FCesiumGeoreferenceCustomization::CustomizeDetails(
IDetailLayoutBuilder& DetailBuilder) {
IDetailCategoryBuilder& CesiumCategory = DetailBuilder.EditCategory("Cesium");
TSharedPtr<CesiumButtonGroup> pButtons =
CesiumCustomization::CreateButtonGroup();
pButtons->AddButtonForUFunction(
ACesiumGeoreference::StaticClass()->FindFunctionByName(
GET_FUNCTION_NAME_CHECKED(
ACesiumGeoreference,
PlaceGeoreferenceOriginHere)));
pButtons->AddButtonForUFunction(
ACesiumGeoreference::StaticClass()->FindFunctionByName(
GET_FUNCTION_NAME_CHECKED(ACesiumGeoreference, CreateSubLevelHere)));
pButtons->Finish(DetailBuilder, CesiumCategory);
CesiumCategory.AddProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, OriginPlacement));
TSharedPtr<class IPropertyHandle> LatitudeDecimalDegreesHandle =
DetailBuilder.GetProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, OriginLatitude));
IDetailPropertyRow& LatitudeRow =
CesiumCategory.AddProperty(LatitudeDecimalDegreesHandle);
LatitudeEditor = MakeShared<CesiumDegreesMinutesSecondsEditor>(
LatitudeDecimalDegreesHandle,
false);
LatitudeEditor->PopulateRow(LatitudeRow);
TSharedPtr<class IPropertyHandle> LongitudeDecimalDegreesHandle =
DetailBuilder.GetProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, OriginLongitude));
IDetailPropertyRow& LongitudeRow =
CesiumCategory.AddProperty(LongitudeDecimalDegreesHandle);
LongitudeEditor = MakeShared<CesiumDegreesMinutesSecondsEditor>(
LongitudeDecimalDegreesHandle,
true);
LongitudeEditor->PopulateRow(LongitudeRow);
CesiumCategory.AddProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, OriginHeight));
CesiumCategory.AddProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, Scale));
CesiumCategory.AddProperty(
GET_MEMBER_NAME_CHECKED(ACesiumGeoreference, ShowLoadRadii));
}

View File

@ -0,0 +1,27 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumDegreesMinutesSecondsEditor.h"
#include "IDetailCustomization.h"
/**
* An implementation of the IDetailCustomization interface that customizes
* the Details View of a CesiumGeoreference. It is registered in
* FCesiumEditorModule::StartupModule.
*/
class FCesiumGeoreferenceCustomization : public IDetailCustomization {
public:
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
static void Register(FPropertyEditorModule& PropertyEditorModule);
static void Unregister(FPropertyEditorModule& PropertyEditorModule);
static TSharedRef<IDetailCustomization> MakeInstance();
private:
TSharedPtr<CesiumDegreesMinutesSecondsEditor> LongitudeEditor;
TSharedPtr<CesiumDegreesMinutesSecondsEditor> LatitudeEditor;
static FName RegisteredLayoutName;
};

View File

@ -0,0 +1,298 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumGlobeAnchorCustomization.h"
#include "CesiumCustomization.h"
#include "CesiumDegreesMinutesSecondsEditor.h"
#include "CesiumGeoreference.h"
#include "CesiumGlobeAnchorComponent.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "IDetailGroup.h"
#include "Widgets/SToolTip.h"
FName FCesiumGlobeAnchorCustomization::RegisteredLayoutName;
void FCesiumGlobeAnchorCustomization::Register(
FPropertyEditorModule& PropertyEditorModule) {
RegisteredLayoutName = UCesiumGlobeAnchorComponent::StaticClass()->GetFName();
PropertyEditorModule.RegisterCustomClassLayout(
RegisteredLayoutName,
FOnGetDetailCustomizationInstance::CreateStatic(
&FCesiumGlobeAnchorCustomization::MakeInstance));
}
void FCesiumGlobeAnchorCustomization::Unregister(
FPropertyEditorModule& PropertyEditorModule) {
PropertyEditorModule.UnregisterCustomClassLayout(RegisteredLayoutName);
}
TSharedRef<IDetailCustomization>
FCesiumGlobeAnchorCustomization::MakeInstance() {
return MakeShareable(new FCesiumGlobeAnchorCustomization);
}
void FCesiumGlobeAnchorCustomization::CustomizeDetails(
IDetailLayoutBuilder& DetailBuilder) {
DetailBuilder.GetObjectsBeingCustomized(this->SelectedObjects);
IDetailCategoryBuilder& CesiumCategory = DetailBuilder.EditCategory("Cesium");
TSharedPtr<CesiumButtonGroup> pButtons =
CesiumCustomization::CreateButtonGroup();
pButtons->AddButtonForUFunction(
UCesiumGlobeAnchorComponent::StaticClass()->FindFunctionByName(
GET_FUNCTION_NAME_CHECKED(
UCesiumGlobeAnchorComponent,
SnapLocalUpToEllipsoidNormal)));
pButtons->AddButtonForUFunction(
UCesiumGlobeAnchorComponent::StaticClass()->FindFunctionByName(
GET_FUNCTION_NAME_CHECKED(
UCesiumGlobeAnchorComponent,
SnapToEastSouthUp)));
pButtons->Finish(DetailBuilder, CesiumCategory);
CesiumCategory.AddProperty(
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorComponent, Georeference));
CesiumCategory.AddProperty(GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorComponent,
ResolvedGeoreference));
CesiumCategory.AddProperty(GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorComponent,
AdjustOrientationForGlobeWhenMoving));
CesiumCategory.AddProperty(GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorComponent,
TeleportWhenUpdatingTransform));
this->UpdateDerivedProperties();
this->CreatePositionLongitudeLatitudeHeight(DetailBuilder, CesiumCategory);
this->CreatePositionEarthCenteredEarthFixed(DetailBuilder, CesiumCategory);
this->CreateRotationEastSouthUp(DetailBuilder, CesiumCategory);
}
void FCesiumGlobeAnchorCustomization::CreatePositionEarthCenteredEarthFixed(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category) {
IDetailGroup& Group = CesiumCustomization::CreateGroup(
Category,
"PositionEarthCenteredEarthFixed",
FText::FromString("Position (Earth-Centered, Earth-Fixed)"),
false,
true);
TArrayView<UObject*> View = this->DerivedPointers;
TSharedPtr<IPropertyHandle> XProperty = DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, X));
TSharedPtr<IPropertyHandle> YProperty = DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Y));
TSharedPtr<IPropertyHandle> ZProperty = DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Z));
Group.AddPropertyRow(XProperty.ToSharedRef());
Group.AddPropertyRow(YProperty.ToSharedRef());
Group.AddPropertyRow(ZProperty.ToSharedRef());
}
void FCesiumGlobeAnchorCustomization::CreatePositionLongitudeLatitudeHeight(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category) {
IDetailGroup& Group = CesiumCustomization::CreateGroup(
Category,
"PositionLatitudeLongitudeHeight",
FText::FromString("Position (Latitude, Longitude, Height)"),
false,
true);
TArrayView<UObject*> View = this->DerivedPointers;
TSharedPtr<IPropertyHandle> LatitudeProperty =
DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Latitude));
TSharedPtr<IPropertyHandle> LongitudeProperty =
DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Longitude));
TSharedPtr<IPropertyHandle> HeightProperty =
DetailBuilder.AddObjectPropertyData(
View,
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Height));
IDetailPropertyRow& LatitudeRow =
Group.AddPropertyRow(LatitudeProperty.ToSharedRef());
LatitudeEditor =
MakeShared<CesiumDegreesMinutesSecondsEditor>(LatitudeProperty, false);
LatitudeEditor->PopulateRow(LatitudeRow);
IDetailPropertyRow& LongitudeRow =
Group.AddPropertyRow(LongitudeProperty.ToSharedRef());
LongitudeEditor =
MakeShared<CesiumDegreesMinutesSecondsEditor>(LongitudeProperty, true);
LongitudeEditor->PopulateRow(LongitudeRow);
Group.AddPropertyRow(HeightProperty.ToSharedRef());
}
void FCesiumGlobeAnchorCustomization::CreateRotationEastSouthUp(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category) {
IDetailGroup& Group = CesiumCustomization::CreateGroup(
Category,
"RotationEastSouthUp",
FText::FromString("Rotation (East-South-Up)"),
false,
true);
this->UpdateDerivedProperties();
TArrayView<UObject*> EastSouthUpPointerView = this->DerivedPointers;
TSharedPtr<IPropertyHandle> RollProperty =
DetailBuilder.AddObjectPropertyData(EastSouthUpPointerView, "Roll");
TSharedPtr<IPropertyHandle> PitchProperty =
DetailBuilder.AddObjectPropertyData(EastSouthUpPointerView, "Pitch");
TSharedPtr<IPropertyHandle> YawProperty =
DetailBuilder.AddObjectPropertyData(EastSouthUpPointerView, "Yaw");
Group.AddPropertyRow(RollProperty.ToSharedRef());
Group.AddPropertyRow(PitchProperty.ToSharedRef());
Group.AddPropertyRow(YawProperty.ToSharedRef());
}
void FCesiumGlobeAnchorCustomization::UpdateDerivedProperties() {
this->DerivedObjects.SetNum(this->SelectedObjects.Num());
this->DerivedPointers.SetNum(DerivedObjects.Num());
for (int i = 0; i < this->SelectedObjects.Num(); ++i) {
if (!IsValid(this->DerivedObjects[i].Get())) {
this->DerivedObjects[i] =
NewObject<UCesiumGlobeAnchorDerivedProperties>();
}
UCesiumGlobeAnchorComponent* GlobeAnchor =
Cast<UCesiumGlobeAnchorComponent>(this->SelectedObjects[i]);
this->DerivedObjects[i]->Initialize(GlobeAnchor);
this->DerivedPointers[i] = this->DerivedObjects[i].Get();
}
}
void UCesiumGlobeAnchorDerivedProperties::PostEditChangeProperty(
FPropertyChangedEvent& PropertyChangedEvent) {
Super::PostEditChangeProperty(PropertyChangedEvent);
if (!PropertyChangedEvent.Property) {
return;
}
FName propertyName = PropertyChangedEvent.Property->GetFName();
if (propertyName ==
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, X) ||
propertyName ==
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Y) ||
propertyName ==
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Z)) {
this->GlobeAnchor->Modify();
this->GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(
FVector(this->X, this->Y, this->Z));
} else if (true) {
if (propertyName == GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Longitude) ||
propertyName == GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Latitude) ||
propertyName == GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Height)) {
this->GlobeAnchor->Modify();
this->GlobeAnchor->MoveToLongitudeLatitudeHeight(
FVector(this->Longitude, this->Latitude, this->Height));
} else if (
propertyName == GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Pitch) ||
propertyName ==
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Yaw) ||
propertyName == GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Roll)) {
this->GlobeAnchor->Modify();
this->GlobeAnchor->SetEastSouthUpRotation(
FRotator(this->Pitch, this->Yaw, this->Roll).Quaternion());
}
}
}
bool UCesiumGlobeAnchorDerivedProperties::CanEditChange(
const FProperty* InProperty) const {
const FName Name = InProperty->GetFName();
// Valid georeference, nothing to disable
if (IsValid(this->GlobeAnchor->ResolveGeoreference())) {
return true;
}
return Name != GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Longitude) &&
Name != GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Latitude) &&
Name != GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Height) &&
Name != GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Pitch) &&
Name != GET_MEMBER_NAME_CHECKED(
UCesiumGlobeAnchorDerivedProperties,
Yaw) &&
Name !=
GET_MEMBER_NAME_CHECKED(UCesiumGlobeAnchorDerivedProperties, Roll);
}
void UCesiumGlobeAnchorDerivedProperties::Initialize(
UCesiumGlobeAnchorComponent* GlobeAnchorComponent) {
this->GlobeAnchor = GlobeAnchorComponent;
this->Tick(0.0f);
}
void UCesiumGlobeAnchorDerivedProperties::Tick(float DeltaTime) {
if (this->GlobeAnchor) {
FVector position = this->GlobeAnchor->GetEarthCenteredEarthFixedPosition();
this->X = position.X;
this->Y = position.Y;
this->Z = position.Z;
// We can't transform the GlobeAnchor's ECEF coordinates back to
// cartographic & rotation without a valid georeference to know what
// ellipsoid to use.
if (IsValid(this->GlobeAnchor->ResolveGeoreference())) {
FVector llh = this->GlobeAnchor->GetLongitudeLatitudeHeight();
this->Longitude = llh.X;
this->Latitude = llh.Y;
this->Height = llh.Z;
FQuat rotation = this->GlobeAnchor->GetEastSouthUpRotation();
FRotator rotator = rotation.Rotator();
this->Roll = rotator.Roll;
this->Pitch = rotator.Pitch;
this->Yaw = rotator.Yaw;
}
}
}
TStatId UCesiumGlobeAnchorDerivedProperties::GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(
UCesiumGlobeAnchorRotationEastSouthUp,
STATGROUP_Tickables);
}

View File

@ -0,0 +1,147 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumDegreesMinutesSecondsEditor.h"
#include "IDetailCustomization.h"
#include "TickableEditorObject.h"
#include "CesiumGlobeAnchorCustomization.generated.h"
class IDetailCategoryBuilder;
class UCesiumGlobeAnchorDerivedProperties;
/**
* An implementation of the IDetailCustomization interface that customizes
* the Details View of a UCesiumGlobeAnchorComponent. It is registered in
* FCesiumEditorModule::StartupModule.
*/
class FCesiumGlobeAnchorCustomization : public IDetailCustomization {
public:
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
static void Register(FPropertyEditorModule& PropertyEditorModule);
static void Unregister(FPropertyEditorModule& PropertyEditorModule);
static TSharedRef<IDetailCustomization> MakeInstance();
private:
void CreatePositionEarthCenteredEarthFixed(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category);
void CreatePositionLongitudeLatitudeHeight(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category);
void CreateRotationEastSouthUp(
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category);
void UpdateDerivedProperties();
TSharedPtr<CesiumDegreesMinutesSecondsEditor> LongitudeEditor;
TSharedPtr<CesiumDegreesMinutesSecondsEditor> LatitudeEditor;
TArray<TWeakObjectPtr<UObject>> SelectedObjects;
TArray<TObjectPtr<UCesiumGlobeAnchorDerivedProperties>> DerivedObjects;
TArray<UObject*> DerivedPointers;
static FName RegisteredLayoutName;
};
UCLASS()
class UCesiumGlobeAnchorDerivedProperties : public UObject,
public FTickableEditorObject {
GENERATED_BODY()
public:
UPROPERTY()
class UCesiumGlobeAnchorComponent* GlobeAnchor;
/**
* The Earth-Centered Earth-Fixed (ECEF) X-coordinate of this component in
* meters. The ECEF coordinate system's origin is at the center of the Earth
* and +X points to the intersection of the Equator (zero degrees latitude)
* and Prime Meridian (zero degrees longitude).
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double X = 0.0;
/**
* The Earth-Centered Earth-Fixed (ECEF) Y-coordinate of this component in
* meters. The ECEF coordinate system's origin is at the center of the Earth
* and +Y points to the intersection of the Equator (zero degrees latitude)
* and +90 degrees longitude.
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double Y = 0.0;
/**
* The Earth-Centered Earth-Fixed (ECEF) Z-coordinate of this component in
* meters. The ECEF coordinate system's origin is at the center of the Earth
* and +Z points up through the North Pole.
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double Z = 0.0;
/**
* The latitude in degrees.
*/
UPROPERTY(
EditAnywhere,
Category = "Cesium",
Meta = (ClampMin = -90.0, ClampMax = 90.0))
double Latitude = 0.0;
/**
* The longitude in degrees.
*/
UPROPERTY(
EditAnywhere,
Category = "Cesium",
Meta = (ClampMin = -180.0, ClampMax = 180.0))
double Longitude = 0.0;
/**
* The height in meters above the ellipsoid.
*
* Do not confuse the ellipsoid height with a geoid height or height above
* mean sea level, which can be tens of meters higher or lower depending on
* where in the world the object is located.
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double Height = 0.0;
/**
* The rotation around the right (Y) axis. Zero pitch means the look direction
* (+X) is level with the horizon. Positive pitch is looking up, negative
* pitch is looking down.
*/
UPROPERTY(
EditAnywhere,
Category = "Cesium",
Meta = (ClampMin = -89.9999, ClampMax = 89.9999))
double Pitch = 0.0;
/**
* The rotation around the up (Z) axis. Zero yaw means the look direction (+X)
* points East. Positive yaw rotates right toward South, while negative yaw
* rotates left toward North.
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double Yaw = 0.0;
/**
* The rotation around the forward (X) axis. Zero roll is upright. Positive
* roll is like tilting your head to the right (clockwise), while negative
* roll is tilting to the left (counter-clockwise).
*/
UPROPERTY(EditAnywhere, Category = "Cesium")
double Roll = 0.0;
virtual void PostEditChangeProperty(
struct FPropertyChangedEvent& PropertyChangedEvent) override;
virtual bool CanEditChange(const FProperty* InProperty) const override;
void Initialize(UCesiumGlobeAnchorComponent* GlobeAnchor);
// Inherited via FTickableEditorObject
void Tick(float DeltaTime) override;
TStatId GetStatId() const override;
};

View File

@ -0,0 +1,625 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonPanel.h"
#include "Cesium3DTilesSelection/Tile.h"
#include "Cesium3DTilesSelection/Tileset.h"
#include "Cesium3DTileset.h"
#include "CesiumCommands.h"
#include "CesiumEditor.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServerSelector.h"
#include "CesiumRuntime.h"
#include "Editor.h"
#include "EditorModeManager.h"
#include "EngineUtils.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "IonLoginPanel.h"
#include "IonQuickAddPanel.h"
#include "SelectCesiumIonToken.h"
#include "Styling/SlateStyleRegistry.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SHeader.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "Widgets/Views/SListView.h"
using namespace CesiumIonClient;
// Identifiers for the columns of the asset table view
static FName ColumnName_Name = "Name";
static FName ColumnName_Type = "Type";
static FName ColumnName_DateAdded = "DateAdded";
CesiumIonPanel::CesiumIonPanel()
: _pListView(nullptr),
_assets(),
_pSelection(nullptr),
_pLastServer(nullptr) {
this->_serverChangedDelegateHandle =
FCesiumEditorModule::serverManager().CurrentServerChanged.AddRaw(
this,
&CesiumIonPanel::OnServerChanged);
this->_sortColumnName = ColumnName_DateAdded;
this->_sortMode = EColumnSortMode::Type::Descending;
this->OnServerChanged();
}
CesiumIonPanel::~CesiumIonPanel() {
this->Subscribe(nullptr);
FCesiumEditorModule::serverManager().CurrentServerChanged.Remove(
this->_serverChangedDelegateHandle);
}
void CesiumIonPanel::Construct(const FArguments& InArgs) {
this->Subscribe(FCesiumEditorModule::serverManager().GetCurrentServer());
// A function that returns the lambda that is used for rendering
// the sort mode indicator of the header column: If sorting is
// currently done based on the given name, then this will
// return the current _sortMode. Otherwise, it will return
// the 'None' sort mode.
auto sortModeLambda = [this](const FName& columnName) {
return [this, columnName]() {
if (_sortColumnName != columnName) {
return EColumnSortMode::None;
}
return _sortMode;
};
};
this->_pListView =
SNew(SListView<TSharedPtr<Asset>>)
.ListItemsSource(&this->_assets)
.OnMouseButtonDoubleClick(this, &CesiumIonPanel::AddAsset)
.OnGenerateRow(this, &CesiumIonPanel::CreateAssetRow)
.OnSelectionChanged(this, &CesiumIonPanel::AssetSelected)
.HeaderRow(
SNew(SHeaderRow) +
SHeaderRow::Column(ColumnName_Name)
.DefaultLabel(FText::FromString(TEXT("Name")))
.SortMode_Lambda(sortModeLambda(ColumnName_Name))
.OnSort(FOnSortModeChanged::CreateSP(
this,
&CesiumIonPanel::OnSortChange)) +
SHeaderRow::Column(ColumnName_Type)
.DefaultLabel(FText::FromString(TEXT("Type")))
.SortMode_Lambda(sortModeLambda(ColumnName_Type))
.OnSort(FOnSortModeChanged::CreateSP(
this,
&CesiumIonPanel::OnSortChange)) +
SHeaderRow::Column(ColumnName_DateAdded)
.DefaultLabel(FText::FromString(TEXT("Date added")))
.SortMode_Lambda(sortModeLambda(ColumnName_DateAdded))
.OnSort(FOnSortModeChanged::CreateSP(
this,
&CesiumIonPanel::OnSortChange)));
TSharedPtr<SWidget> pDetails = this->AssetDetails();
// Create a splitter where the left shows the actual asset list
// (with the controls (search, refresh) on top), and the right
// shows the AssetDetails panel
// clang-format off
ChildSlot[
SNew(SSplitter).Orientation(EOrientation::Orient_Horizontal) +
SSplitter::Slot().Value(0.66f)
[
SNew(SVerticalBox) +
SVerticalBox::Slot().AutoHeight()
[
SNew(SHorizontalBox) +
SHorizontalBox::Slot().Padding(5.0f)[SNew(CesiumIonServerSelector)] +
// Add the refresh button at the upper left
SHorizontalBox::Slot().HAlign(HAlign_Left).Padding(5.0f)
[
SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(), "CesiumButtonText")
.ContentPadding(FMargin(1.0, 1.0))
.HAlign(EHorizontalAlignment::HAlign_Center)
.Text(FText::FromString(TEXT("Refresh")))
.ToolTipText(FText::FromString(TEXT("Refresh the asset list")))
.OnClicked_Lambda([this]() {
FCesiumEditorModule::serverManager().GetCurrentSession()->refreshAssets();
Refresh();
return FReply::Handled();
})
[
SNew(SImage).Image(
FCesiumEditorModule::GetStyle()->GetBrush(
TEXT("Cesium.Common.Refresh")))
]
] +
// Add the search bar at the upper right
SHorizontalBox::Slot().HAlign(HAlign_Right).Padding(5.0f)
[
SAssignNew(SearchBox, SSearchBox).OnTextChanged(
this, &CesiumIonPanel::OnSearchTextChange)
.MinDesiredWidth(200.f)
]
] +
SVerticalBox::Slot()
[
this->_pListView.ToSharedRef()
]
] +
SSplitter::Slot().Value(0.34f)
[
SNew(SBorder).Padding(10)
[
pDetails.ToSharedRef()
]
]
];
// clang-format on
FCesiumEditorModule::serverManager().GetCurrentSession()->refreshAssets();
}
void CesiumIonPanel::OnSortChange(
const EColumnSortPriority::Type SortPriority,
const FName& ColumnName,
const EColumnSortMode::Type Mode) {
if (_sortColumnName == ColumnName) {
if (_sortMode == EColumnSortMode::Type::None) {
_sortMode = EColumnSortMode::Type::Ascending;
} else if (_sortMode == EColumnSortMode::Type::Ascending) {
_sortMode = EColumnSortMode::Type::Descending;
} else {
_sortMode = EColumnSortMode::Type::None;
}
} else {
_sortColumnName = ColumnName;
_sortMode = EColumnSortMode::Type::Ascending;
}
Refresh();
}
void CesiumIonPanel::OnSearchTextChange(const FText& SearchText) {
_searchString = SearchText.ToString().TrimStartAndEnd();
Refresh();
}
static bool isSupportedTileset(const TSharedPtr<Asset>& pAsset) {
return pAsset && (pAsset->type == "3DTILES" || pAsset->type == "TERRAIN");
}
static bool isSupportedImagery(const TSharedPtr<Asset>& pAsset) {
return pAsset && pAsset->type == "IMAGERY";
}
TSharedRef<SWidget> CesiumIonPanel::AssetDetails() {
return SNew(SScrollBox).Visibility_Lambda([this]() {
return this->_pSelection ? EVisibility::Visible : EVisibility::Collapsed;
}) +
SScrollBox::Slot().Padding(
10,
10,
10,
0)[SNew(STextBlock)
.AutoWrapText(true)
.TextStyle(FCesiumEditorModule::GetStyle(), "Heading")
.Text_Lambda([this]() {
return FText::FromString(
UTF8_TO_TCHAR(this->_pSelection->name.c_str()));
})] +
SScrollBox::Slot()
.Padding(10, 5, 10, 10)
.HAlign(EHorizontalAlignment::HAlign_Fill)
[SNew(STextBlock).Text_Lambda([this]() {
return FText::FromString(UTF8_TO_TCHAR(
(std::string("(ID: ") +
std::to_string(this->_pSelection->id) + ")")
.c_str()));
})] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Center)
[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.Visibility_Lambda([this]() {
return isSupportedTileset(this->_pSelection)
? EVisibility::Visible
: EVisibility::Collapsed;
})
.HAlign(EHorizontalAlignment::HAlign_Center)
.Text(FText::FromString(TEXT("Add to Level")))
.OnClicked_Lambda([this]() {
this->AddAssetToLevel(this->_pSelection);
return FReply::Handled();
})] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Center)
[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.Visibility_Lambda([this]() {
return isSupportedImagery(this->_pSelection)
? EVisibility::Visible
: EVisibility::Collapsed;
})
.HAlign(EHorizontalAlignment::HAlign_Center)
.Text(FText::FromString(
TEXT("Use as Terrain Tileset Base Layer")))
.ToolTipText(FText::FromString(TEXT(
"Makes this asset the base overlay on the terrain tileset, underlying all others, by setting its MaterialLayerKey to 'Overlay0'. If the terrain tileset already has an 'Overlay0' it is removed. If no terrain tileset exists in the level, Cesium World Terrain is added.")))
.OnClicked_Lambda([this]() {
this->AddOverlayToTerrain(this->_pSelection, true);
return FReply::Handled();
})] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Center)
[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.Visibility_Lambda([this]() {
return isSupportedImagery(this->_pSelection)
? EVisibility::Visible
: EVisibility::Collapsed;
})
.HAlign(EHorizontalAlignment::HAlign_Center)
.Text(FText::FromString(TEXT("Drape Over Terrain Tileset")))
.ToolTipText(FText::FromString(TEXT(
"Adds this asset to any existing overlays on the terrain tileset by assigning it the first unused 'OverlayN` MaterialLayerKey. If no terrain tileset exists in the level, Cesium World Terrain is added.")))
.OnClicked_Lambda([this]() {
this->AddOverlayToTerrain(this->_pSelection, false);
return FReply::Handled();
})] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Center)
[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.Visibility_Lambda([this]() {
return !isSupportedTileset(this->_pSelection) &&
!isSupportedImagery(this->_pSelection)
? EVisibility::Visible
: EVisibility::Collapsed;
})
.HAlign(EHorizontalAlignment::HAlign_Center)
.Text(FText::FromString(
TEXT("This type of asset is not currently supported")))
.IsEnabled(false)] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Fill)
[SNew(STextBlock)
.TextStyle(
FCesiumEditorModule::GetStyle(),
"AssetDetailsFieldHeader")
.Text(FText::FromString(TEXT("Description")))] +
SScrollBox::Slot().Padding(
10,
0)[SNew(STextBlock)
.AutoWrapText(true)
.TextStyle(
FCesiumEditorModule::GetStyle(),
"AssetDetailsFieldValue")
.Text_Lambda([this]() {
return FText::FromString(UTF8_TO_TCHAR(
this->_pSelection->description.c_str()));
})] +
SScrollBox::Slot().Padding(10).HAlign(
EHorizontalAlignment::HAlign_Fill)
[SNew(STextBlock)
.TextStyle(
FCesiumEditorModule::GetStyle(),
"AssetDetailsFieldHeader")
.Text(FText::FromString(TEXT("Attribution")))] +
SScrollBox::Slot().Padding(
10,
0)[SNew(STextBlock)
.AutoWrapText(true)
.TextStyle(
FCesiumEditorModule::GetStyle(),
"AssetDetailsFieldValue")
.Text_Lambda([this]() {
return FText::FromString(UTF8_TO_TCHAR(
this->_pSelection->attribution.c_str()));
})];
}
/**
* @brief Returns a comparator for the property of an Asset that is
* associated with the given column name.
*
* @param columnName The column name
* @return The comparator, comparing is ascending order (comparing by
* the asset->name by default, if the given column name was not known)
*/
static std::function<bool(const TSharedPtr<Asset>&, const TSharedPtr<Asset>&)>
comparatorFor(const FName& columnName) {
if (columnName == ColumnName_Type) {
return [](const TSharedPtr<Asset>& a0, const TSharedPtr<Asset>& a1) {
return a0->type < a1->type;
};
}
if (columnName == ColumnName_DateAdded) {
return [](const TSharedPtr<Asset>& a0, const TSharedPtr<Asset>& a1) {
return a0->dateAdded < a1->dateAdded;
};
}
return [](const TSharedPtr<Asset>& a0, const TSharedPtr<Asset>& a1) {
return a0->name < a1->name;
};
}
void CesiumIonPanel::ApplyFilter() {
// UE_LOG(LogCesiumEditor, Warning, TEXT("ApplyFilter %s"), *_searchString);
if (_searchString.IsEmpty()) {
return;
}
this->_assets =
this->_assets.FilterByPredicate([this](const TSharedPtr<Asset>& Asset) {
// This mimics the behavior of the ion web UI, which
// searches for the given text in the name and description.
//
// Creating and using FString instances here instead of
// converting the _searchString to a std::string, because
// the 'FString::Contains' function does the desired
// case-INsensitive check by default.
FString Name = UTF8_TO_TCHAR(Asset->name.c_str());
if (Name.Contains(_searchString)) {
return true;
}
FString Description = UTF8_TO_TCHAR(Asset->description.c_str());
if (Description.Contains(_searchString)) {
return true;
}
return false;
});
}
void CesiumIonPanel::ApplySorting() {
// UE_LOG(LogCesiumEditor, Warning, TEXT("ApplySorting %s with %d"),
// *_sortColumnName.ToString(), _sortMode);
if (_sortMode == EColumnSortMode::Type::None) {
return;
}
auto baseComparator = comparatorFor(_sortColumnName);
if (_sortMode == EColumnSortMode::Type::Ascending) {
this->_assets.Sort(baseComparator);
} else {
this->_assets.Sort(
[&baseComparator](
const TSharedPtr<Asset>& a0,
const TSharedPtr<Asset>& a1) { return baseComparator(a1, a0); });
}
}
void CesiumIonPanel::Refresh() {
if (!this->_pListView)
return;
const Assets& assets =
FCesiumEditorModule::serverManager().GetCurrentSession()->getAssets();
this->_assets.SetNum(assets.items.size());
for (size_t i = 0; i < assets.items.size(); ++i) {
this->_assets[i] = MakeShared<Asset>(assets.items[i]);
}
ApplyFilter();
ApplySorting();
this->_pListView->RequestListRefresh();
}
void CesiumIonPanel::Tick(
const FGeometry& AllottedGeometry,
const double InCurrentTime,
const float InDeltaTime) {
getAsyncSystem().dispatchMainThreadTasks();
SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
}
void CesiumIonPanel::AssetSelected(
TSharedPtr<CesiumIonClient::Asset> item,
ESelectInfo::Type selectionType) {
this->_pSelection = item;
}
void CesiumIonPanel::Subscribe(UCesiumIonServer* pNewServer) {
if (this->_pLastServer) {
std::shared_ptr<CesiumIonSession> pLastSession =
FCesiumEditorModule::serverManager().GetSession(this->_pLastServer);
if (pLastSession) {
pLastSession->ConnectionUpdated.RemoveAll(this);
pLastSession->AssetsUpdated.RemoveAll(this);
}
}
this->_pLastServer = pNewServer;
if (pNewServer) {
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(pNewServer);
pSession->ConnectionUpdated.AddRaw(this, &CesiumIonPanel::Refresh);
pSession->AssetsUpdated.AddRaw(this, &CesiumIonPanel::Refresh);
}
}
void CesiumIonPanel::OnServerChanged() {
UCesiumIonServer* pNewServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
this->Subscribe(pNewServer);
this->Refresh();
}
void CesiumIonPanel::AddAsset(TSharedPtr<CesiumIonClient::Asset> item) {
if (isSupportedImagery(item)) {
// Don't add imagery on double-click, because we don't know if we should
// replace the base layer or add a new layer.
} else if (isSupportedTileset(item)) {
this->AddAssetToLevel(item);
} else {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Cannot add asset of type %s"),
UTF8_TO_TCHAR(item->type.c_str()));
}
}
void CesiumIonPanel::AddAssetToLevel(TSharedPtr<CesiumIonClient::Asset> item) {
SelectCesiumIonToken::SelectAndAuthorizeToken(
FCesiumEditorModule::serverManager().GetCurrentServer(),
{item->id})
.thenInMainThread([item](const std::optional<Token>& /*maybeToken*/) {
// If token selection was canceled, or if an error occurred while
// selecting the token, ignore it and create the tileset anyway. It's
// already been logged if necessary, and we can let the user sort out
// the problem using the resulting Troubleshooting panel.
ACesium3DTileset* pTileset =
FCesiumEditorModule::CreateTileset(item->name, item->id);
if (pTileset) {
pTileset->RerunConstructionScripts();
}
});
}
void CesiumIonPanel::AddOverlayToTerrain(
TSharedPtr<CesiumIonClient::Asset> item,
bool useAsBaseLayer) {
SelectCesiumIonToken::SelectAndAuthorizeToken(
FCesiumEditorModule::serverManager().GetCurrentServer(),
{item->id})
.thenInMainThread([useAsBaseLayer, item](const std::optional<Token>&) {
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
ULevel* pCurrentLevel = pCurrentWorld->GetCurrentLevel();
ACesium3DTileset* pTilesetActor =
FCesiumEditorModule::FindFirstTilesetSupportingOverlays();
if (!pTilesetActor) {
pTilesetActor =
FCesiumEditorModule::CreateTileset("Cesium World Terrain", 1);
}
UCesiumRasterOverlay* pOverlay =
useAsBaseLayer ? FCesiumEditorModule::AddBaseOverlay(
pTilesetActor,
item->name,
item->id)
: FCesiumEditorModule::AddOverlay(
pTilesetActor,
item->name,
item->id);
pTilesetActor->RerunConstructionScripts();
GEditor->SelectNone(true, false);
GEditor->SelectActor(pTilesetActor, true, true, true, true);
GEditor->SelectComponent(pOverlay, true, true, true);
});
}
namespace {
/**
* @brief Returns a short string indicating the given asset type.
*
* The input must be one of the strings indicating the type of
* an asset, as of https://cesium.com/docs/rest-api/#tag/Assets.
*
* If the input is not a known type, then an unspecified error
* indicator will be returned.
*
* @param assetType The asset type.
* @return The string.
*/
std::string assetTypeToString(const std::string& assetType) {
static std::map<std::string, std::string> lookup = {
{"3DTILES", "3D Tiles"},
{"GLTF", "glTF"},
{"IMAGERY", "Imagery"},
{"TERRAIN", "Terrain"},
{"CZML", "CZML"},
{"KML", "KML"},
{"GEOJSON", "GeoJSON"}};
auto it = lookup.find(assetType);
if (it != lookup.end()) {
return it->second;
}
return "(Unknown)";
}
/**
* @brief Format the given asset date into a date string.
*
* The given string is assumed to be in ISO8601 format, as returned
* from the `asset.dateAdded`. It will be returned as a string in
* the YYYY-MM-DD format. If the string cannot be parsed, it will
* be returned as-it-is.
*
* @param assetDate The asset date
* @return The formatted string
*/
FString formatDate(const std::string& assetDate) {
FString unrealDateString = UTF8_TO_TCHAR(assetDate.c_str());
FDateTime dateTime;
bool success = FDateTime::ParseIso8601(*unrealDateString, dateTime);
if (!success) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Could not parse date %s"),
UTF8_TO_TCHAR(assetDate.c_str()));
return UTF8_TO_TCHAR(assetDate.c_str());
}
return dateTime.ToString(TEXT("%Y-%m-%d"));
}
class AssetsTableRow : public SMultiColumnTableRow<TSharedPtr<Asset>> {
public:
void Construct(
const FArguments& InArgs,
const TSharedRef<STableViewBase>& InOwnerTableView,
const TSharedPtr<Asset>& pItem) {
this->_pItem = pItem;
SMultiColumnTableRow<TSharedPtr<Asset>>::Construct(
InArgs,
InOwnerTableView);
}
virtual TSharedRef<SWidget>
GenerateWidgetForColumn(const FName& InColumnName) override {
if (InColumnName == ColumnName_Name) {
return SNew(STextBlock)
.Text(FText::FromString(UTF8_TO_TCHAR(_pItem->name.c_str())));
} else if (InColumnName == ColumnName_Type) {
return SNew(STextBlock)
.Text(FText::FromString(
UTF8_TO_TCHAR(assetTypeToString(_pItem->type).c_str())));
} else if (InColumnName == ColumnName_DateAdded) {
return SNew(STextBlock)
.Text(FText::FromString(formatDate(_pItem->dateAdded)));
} else {
return SNew(STextBlock);
}
}
private:
TSharedPtr<Asset> _pItem;
};
} // namespace
TSharedRef<ITableRow> CesiumIonPanel::CreateAssetRow(
TSharedPtr<Asset> item,
const TSharedRef<STableViewBase>& list) {
return SNew(AssetsTableRow, list, item);
}

View File

@ -0,0 +1,107 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumIonClient/Assets.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Input/SSearchBox.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Views/SHeaderRow.h"
class FArguments;
class ITableRow;
class STableViewBase;
class UCesiumIonServer;
template <typename ItemType> class SListView;
class CesiumIonPanel : public SCompoundWidget {
SLATE_BEGIN_ARGS(CesiumIonPanel) {}
SLATE_END_ARGS()
CesiumIonPanel();
virtual ~CesiumIonPanel();
void Construct(const FArguments& InArgs);
void Refresh();
virtual void Tick(
const FGeometry& AllottedGeometry,
const double InCurrentTime,
const float InDeltaTime) override;
private:
TSharedRef<SWidget> AssetDetails();
TSharedRef<ITableRow> CreateAssetRow(
TSharedPtr<CesiumIonClient::Asset> item,
const TSharedRef<STableViewBase>& list);
void AddAssetToLevel(TSharedPtr<CesiumIonClient::Asset> item);
void AddOverlayToTerrain(
TSharedPtr<CesiumIonClient::Asset> item,
bool useAsBaseLayer);
void AddAsset(TSharedPtr<CesiumIonClient::Asset> item);
void AssetSelected(
TSharedPtr<CesiumIonClient::Asset> item,
ESelectInfo::Type selectionType);
void Subscribe(UCesiumIonServer* pNewServer);
void OnServerChanged();
/**
* Filter the current _assets array, based on the current _searchString.
* This will replace the _assets array with one that only contains
* assets whose name or description contain the search string
*/
void ApplyFilter();
/**
* Sort the current _assets array, based on the current _sortColumnName
* and _sortMode, before using it to populate the list view.
*/
void ApplySorting();
/**
* Will be called whenever one header of the asset list view is
* clicked, and update the current _sortColumnName and _sortMode
* accordingly.
*/
void OnSortChange(
EColumnSortPriority::Type SortPriority,
const FName& ColumnName,
EColumnSortMode::Type NewSortMode);
/**
* Will be called whenever the contents of the _SearchBox changes,
* store the corresponding _searchString, and refresh the view.
*/
void OnSearchTextChange(const FText& SearchText);
FDelegateHandle _serverChangedDelegateHandle;
TSharedPtr<SListView<TSharedPtr<CesiumIonClient::Asset>>> _pListView;
TArray<TSharedPtr<CesiumIonClient::Asset>> _assets;
TSharedPtr<CesiumIonClient::Asset> _pSelection;
TObjectPtr<UCesiumIonServer> _pLastServer;
/**
* The column name based on which the main assets list view is currently
* sorted.
*/
FName _sortColumnName;
/**
* The sort mode that is currently applied to the _sortColumnName.
*/
EColumnSortMode::Type _sortMode = EColumnSortMode::Type::None;
/**
* The search box for entering the _searchString
*/
TSharedPtr<SSearchBox> SearchBox;
/**
* The string that is currently entered in the SearchBox,
* (trimmed from whitespace), used for filtering the asset
* list in ApplyFilter.
*/
FString _searchString;
};

View File

@ -0,0 +1,44 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonServerDisplay.h"
#include "CesiumEditor.h"
#include "CesiumIonServer.h"
#include "Editor.h"
#include "PropertyCustomizationHelpers.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Text/STextBlock.h"
void CesiumIonServerDisplay::Construct(const FArguments& InArgs) {
UCesiumIonServer* pServer = InArgs._Server;
ChildSlot
[SNew(SHorizontalBox) +
SHorizontalBox::Slot()
.AutoWidth()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(5.0f)[SNew(STextBlock)
.Text(FText::FromString("Cesium ion Server:"))] +
SHorizontalBox::Slot()
.AutoWidth()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(5.0f)[SNew(SEditableTextBox)
.IsEnabled(false)
.Padding(5.0f)
.Text(FText::FromString(pServer->DisplayName))] +
SHorizontalBox::Slot()
.AutoWidth()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(5.0f)[PropertyCustomizationHelpers::MakeBrowseButton(
FSimpleDelegate::
CreateSP(this, &CesiumIonServerDisplay::OnBrowseForServer),
FText::FromString(
"Show this Cesium ion Server in the Content Browser."),
true,
false)]];
}
void CesiumIonServerDisplay::OnBrowseForServer() {
TArray<UObject*> Objects;
Objects.Add(FCesiumEditorModule::serverManager().GetCurrentServer());
GEditor->SyncBrowserToObjects(Objects);
}

View File

@ -0,0 +1,19 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/SCompoundWidget.h"
class FArguments;
class UCesiumIonServer;
class CesiumIonServerDisplay : public SCompoundWidget {
SLATE_BEGIN_ARGS(CesiumIonServerDisplay) {}
SLATE_ARGUMENT(UCesiumIonServer*, Server)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
void OnBrowseForServer();
};

View File

@ -0,0 +1,245 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonServerManager.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Cesium3DTileset.h"
#include "CesiumEditorSettings.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServer.h"
#include "CesiumIonSession.h"
#include "CesiumRuntime.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumSourceControl.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "FileHelpers.h"
CesiumIonServerManager::CesiumIonServerManager() noexcept {
FAssetRegistryModule& AssetRegistryModule =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
AssetRegistryModule.GetRegistry().OnAssetAdded().AddRaw(
this,
&CesiumIonServerManager::OnAssetAdded);
AssetRegistryModule.GetRegistry().OnAssetRemoved().AddRaw(
this,
&CesiumIonServerManager::OnAssetRemoved);
AssetRegistryModule.GetRegistry().OnAssetUpdated().AddRaw(
this,
&CesiumIonServerManager::OnAssetUpdated);
}
CesiumIonServerManager::~CesiumIonServerManager() noexcept {
FAssetRegistryModule* pAssetRegistryModule =
FModuleManager::GetModulePtr<FAssetRegistryModule>("AssetRegistry");
if (pAssetRegistryModule) {
pAssetRegistryModule->GetRegistry().OnAssetAdded().RemoveAll(this);
pAssetRegistryModule->GetRegistry().OnAssetRemoved().RemoveAll(this);
pAssetRegistryModule->GetRegistry().OnAssetUpdated().RemoveAll(this);
}
}
void CesiumIonServerManager::Initialize() {
UCesiumRuntimeSettings* pSettings =
GetMutableDefault<UCesiumRuntimeSettings>();
if (pSettings) {
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (!pSettings->DefaultIonAccessTokenId_DEPRECATED.IsEmpty() ||
!pSettings->DefaultIonAccessToken_DEPRECATED.IsEmpty()) {
UCesiumIonServer* pServer = UCesiumIonServer::GetDefaultServer();
pServer->Modify();
pServer->DefaultIonAccessTokenId =
std::move(pSettings->DefaultIonAccessTokenId_DEPRECATED);
pSettings->DefaultIonAccessTokenId_DEPRECATED.Empty();
pServer->DefaultIonAccessToken =
std::move(pSettings->DefaultIonAccessToken_DEPRECATED);
pSettings->DefaultIonAccessToken_DEPRECATED.Empty();
UEditorLoadingAndSavingUtils::SavePackages({pServer->GetPackage()}, true);
CesiumSourceControl::PromptToCheckoutConfigFile(
pSettings->GetDefaultConfigFilename());
pSettings->Modify();
pSettings->TryUpdateDefaultConfigFile();
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
UCesiumEditorSettings* pEditorSettings =
GetMutableDefault<UCesiumEditorSettings>();
if (pEditorSettings) {
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (!pEditorSettings->UserAccessToken_DEPRECATED.IsEmpty()) {
UCesiumIonServer* pServer = UCesiumIonServer::GetDefaultServer();
pEditorSettings->UserAccessTokenMap.Add(
pServer,
pEditorSettings->UserAccessToken_DEPRECATED);
pEditorSettings->UserAccessToken_DEPRECATED.Empty();
pEditorSettings->Save();
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
UCesiumIonServer::SetServerForNewObjects(this->GetCurrentServer());
}
void CesiumIonServerManager::ResumeAll() {
const TArray<TWeakObjectPtr<UCesiumIonServer>>& servers =
this->GetServerList();
for (const TWeakObjectPtr<UCesiumIonServer>& pWeakServer : servers) {
UCesiumIonServer* pServer = pWeakServer.Get();
if (pServer) {
std::shared_ptr<CesiumIonSession> pSession = this->GetSession(pServer);
pSession->resume();
pSession->refreshProfileIfNeeded();
}
}
}
std::shared_ptr<CesiumIonSession>
CesiumIonServerManager::GetSession(UCesiumIonServer* Server) {
if (Server == nullptr)
return nullptr;
ServerSession* Found = this->_sessions.FindByPredicate(
[Server](const ServerSession& ServerSession) {
return ServerSession.Server == Server;
});
if (!Found) {
std::shared_ptr<CesiumIonSession> pSession =
std::make_shared<CesiumIonSession>(
getAsyncSystem(),
getAssetAccessor(),
TWeakObjectPtr<UCesiumIonServer>(Server));
int32 index = this->_sessions.Add(
ServerSession{TWeakObjectPtr<UCesiumIonServer>(Server), pSession});
Found = &this->_sessions[index];
}
return Found->Session;
}
std::shared_ptr<CesiumIonSession> CesiumIonServerManager::GetCurrentSession() {
return this->GetSession(this->GetCurrentServer());
}
const TArray<TWeakObjectPtr<UCesiumIonServer>>&
CesiumIonServerManager::GetServerList() {
this->RefreshServerList();
return this->_servers;
}
void CesiumIonServerManager::RefreshServerList() {
this->_servers.Empty();
TArray<FAssetData> CesiumIonServers;
FAssetRegistryModule& AssetRegistryModule =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
AssetRegistryModule.Get().GetAssetsByClass(
UCesiumIonServer::StaticClass()->GetClassPathName(),
CesiumIonServers);
for (const FAssetData& ServerAsset : CesiumIonServers) {
this->_servers.Add(Cast<UCesiumIonServer>(ServerAsset.GetAsset()));
}
this->ServerListChanged.Broadcast();
}
UCesiumIonServer* CesiumIonServerManager::GetCurrentServer() {
UCesiumEditorSettings* pSettings = GetMutableDefault<UCesiumEditorSettings>();
if (!pSettings) {
return UCesiumIonServer::GetDefaultServer();
}
UCesiumIonServer* pServer =
pSettings->CurrentCesiumIonServer.LoadSynchronous();
if (pServer == nullptr) {
pServer = UCesiumIonServer::GetDefaultServer();
pSettings->CurrentCesiumIonServer = pServer;
pSettings->Save();
}
return pServer;
}
void CesiumIonServerManager::SetCurrentServer(UCesiumIonServer* pServer) {
UCesiumEditorSettings* pSettings = GetMutableDefault<UCesiumEditorSettings>();
if (pSettings) {
pSettings->CurrentCesiumIonServer = pServer;
pSettings->Save();
}
if (UCesiumIonServer::GetServerForNewObjects() != pServer) {
UCesiumIonServer::SetServerForNewObjects(pServer);
CurrentServerChanged.Broadcast();
}
}
void CesiumIonServerManager::OnAssetAdded(const FAssetData& asset) {
if (asset.AssetClassPath !=
UCesiumIonServer::StaticClass()->GetClassPathName())
return;
this->RefreshServerList();
}
void CesiumIonServerManager::OnAssetRemoved(const FAssetData& asset) {
if (asset.AssetClassPath !=
UCesiumIonServer::StaticClass()->GetClassPathName())
return;
this->RefreshServerList();
UCesiumIonServer* pServer = Cast<UCesiumIonServer>(asset.GetAsset());
if (pServer && this->GetCurrentServer() == pServer) {
// Current server is being removed, so select a different one.
TWeakObjectPtr<UCesiumIonServer>* ppNewServer =
this->_servers.FindByPredicate(
[pServer](const TWeakObjectPtr<UCesiumIonServer>& pCandidate) {
return pCandidate.Get() != pServer;
});
if (ppNewServer != nullptr) {
this->SetCurrentServer(ppNewServer->Get());
} else {
this->SetCurrentServer(nullptr);
}
}
}
void CesiumIonServerManager::OnAssetUpdated(const FAssetData& asset) {
if (!GEditor)
return;
if (asset.AssetClassPath !=
UCesiumIonServer::StaticClass()->GetClassPathName())
return;
// When a Cesium ion Server definition changes, refresh any objects that use
// it.
UCesiumIonServer* pServer = Cast<UCesiumIonServer>(asset.GetAsset());
if (!pServer)
return;
UWorld* pCurrentWorld = GEditor->GetEditorWorldContext().World();
if (!pCurrentWorld)
return;
for (TActorIterator<ACesium3DTileset> it(pCurrentWorld); it; ++it) {
if (it->GetCesiumIonServer() == pServer) {
it->RefreshTileset();
} else {
TArray<UCesiumIonRasterOverlay*> rasterOverlays;
it->GetComponents<UCesiumIonRasterOverlay>(rasterOverlays);
for (UCesiumIonRasterOverlay* pOverlay : rasterOverlays) {
if (pOverlay->CesiumIonServer == pServer)
pOverlay->Refresh();
}
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "UObject/WeakObjectPtrTemplates.h"
#include <memory>
class UCesiumIonServer;
class CesiumIonSession;
DECLARE_MULTICAST_DELEGATE(FCesiumIonServerChanged);
class CESIUMEDITOR_API CesiumIonServerManager {
public:
CesiumIonServerManager() noexcept;
~CesiumIonServerManager() noexcept;
void Initialize();
void ResumeAll();
std::shared_ptr<CesiumIonSession> GetSession(UCesiumIonServer* Server);
std::shared_ptr<CesiumIonSession> GetCurrentSession();
const TArray<TWeakObjectPtr<UCesiumIonServer>>& GetServerList();
void RefreshServerList();
UCesiumIonServer* GetCurrentServer();
void SetCurrentServer(UCesiumIonServer* pServer);
FCesiumIonServerChanged ServerListChanged;
FCesiumIonServerChanged CurrentServerChanged;
private:
void OnAssetAdded(const FAssetData& asset);
void OnAssetRemoved(const FAssetData& asset);
void OnAssetUpdated(const FAssetData& asset);
struct ServerSession {
TWeakObjectPtr<UCesiumIonServer> Server;
std::shared_ptr<CesiumIonSession> Session;
};
TArray<ServerSession> _sessions;
TArray<TWeakObjectPtr<UCesiumIonServer>> _servers;
};

View File

@ -0,0 +1,117 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonServerSelector.h"
#include "CesiumEditor.h"
#include "CesiumIonServer.h"
#include "Editor.h"
#include "PropertyCustomizationHelpers.h"
CesiumIonServerSelector::CesiumIonServerSelector() {
FCesiumEditorModule::serverManager().CurrentServerChanged.AddRaw(
this,
&CesiumIonServerSelector::OnCurrentServerChanged);
}
CesiumIonServerSelector::~CesiumIonServerSelector() {
FCesiumEditorModule::serverManager().CurrentServerChanged.RemoveAll(this);
}
void CesiumIonServerSelector::Construct(const FArguments& InArgs) {
ChildSlot
[SNew(SHorizontalBox) +
SHorizontalBox::Slot().FillWidth(1.0f).VAlign(
EVerticalAlignment::VAlign_Center)
[SAssignNew(_pCombo, SComboBox<TWeakObjectPtr<UCesiumIonServer>>)
.OptionsSource(
&FCesiumEditorModule::serverManager().GetServerList())
.OnGenerateWidget(
this,
&CesiumIonServerSelector::OnGenerateServerEntry)
.OnSelectionChanged(
this,
&CesiumIonServerSelector::OnServerSelectionChanged)
.Content()
[SNew(STextBlock)
.Text(
this,
&CesiumIonServerSelector::GetServerValueAsText)]] +
SHorizontalBox::Slot().AutoWidth().VAlign(
EVerticalAlignment::VAlign_Center)
[PropertyCustomizationHelpers::MakeBrowseButton(
FSimpleDelegate::
CreateSP(this, &CesiumIonServerSelector::OnBrowseForServer),
FText::FromString(
"Show this Cesium ion Server in the Content Browser."),
true,
false)]];
}
namespace {
FText GetNameFromCesiumIonServerAsset(
const TWeakObjectPtr<UCesiumIonServer>& pServer) {
if (!pServer.IsValid())
return FText::FromString("Error: No Cesium ion server configured.");
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(pServer.Get());
// Get the profile here, which will trigger it to load if it hasn't been
// loaded already.
const CesiumIonClient::Profile& profile = pSession->getProfile();
FString prefix;
FString suffix;
if (pSession->isConnecting() || pSession->isResuming()) {
suffix = " (connecting...)";
} else if (pSession->isLoadingProfile()) {
suffix = " (loading profile...)";
} else if (pSession->isConnected() && pSession->isProfileLoaded()) {
prefix = FString(UTF8_TO_TCHAR(profile.username.c_str()));
prefix += " @ ";
} else {
suffix = " (not connected)";
}
return FText::FromString(
prefix +
(pServer->DisplayName.IsEmpty() ? pServer->GetPackage()->GetName()
: pServer->DisplayName) +
suffix);
}
} // namespace
FText CesiumIonServerSelector::GetServerValueAsText() const {
UCesiumIonServer* pServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
return GetNameFromCesiumIonServerAsset(pServer);
}
TSharedRef<SWidget> CesiumIonServerSelector::OnGenerateServerEntry(
TWeakObjectPtr<UCesiumIonServer> pServerAsset) {
return SNew(STextBlock).Text_Lambda([pServerAsset]() {
return GetNameFromCesiumIonServerAsset(pServerAsset);
});
}
void CesiumIonServerSelector::OnServerSelectionChanged(
TWeakObjectPtr<UCesiumIonServer> InItem,
ESelectInfo::Type InSeletionInfo) {
FCesiumEditorModule::serverManager().SetCurrentServer(InItem.Get());
FCesiumEditorModule::serverManager().GetCurrentSession()->resume();
}
void CesiumIonServerSelector::OnBrowseForServer() {
TArray<UObject*> Objects;
Objects.Add(FCesiumEditorModule::serverManager().GetCurrentServer());
GEditor->SyncBrowserToObjects(Objects);
}
void CesiumIonServerSelector::OnCurrentServerChanged() {
if (this->_pCombo) {
this->_pCombo->SetSelectedItem(
FCesiumEditorModule::serverManager().GetCurrentServer());
}
}

View File

@ -0,0 +1,33 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/Input/SComboBox.h"
#include "Widgets/SCompoundWidget.h"
class FArguments;
class UCesiumIonServer;
class CesiumIonServerSelector : public SCompoundWidget {
SLATE_BEGIN_ARGS(CesiumIonServerSelector) {}
SLATE_END_ARGS()
CesiumIonServerSelector();
virtual ~CesiumIonServerSelector();
void Construct(const FArguments& InArgs);
private:
TSharedRef<SWidget>
OnGenerateServerEntry(TWeakObjectPtr<UCesiumIonServer> pServerAsset);
FText GetServerValueAsText() const;
void OnServerSelectionChanged(
TWeakObjectPtr<UCesiumIonServer> InItem,
ESelectInfo::Type InSeletionInfo);
void OnBrowseForServer();
void OnCurrentServerChanged();
TSharedPtr<SComboBox<TWeakObjectPtr<UCesiumIonServer>>> _pCombo;
};

View File

@ -0,0 +1,629 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonSession.h"
#include "CesiumEditor.h"
#include "CesiumEditorSettings.h"
#include "CesiumIonServer.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumSourceControl.h"
#include "CesiumUtility/Uri.h"
#include "FileHelpers.h"
#include "HAL/PlatformProcess.h"
#include "Misc/App.h"
using namespace CesiumAsync;
using namespace CesiumIonClient;
namespace {
template <typename T> void logResponseErrors(const Response<T>& response) {
if (!response.errorCode.empty() && !response.errorMessage.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("%s (Code %s)"),
UTF8_TO_TCHAR(response.errorMessage.c_str()),
UTF8_TO_TCHAR(response.errorCode.c_str()));
} else if (!response.errorCode.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Code %s"),
UTF8_TO_TCHAR(response.errorCode.c_str()));
} else if (!response.errorMessage.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("%s"),
UTF8_TO_TCHAR(response.errorMessage.c_str()));
}
}
void logResponseErrors(const std::exception& exception) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Exception: %s"),
UTF8_TO_TCHAR(exception.what()));
}
} // namespace
CesiumIonSession::CesiumIonSession(
CesiumAsync::AsyncSystem& asyncSystem,
const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor,
TWeakObjectPtr<UCesiumIonServer> pServer)
: _asyncSystem(asyncSystem),
_pAssetAccessor(pAssetAccessor),
_pServer(pServer),
_connection(std::nullopt),
_profile(std::nullopt),
_assets(std::nullopt),
_tokens(std::nullopt),
_defaults(std::nullopt),
_appData(std::nullopt),
_isConnecting(false),
_isResuming(false),
_isLoadingProfile(false),
_isLoadingAssets(false),
_isLoadingTokens(false),
_isLoadingDefaults(false),
_loadProfileQueued(false),
_loadAssetsQueued(false),
_loadTokensQueued(false),
_loadDefaultsQueued(false),
_authorizeUrl() {}
bool CesiumIonSession::isAuthenticationRequired() const {
return this->_appData.has_value() ? this->_appData->needsOauthAuthentication()
: true;
}
void CesiumIonSession::connect() {
if (!this->_pServer.IsValid() || this->isConnecting() ||
this->isConnected() || this->isResuming()) {
return;
}
UCesiumIonServer* pServer = this->_pServer.Get();
this->_isConnecting = true;
std::string ionServerUrl = TCHAR_TO_UTF8(*pServer->ServerUrl);
Future<std::optional<std::string>> futureApiUrl =
!pServer->ApiUrl.IsEmpty()
? this->_asyncSystem.createResolvedFuture<std::optional<std::string>>(
TCHAR_TO_UTF8(*pServer->ApiUrl))
: Connection::getApiUrl(
this->_asyncSystem,
this->_pAssetAccessor,
ionServerUrl);
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
std::move(futureApiUrl)
.thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer](
std::optional<std::string>&& ionApiUrl) {
CesiumAsync::Promise<bool> promise =
thiz->_asyncSystem.createPromise<bool>();
if (!pServer.IsValid()) {
promise.reject(
std::runtime_error("CesiumIonServer unexpectedly nullptr"));
return promise.getFuture();
}
if (!ionApiUrl) {
promise.reject(std::runtime_error(fmt::format(
"Failed to retrieve API URL from the config.json file at the "
"specified Ion server URL: {}",
ionServerUrl)));
return promise.getFuture();
}
if (pServer->ApiUrl.IsEmpty()) {
pServer->ApiUrl = UTF8_TO_TCHAR(ionApiUrl->c_str());
pServer->Modify();
UEditorLoadingAndSavingUtils::SavePackages(
{pServer->GetPackage()},
true);
}
// Make request to /appData to learn the server's authentication mode
return thiz->ensureAppDataLoaded();
})
.thenInMainThread(
[ionServerUrl, thiz, pServer = this->_pServer](bool loadedAppData) {
if (!loadedAppData || !thiz->_appData.has_value()) {
Promise<Connection> promise =
thiz->_asyncSystem.createPromise<Connection>();
promise.reject(std::runtime_error(
"Failed to obtain _appData, can't create connection"));
return promise.getFuture();
}
if (thiz->_appData->needsOauthAuthentication()) {
int64_t clientID = pServer->OAuth2ApplicationID;
return CesiumIonClient::Connection::authorize(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
"Cesium for Unreal",
clientID,
"/cesium-for-unreal/oauth2/callback",
{"assets:list",
"assets:read",
"profile:read",
"tokens:read",
"tokens:write",
"geocode"},
[thiz](const std::string& url) {
thiz->_authorizeUrl = url;
thiz->_redirectUrl =
CesiumUtility::Uri::getQueryValue(url, "redirect_uri");
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(thiz->_authorizeUrl.c_str()),
NULL,
NULL);
},
thiz->_appData.value(),
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)),
CesiumUtility::Uri::resolve(ionServerUrl, "oauth"));
}
return thiz->_asyncSystem
.createResolvedFuture<CesiumIonClient::Connection>(
CesiumIonClient::Connection(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
"",
thiz->_appData.value(),
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl))));
})
.thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer](
CesiumIonClient::Connection&& connection) {
thiz->_isConnecting = false;
thiz->_connection = std::move(connection);
UCesiumEditorSettings* pSettings =
GetMutableDefault<UCesiumEditorSettings>();
pSettings->UserAccessTokenMap.Add(
thiz->_pServer.Get(),
UTF8_TO_TCHAR(thiz->_connection.value().getAccessToken().c_str()));
pSettings->Save();
thiz->ConnectionUpdated.Broadcast();
thiz->startQueuedLoads();
})
.catchInMainThread(
[ionServerUrl, thiz, pServer = this->_pServer](std::exception&& e) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Error connecting: %s"),
UTF8_TO_TCHAR(e.what()));
thiz->_isConnecting = false;
thiz->_connection = std::nullopt;
thiz->ConnectionUpdated.Broadcast();
});
}
void CesiumIonSession::resume() {
if (!this->_pServer.IsValid() || this->isConnecting() ||
this->isConnected() || this->isResuming()) {
return;
}
const UCesiumEditorSettings* pSettings = GetDefault<UCesiumEditorSettings>();
const FString* pUserAccessToken =
pSettings->UserAccessTokenMap.Find(this->_pServer.Get());
if (!pUserAccessToken || pUserAccessToken->IsEmpty()) {
// No existing session to resume.
return;
}
this->_isResuming = true;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
// Verify that the connection actually works.
this->ensureAppDataLoaded()
.thenInMainThread([thiz, pUserAccessToken](bool loadedAppData) {
if (!loadedAppData || !thiz->_appData.has_value()) {
Promise<void> promise = thiz->_asyncSystem.createPromise<void>();
promise.reject(std::runtime_error(
"Failed to obtain _appData, can't resume connection"));
return promise.getFuture();
}
std::shared_ptr<Connection> pConnection = std::make_shared<Connection>(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
TCHAR_TO_UTF8(**pUserAccessToken),
*thiz->_appData,
TCHAR_TO_UTF8(*thiz->_pServer->ApiUrl));
return pConnection->me().thenInMainThread(
[thiz, pConnection](Response<Profile>&& response) {
logResponseErrors(response);
if (response.value.has_value()) {
thiz->_connection = std::move(*pConnection);
}
thiz->_isResuming = false;
thiz->ConnectionUpdated.Broadcast();
thiz->startQueuedLoads();
});
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isResuming = false;
});
}
void CesiumIonSession::disconnect() {
this->_connection.reset();
this->_profile.reset();
this->_assets.reset();
this->_tokens.reset();
this->_defaults.reset();
this->_appData.reset();
UCesiumEditorSettings* pSettings = GetMutableDefault<UCesiumEditorSettings>();
pSettings->UserAccessTokenMap.Remove(this->_pServer.Get());
pSettings->Save();
this->ConnectionUpdated.Broadcast();
this->ProfileUpdated.Broadcast();
this->AssetsUpdated.Broadcast();
this->TokensUpdated.Broadcast();
this->DefaultsUpdated.Broadcast();
}
void CesiumIonSession::refreshProfile() {
if (!this->_connection || this->_isLoadingProfile) {
this->_loadProfileQueued = true;
return;
}
this->_isLoadingProfile = true;
this->_loadProfileQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->me()
.thenInMainThread([thiz](Response<Profile>&& profile) {
logResponseErrors(profile);
thiz->_isLoadingProfile = false;
thiz->_profile = std::move(profile.value);
thiz->ProfileUpdated.Broadcast();
if (thiz->_loadProfileQueued)
thiz->refreshProfile();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingProfile = false;
thiz->_profile = std::nullopt;
thiz->ProfileUpdated.Broadcast();
if (thiz->_loadProfileQueued)
thiz->refreshProfile();
});
}
void CesiumIonSession::refreshAssets() {
if (!this->_connection || this->_isLoadingAssets) {
this->_loadAssetsQueued = true;
return;
}
this->_isLoadingAssets = true;
this->_loadAssetsQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->assets()
.thenInMainThread([thiz](Response<Assets>&& assets) {
logResponseErrors(assets);
thiz->_isLoadingAssets = false;
thiz->_assets = std::move(assets.value);
thiz->AssetsUpdated.Broadcast();
if (thiz->_loadAssetsQueued)
thiz->refreshAssets();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingAssets = false;
thiz->_assets = std::nullopt;
thiz->AssetsUpdated.Broadcast();
if (thiz->_loadAssetsQueued)
thiz->refreshAssets();
});
}
void CesiumIonSession::refreshTokens() {
if (!this->_connection || this->_isLoadingTokens) {
this->_loadTokensQueued = true;
return;
}
this->_isLoadingTokens = true;
this->_loadTokensQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->tokens()
.thenInMainThread([thiz](Response<TokenList>&& tokens) {
logResponseErrors(tokens);
thiz->_isLoadingTokens = false;
thiz->_tokens = tokens.value
? std::make_optional(std::move(tokens.value->items))
: std::nullopt;
thiz->TokensUpdated.Broadcast();
if (thiz->_loadTokensQueued)
thiz->refreshTokens();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingTokens = false;
thiz->_tokens = std::nullopt;
thiz->TokensUpdated.Broadcast();
if (thiz->_loadTokensQueued)
thiz->refreshTokens();
});
}
void CesiumIonSession::refreshDefaults() {
if (!this->_connection || this->_isLoadingDefaults) {
this->_loadDefaultsQueued = true;
return;
}
this->_isLoadingDefaults = true;
this->_loadDefaultsQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->defaults()
.thenInMainThread([thiz](Response<Defaults>&& defaults) {
logResponseErrors(defaults);
thiz->_isLoadingDefaults = false;
thiz->_defaults = std::move(defaults.value);
thiz->DefaultsUpdated.Broadcast();
if (thiz->_loadDefaultsQueued)
thiz->refreshDefaults();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingDefaults = false;
thiz->_defaults = std::nullopt;
thiz->DefaultsUpdated.Broadcast();
if (thiz->_loadDefaultsQueued)
thiz->refreshDefaults();
});
}
const std::optional<CesiumIonClient::Connection>&
CesiumIonSession::getConnection() const {
return this->_connection;
}
const CesiumIonClient::Profile& CesiumIonSession::getProfile() {
static const CesiumIonClient::Profile empty{};
if (this->_profile) {
return *this->_profile;
} else {
this->refreshProfile();
return empty;
}
}
const CesiumIonClient::Assets& CesiumIonSession::getAssets() {
static const CesiumIonClient::Assets empty;
if (this->_assets) {
return *this->_assets;
} else {
this->refreshAssets();
return empty;
}
}
const std::vector<CesiumIonClient::Token>& CesiumIonSession::getTokens() {
static const std::vector<CesiumIonClient::Token> empty;
if (this->_tokens) {
return *this->_tokens;
} else {
this->refreshTokens();
return empty;
}
}
const CesiumIonClient::Defaults& CesiumIonSession::getDefaults() {
static const CesiumIonClient::Defaults empty;
if (this->_defaults) {
return *this->_defaults;
} else {
this->refreshDefaults();
return empty;
}
}
const CesiumIonClient::ApplicationData& CesiumIonSession::getAppData() {
static const CesiumIonClient::ApplicationData empty{};
if (this->_appData) {
return *this->_appData;
}
return empty;
}
bool CesiumIonSession::refreshProfileIfNeeded() {
if (this->_loadProfileQueued || !this->_profile.has_value()) {
this->refreshProfile();
}
return this->isProfileLoaded();
}
bool CesiumIonSession::refreshAssetsIfNeeded() {
if (this->_loadAssetsQueued || !this->_assets.has_value()) {
this->refreshAssets();
}
return this->isAssetListLoaded();
}
bool CesiumIonSession::refreshTokensIfNeeded() {
if (this->_loadTokensQueued || !this->_tokens.has_value()) {
this->refreshTokens();
}
return this->isTokenListLoaded();
}
bool CesiumIonSession::refreshDefaultsIfNeeded() {
if (this->_loadDefaultsQueued || !this->_defaults.has_value()) {
this->refreshDefaults();
}
return this->isDefaultsLoaded();
}
Future<Response<Token>>
CesiumIonSession::findToken(const FString& token) const {
if (!this->_connection) {
return this->getAsyncSystem().createResolvedFuture(
Response<Token>(0, "NOTCONNECTED", "Not connected to Cesium ion."));
}
std::string tokenString = TCHAR_TO_UTF8(*token);
std::optional<std::string> maybeTokenID =
Connection::getIdFromToken(tokenString);
if (!maybeTokenID) {
return this->getAsyncSystem().createResolvedFuture(
Response<Token>(0, "INVALIDTOKEN", "The token is not valid."));
}
return this->_connection->token(*maybeTokenID);
}
namespace {
Token tokenFromServer(UCesiumIonServer* pServer) {
Token result;
if (pServer) {
result.token = TCHAR_TO_UTF8(*pServer->DefaultIonAccessToken);
}
return result;
}
Future<Token> getTokenFuture(const CesiumIonSession& session) {
std::shared_ptr<const CesiumIonSession> pSession = session.shared_from_this();
TWeakObjectPtr<UCesiumIonServer> pServer = session.getServer();
if (pServer.IsValid() && !pServer->DefaultIonAccessTokenId.IsEmpty()) {
return session.getConnection()
->token(TCHAR_TO_UTF8(*pServer->DefaultIonAccessTokenId))
.thenImmediately([pServer](Response<Token>&& tokenResponse) {
if (tokenResponse.value) {
return *tokenResponse.value;
} else {
return tokenFromServer(pServer.Get());
}
});
} else if (!pServer->DefaultIonAccessToken.IsEmpty()) {
return session.findToken(pServer->DefaultIonAccessToken)
.thenImmediately([pServer](Response<Token>&& response) {
if (response.value) {
return *response.value;
} else {
return tokenFromServer(pServer.Get());
}
});
} else {
return session.getAsyncSystem().createResolvedFuture(
tokenFromServer(pServer.Get()));
}
}
} // namespace
SharedFuture<Token> CesiumIonSession::getProjectDefaultTokenDetails() {
if (this->_projectDefaultTokenDetailsFuture) {
// If the future is resolved but its token doesn't match the designated
// default token, do the request again because the user probably specified a
// new token.
if (this->_projectDefaultTokenDetailsFuture->isReady() &&
this->_projectDefaultTokenDetailsFuture->wait().token !=
TCHAR_TO_UTF8(*this->_pServer->DefaultIonAccessToken)) {
this->_projectDefaultTokenDetailsFuture.reset();
} else {
return *this->_projectDefaultTokenDetailsFuture;
}
}
if (!this->isConnected()) {
return this->getAsyncSystem()
.createResolvedFuture(tokenFromServer(this->_pServer.Get()))
.share();
}
this->_projectDefaultTokenDetailsFuture = getTokenFuture(*this).share();
return *this->_projectDefaultTokenDetailsFuture;
}
void CesiumIonSession::invalidateProjectDefaultTokenDetails() {
this->_projectDefaultTokenDetailsFuture.reset();
}
void CesiumIonSession::startQueuedLoads() {
if (this->_loadProfileQueued)
this->refreshProfile();
if (this->_loadAssetsQueued)
this->refreshAssets();
if (this->_loadTokensQueued)
this->refreshTokens();
if (this->_loadDefaultsQueued)
this->refreshDefaults();
}
CesiumAsync::Future<bool> CesiumIonSession::ensureAppDataLoaded() {
UCesiumIonServer* pServer = this->_pServer.Get();
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
return CesiumIonClient::Connection::appData(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)))
.thenInMainThread(
[thiz, pServer = this->_pServer](
CesiumIonClient::Response<CesiumIonClient::ApplicationData>&&
appData) {
CesiumAsync::Promise<bool> promise =
thiz->_asyncSystem.createPromise<bool>();
thiz->_appData = appData.value;
if (!appData.value.has_value()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Failed to obtain ion server application data: %s"),
UTF8_TO_TCHAR(appData.errorMessage.c_str()));
promise.resolve(false);
} else {
promise.resolve(true);
}
return promise.getFuture();
})
.catchInMainThread([thiz, pServer = this->_pServer](std::exception&& e) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Error obtaining appData: %s"),
UTF8_TO_TCHAR(e.what()));
return thiz->_asyncSystem.createResolvedFuture(false);
});
}

View File

@ -0,0 +1,150 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumAsync/AsyncSystem.h"
#include "CesiumAsync/IAssetAccessor.h"
#include "CesiumAsync/SharedFuture.h"
#include "CesiumIonClient/Connection.h"
#include "Delegates/Delegate.h"
#include <memory>
DECLARE_MULTICAST_DELEGATE(FIonUpdated);
class UCesiumIonServer;
class CesiumIonSession : public std::enable_shared_from_this<CesiumIonSession> {
public:
CesiumIonSession(
CesiumAsync::AsyncSystem& asyncSystem,
const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor,
TWeakObjectPtr<UCesiumIonServer> pServer);
const std::shared_ptr<CesiumAsync::IAssetAccessor>& getAssetAccessor() const {
return this->_pAssetAccessor;
}
const CesiumAsync::AsyncSystem& getAsyncSystem() const {
return this->_asyncSystem;
}
CesiumAsync::AsyncSystem& getAsyncSystem() { return this->_asyncSystem; }
TWeakObjectPtr<UCesiumIonServer> getServer() const { return this->_pServer; }
bool isConnected() const { return this->_connection.has_value(); }
bool isConnecting() const { return this->_isConnecting; }
bool isResuming() const { return this->_isResuming; }
bool isProfileLoaded() const { return this->_profile.has_value(); }
bool isLoadingProfile() const { return this->_isLoadingProfile; }
bool isAssetListLoaded() const { return this->_assets.has_value(); }
bool isLoadingAssetList() const { return this->_isLoadingAssets; }
bool isTokenListLoaded() const { return this->_tokens.has_value(); }
bool isLoadingTokenList() const { return this->_isLoadingTokens; }
bool isDefaultsLoaded() const { return this->_defaults.has_value(); }
bool isLoadingDefaults() const { return this->_isLoadingDefaults; }
bool isAuthenticationRequired() const;
void connect();
void resume();
void disconnect();
void refreshProfile();
void refreshAssets();
void refreshTokens();
void refreshDefaults();
FIonUpdated ConnectionUpdated;
FIonUpdated ProfileUpdated;
FIonUpdated AssetsUpdated;
FIonUpdated TokensUpdated;
FIonUpdated DefaultsUpdated;
const std::optional<CesiumIonClient::Connection>& getConnection() const;
const CesiumIonClient::Profile& getProfile();
const CesiumIonClient::Assets& getAssets();
const std::vector<CesiumIonClient::Token>& getTokens();
const CesiumIonClient::Defaults& getDefaults();
const CesiumIonClient::ApplicationData& getAppData();
const std::string& getAuthorizeUrl() const { return this->_authorizeUrl; }
const std::string& getRedirectUrl() const { return this->_redirectUrl; }
bool refreshProfileIfNeeded();
bool refreshAssetsIfNeeded();
bool refreshTokensIfNeeded();
bool refreshDefaultsIfNeeded();
/**
* Finds the details of the specified token in the user's account.
*
* If this session is not connected, returns std::nullopt.
*
* Even if the list of tokens is already loaded, this method does a new query
* in order get the most up-to-date information about the token.
*
* @param token The token.
* @return The details of the token, or an error response if the token does
* not exist in the signed-in user account.
*/
CesiumAsync::Future<CesiumIonClient::Response<CesiumIonClient::Token>>
findToken(const FString& token) const;
/**
* Gets the project default token.
*
* If the project default token exists in the signed-in user's account, full
* token details will be included. Otherwise, only the token value itself
* (i.e. the `token` property`) will be included, and it may or may not be
* valid. In the latter case, the `id` property will be an empty string.
*
* @return A future that resolves to the project default token.
*/
CesiumAsync::SharedFuture<CesiumIonClient::Token>
getProjectDefaultTokenDetails();
void invalidateProjectDefaultTokenDetails();
private:
void startQueuedLoads();
/**
* If the {@link _appData} field has no value, this method will request the
* ion server's /appData endpoint to obtain its data.
* @returns A future that resolves to true if _appData is present or false if
* it couldn't be fetched.
*/
CesiumAsync::Future<bool> ensureAppDataLoaded();
CesiumAsync::AsyncSystem _asyncSystem;
std::shared_ptr<CesiumAsync::IAssetAccessor> _pAssetAccessor;
TWeakObjectPtr<UCesiumIonServer> _pServer;
std::optional<CesiumIonClient::Connection> _connection;
std::optional<CesiumIonClient::Profile> _profile;
std::optional<CesiumIonClient::Assets> _assets;
std::optional<std::vector<CesiumIonClient::Token>> _tokens;
std::optional<CesiumIonClient::Defaults> _defaults;
std::optional<CesiumIonClient::ApplicationData> _appData;
std::optional<CesiumAsync::SharedFuture<CesiumIonClient::Token>>
_projectDefaultTokenDetailsFuture;
bool _isConnecting;
bool _isResuming;
bool _isLoadingProfile;
bool _isLoadingAssets;
bool _isLoadingTokens;
bool _isLoadingDefaults;
bool _loadProfileQueued;
bool _loadAssetsQueued;
bool _loadTokensQueued;
bool _loadDefaultsQueued;
std::string _authorizeUrl;
std::string _redirectUrl;
};

View File

@ -0,0 +1,853 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonTokenTroubleshooting.h"
#include "Cesium3DTileset.h"
#include "CesiumCommon.h"
#include "CesiumEditor.h"
#include "CesiumIonClient/Connection.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServerDisplay.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumUtility/Uri.h"
#include "EditorStyleSet.h"
#include "LevelEditor.h"
#include "ScopedTransaction.h"
#include "SelectCesiumIonToken.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Images/SThrobber.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SHeader.h"
#include "Widgets/Text/STextBlock.h"
using namespace CesiumIonClient;
/*static*/ std::vector<CesiumIonTokenTroubleshooting::ExistingPanel>
CesiumIonTokenTroubleshooting::_existingPanels{};
/*static*/ void CesiumIonTokenTroubleshooting::Open(
CesiumIonObject ionObject,
bool triggeredByError) {
auto panelMatch = [ionObject](const ExistingPanel& panel) {
return panel.pObject == ionObject;
};
// If a panel is already open for this object, close it.
auto it = std::find_if(
CesiumIonTokenTroubleshooting::_existingPanels.begin(),
CesiumIonTokenTroubleshooting::_existingPanels.end(),
panelMatch);
if (it != CesiumIonTokenTroubleshooting::_existingPanels.end()) {
TSharedRef<CesiumIonTokenTroubleshooting> pPanel = it->pPanel;
CesiumIonTokenTroubleshooting::_existingPanels.erase(it);
FSlateApplication::Get().RequestDestroyWindow(pPanel);
}
// If this is a tileset, close any already-open panels associated with its
// overlays. Overlays won't appear until the tileset is working anyway.
TWeakObjectPtr<ACesium3DTileset>* ppTileset =
swl::get_if<TWeakObjectPtr<ACesium3DTileset>>(&ionObject);
if (ppTileset && ppTileset->IsValid()) {
TArray<UCesiumRasterOverlay*> rasterOverlays;
(*ppTileset)->GetComponents<UCesiumRasterOverlay>(rasterOverlays);
for (UCesiumRasterOverlay* pOverlay : rasterOverlays) {
auto rasterIt = std::find_if(
CesiumIonTokenTroubleshooting::_existingPanels.begin(),
CesiumIonTokenTroubleshooting::_existingPanels.end(),
[pOverlay](const ExistingPanel& candidate) {
return candidate.pObject == CesiumIonObject(pOverlay);
});
if (rasterIt != CesiumIonTokenTroubleshooting::_existingPanels.end()) {
TSharedRef<CesiumIonTokenTroubleshooting> pPanel = rasterIt->pPanel;
CesiumIonTokenTroubleshooting::_existingPanels.erase(rasterIt);
FSlateApplication::Get().RequestDestroyWindow(pPanel);
}
}
}
// If this is a raster overlay and this panel is already open for its attached
// tileset, don't open the panel for the overlay for the same reason as above.
TWeakObjectPtr<UCesiumRasterOverlay>* ppRasterOverlay =
swl::get_if<TWeakObjectPtr<UCesiumRasterOverlay>>(&ionObject);
if (ppRasterOverlay && ppRasterOverlay->IsValid()) {
ACesium3DTileset* pOwner =
Cast<ACesium3DTileset>((*ppRasterOverlay)->GetOwner());
if (pOwner) {
auto tilesetIt = std::find_if(
CesiumIonTokenTroubleshooting::_existingPanels.begin(),
CesiumIonTokenTroubleshooting::_existingPanels.end(),
[pOwner](const ExistingPanel& candidate) {
return candidate.pObject == CesiumIonObject(pOwner);
});
if (tilesetIt != CesiumIonTokenTroubleshooting::_existingPanels.end()) {
return;
}
}
}
// Open the panel
TSharedRef<CesiumIonTokenTroubleshooting> Troubleshooting =
SNew(CesiumIonTokenTroubleshooting)
.IonObject(ionObject)
.TriggeredByError(triggeredByError);
Troubleshooting->GetOnWindowClosedEvent().AddLambda(
[panelMatch](const TSharedRef<SWindow>& pWindow) {
auto it = std::find_if(
CesiumIonTokenTroubleshooting::_existingPanels.begin(),
CesiumIonTokenTroubleshooting::_existingPanels.end(),
panelMatch);
if (it != CesiumIonTokenTroubleshooting::_existingPanels.end()) {
CesiumIonTokenTroubleshooting::_existingPanels.erase(it);
}
});
FSlateApplication::Get().AddWindow(Troubleshooting);
CesiumIonTokenTroubleshooting::_existingPanels.emplace_back(
ExistingPanel{ionObject, Troubleshooting});
}
namespace {
TSharedRef<SWidget>
addTokenCheck(const FString& label, std::optional<bool>& state) {
return SNew(SHorizontalBox) +
SHorizontalBox::Slot().AutoWidth().Padding(
3.0f,
0.0f,
3.0f,
0.0f)[SNew(SThrobber)
.Visibility_Lambda([&state]() {
return state.has_value() ? EVisibility::Collapsed
: EVisibility::Visible;
})
.NumPieces(1)
.Animate(SThrobber::All)] +
SHorizontalBox::Slot().AutoWidth().Padding(
5.0f,
0.0f,
5.0f,
3.0f)[SNew(SImage)
.Visibility_Lambda([&state]() {
return state.has_value() ? EVisibility::Visible
: EVisibility::Collapsed;
})
.Image_Lambda([&state]() {
return state.has_value() && *state
? FCesiumEditorModule::GetStyle()->GetBrush(
TEXT("Cesium.Common.GreenTick"))
: FCesiumEditorModule::GetStyle()->GetBrush(
TEXT("Cesium.Common.RedX"));
})] +
SHorizontalBox::Slot()
.AutoWidth()[SNew(STextBlock).Text(FText::FromString(label))];
}
bool isNull(const CesiumIonObject& o) {
return swl::visit([](auto p) { return p == nullptr; }, o);
}
FString getLabel(const CesiumIonObject& o) {
struct Operation {
FString operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
return pTileset.IsValid() ? pTileset->GetActorLabel() : TEXT("Unknown");
}
FString operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
return pRasterOverlay.IsValid() ? pRasterOverlay->GetName()
: TEXT("Unknown");
}
};
return swl::visit(Operation(), o);
}
FString getName(const CesiumIonObject& o) {
return swl::visit([](auto p) { return p->GetName(); }, o);
}
int64 getIonAssetID(const CesiumIonObject& o) {
struct Operation {
int64 operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
if (!pTileset.IsValid())
return 0;
if (pTileset->GetTilesetSource() != ETilesetSource::FromCesiumIon) {
return 0;
} else {
return pTileset->GetIonAssetID();
}
}
int64 operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
if (!pRasterOverlay.IsValid())
return 0;
UCesiumIonRasterOverlay* pIon =
Cast<UCesiumIonRasterOverlay>(pRasterOverlay);
if (!pIon) {
return 0;
} else {
return pIon->IonAssetID;
}
}
};
return swl::visit(Operation(), o);
}
FString getIonAccessToken(const CesiumIonObject& o) {
struct Operation {
FString operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
if (!pTileset.IsValid())
return FString();
if (pTileset->GetTilesetSource() != ETilesetSource::FromCesiumIon) {
return FString();
} else {
return pTileset->GetIonAccessToken();
}
}
FString operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
if (!pRasterOverlay.IsValid())
return FString();
UCesiumIonRasterOverlay* pIon =
Cast<UCesiumIonRasterOverlay>(pRasterOverlay);
if (!pIon) {
return FString();
} else {
return pIon->IonAccessToken;
}
}
};
return swl::visit(Operation(), o);
}
void setIonAccessToken(const CesiumIonObject& o, const FString& newToken) {
struct Operation {
const FString& newToken;
void operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
if (!pTileset.IsValid())
return;
if (pTileset->GetIonAccessToken() != newToken) {
pTileset->Modify();
pTileset->SetIonAccessToken(newToken);
} else {
pTileset->RefreshTileset();
}
}
void operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
if (!pRasterOverlay.IsValid())
return;
UCesiumIonRasterOverlay* pIon =
Cast<UCesiumIonRasterOverlay>(pRasterOverlay);
if (!pIon) {
return;
}
if (pIon->IonAccessToken != newToken) {
pIon->Modify();
pIon->IonAccessToken = newToken;
}
pIon->Refresh();
}
};
return swl::visit(Operation{newToken}, o);
}
FString getObjectType(const CesiumIonObject& o) {
struct Operation {
FString operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
return TEXT("Tileset");
}
FString operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
return TEXT("Raster Overlay");
}
};
return swl::visit(Operation(), o);
}
UObject* asUObject(const CesiumIonObject& o) {
return swl::visit(
[](auto p) -> UObject* { return p.IsValid() ? p.Get() : nullptr; },
o);
}
bool isUsingCesiumIon(const CesiumIonObject& o) {
struct Operation {
bool operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) {
return pTileset.IsValid() &&
pTileset->GetTilesetSource() == ETilesetSource::FromCesiumIon;
}
bool operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) {
if (!pRasterOverlay.IsValid())
return false;
UCesiumIonRasterOverlay* pIon =
Cast<UCesiumIonRasterOverlay>(pRasterOverlay);
return pIon != nullptr;
}
};
return swl::visit(Operation(), o);
}
UCesiumIonServer* getCesiumIonServer(const CesiumIonObject& o) {
struct Operation {
UCesiumIonServer*
operator()(TWeakObjectPtr<ACesium3DTileset> pTileset) noexcept {
return pTileset.IsValid() ? pTileset->GetCesiumIonServer() : nullptr;
}
UCesiumIonServer*
operator()(TWeakObjectPtr<UCesiumRasterOverlay> pRasterOverlay) noexcept {
if (!pRasterOverlay.IsValid())
return nullptr;
const UCesiumIonRasterOverlay* pIon =
Cast<UCesiumIonRasterOverlay>(pRasterOverlay);
return pIon ? pIon->CesiumIonServer : nullptr;
}
};
UCesiumIonServer* pServer = swl::visit(Operation{}, o);
if (!IsValid(pServer)) {
pServer = UCesiumIonServer::GetDefaultServer();
}
return pServer;
}
CesiumIonSession& getSession(const CesiumIonObject& o) {
return *FCesiumEditorModule::serverManager().GetSession(
getCesiumIonServer(o));
}
} // namespace
void CesiumIonTokenTroubleshooting::Construct(const FArguments& InArgs) {
TSharedRef<SVerticalBox> pMainVerticalBox = SNew(SVerticalBox);
CesiumIonObject pIonObject = InArgs._IonObject;
if (isNull(pIonObject)) {
return;
}
if (!isUsingCesiumIon(pIonObject)) {
SWindow::Construct(
SWindow::FArguments()
.Title(FText::FromString(FString::Format(
TEXT("{0}: Cesium ion Token Troubleshooting"),
{*getLabel(pIonObject)})))
.AutoCenter(EAutoCenter::PreferredWorkArea)
.SizingRule(ESizingRule::UserSized)
.ClientSize(FVector2D(800, 600))
[SNew(SBorder)
.Visibility(EVisibility::Visible)
.BorderImage(FAppStyle::GetBrush("Menu.Background"))
.Padding(FMargin(10.0f, 20.0f, 10.0f, 20.0f))
[SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(TEXT(
"This object is not configured to connect to Cesium ion.")))]]);
return;
}
this->_pIonObject = pIonObject;
if (InArgs._TriggeredByError) {
FString label = getLabel(pIonObject);
FString name = getName(pIonObject);
FString descriptor =
label == name ? FString::Format(TEXT("\"{0}\""), {name})
: FString::Format(TEXT("\"{0}\" ({1})"), {label, name});
FString preamble = FString::Format(
TEXT(
"{0} {1} tried to access Cesium ion for asset ID {2}, but it didn't work, probably due to a problem with the access token. This panel will help you fix it!"),
{getObjectType(pIonObject), *descriptor, getIonAssetID(pIonObject)});
pMainVerticalBox->AddSlot().AutoHeight()
[SNew(STextBlock).AutoWrapText(true).Text(FText::FromString(preamble))];
}
pMainVerticalBox->AddSlot().AutoHeight().Padding(
5.0f)[SNew(CesiumIonServerDisplay)
.Server(getCesiumIonServer(pIonObject))];
TSharedRef<SHorizontalBox> pDiagnosticColumns = SNew(SHorizontalBox);
if (!getIonAccessToken(pIonObject).IsEmpty()) {
this->_assetTokenState.name = FString::Format(
TEXT("This {0}'s Access Token"),
{getObjectType(pIonObject)});
this->_assetTokenState.token = getIonAccessToken(pIonObject);
pDiagnosticColumns->AddSlot()
.Padding(5.0f, 20.0f, 5.0f, 5.0f)
.VAlign(EVerticalAlignment::VAlign_Top)
.AutoWidth()
.FillWidth(
0.5f)[this->createTokenPanel(pIonObject, this->_assetTokenState)];
}
this->_projectDefaultTokenState.name = TEXT("Project Default Access Token");
this->_projectDefaultTokenState.token =
getCesiumIonServer(pIonObject)->DefaultIonAccessToken;
pDiagnosticColumns->AddSlot()
.Padding(5.0f, 20.0f, 5.0f, 5.0f)
.VAlign(EVerticalAlignment::VAlign_Top)
.AutoWidth()
.FillWidth(0.5f)
[this->createTokenPanel(pIonObject, this->_projectDefaultTokenState)];
if (getSession(this->_pIonObject).isConnected()) {
// Don't let this panel be destroyed while the async operations below are in
// progress.
TSharedRef<CesiumIonTokenTroubleshooting> pPanel =
StaticCastSharedRef<CesiumIonTokenTroubleshooting>(this->AsShared());
getSession(this->_pIonObject)
.getConnection()
->asset(getIonAssetID(pIonObject))
.thenInMainThread([pPanel](Response<Asset>&& asset) {
pPanel->_assetExistsInUserAccount = asset.value.has_value();
});
// Start a new row if we have more than two columns.
if (pDiagnosticColumns->NumSlots() >= 2) {
pMainVerticalBox->AddSlot().AutoHeight()[pDiagnosticColumns];
pDiagnosticColumns = SNew(SHorizontalBox);
}
pDiagnosticColumns->AddSlot()
.Padding(5.0f, 20.0f, 5.0f, 5.0f)
.VAlign(EVerticalAlignment::VAlign_Top)
.AutoWidth()
.FillWidth(0.5f)[this->createDiagnosticPanel(
TEXT("Asset"),
{addTokenCheck(
TEXT("Asset ID exists in your user account"),
this->_assetExistsInUserAccount)})];
}
pMainVerticalBox->AddSlot().AutoHeight()[pDiagnosticColumns];
this->addRemedyButton(
pMainVerticalBox,
TEXT("Connect to Cesium ion"),
&CesiumIonTokenTroubleshooting::canConnectToCesiumIon,
&CesiumIonTokenTroubleshooting::connectToCesiumIon);
this->addRemedyButton(
pMainVerticalBox,
FString::Format(
TEXT("Use the project default token for this {0}"),
{getObjectType(pIonObject)}),
&CesiumIonTokenTroubleshooting::canUseProjectDefaultToken,
&CesiumIonTokenTroubleshooting::useProjectDefaultToken);
this->addRemedyButton(
pMainVerticalBox,
FString::Format(
TEXT("Authorize the {0}'s token to access this asset"),
{getObjectType(pIonObject)}),
&CesiumIonTokenTroubleshooting::canAuthorizeAssetToken,
&CesiumIonTokenTroubleshooting::authorizeAssetToken);
this->addRemedyButton(
pMainVerticalBox,
TEXT("Authorize the project default token to access this asset"),
&CesiumIonTokenTroubleshooting::canAuthorizeProjectDefaultToken,
&CesiumIonTokenTroubleshooting::authorizeProjectDefaultToken);
this->addRemedyButton(
pMainVerticalBox,
TEXT("Select or create a new project default token"),
&CesiumIonTokenTroubleshooting::canSelectNewProjectDefaultToken,
&CesiumIonTokenTroubleshooting::selectNewProjectDefaultToken);
pMainVerticalBox->AddSlot().AutoHeight().Padding(0.0f, 20.0f, 0.0f, 0.0f)
[SNew(STextBlock)
.Visibility_Lambda([this]() {
return (this->_assetTokenState.token.IsEmpty() ||
this->_assetTokenState.allowsAccessToAsset == false) &&
(this->_projectDefaultTokenState.token.IsEmpty() ||
this->_projectDefaultTokenState
.allowsAccessToAsset == false) &&
this->_assetExistsInUserAccount == false
? EVisibility::Visible
: EVisibility::Collapsed;
})
.AutoWrapText(true)
.Text(FText::FromString(FString::Format(
TEXT(
"No automatic remedies are possible for Asset ID {0}, because:\n"
" - The current token does not authorize access to the specified asset ID, and\n"
" - The asset ID does not exist in your Cesium ion account.\n"
"\n"
"Please click the button below to open Cesium ion and check:\n"
" - The {1}'s \"Ion Asset ID\" property is correct.\n"
" - If the asset is from the \"Asset Depot\", verify that it has been added to \"My Assets\"."),
{getIonAssetID(pIonObject), getObjectType(pIonObject)})))];
this->addRemedyButton(
pMainVerticalBox,
TEXT("Open Cesium ion on the Web"),
&CesiumIonTokenTroubleshooting::canOpenCesiumIon,
&CesiumIonTokenTroubleshooting::openCesiumIon);
SWindow::Construct(
SWindow::FArguments()
.Title(FText::FromString(FString::Format(
TEXT("{0}: Cesium ion Token Troubleshooting"),
{*getLabel(pIonObject)})))
.AutoCenter(EAutoCenter::PreferredWorkArea)
.SizingRule(ESizingRule::UserSized)
.ClientSize(FVector2D(800, 600))
[SNew(SBorder)
.Visibility(EVisibility::Visible)
.BorderImage(FAppStyle::GetBrush("Menu.Background"))
.Padding(
FMargin(10.0f, 10.0f, 10.0f, 10.0f))[pMainVerticalBox]]);
}
TSharedRef<SWidget> CesiumIonTokenTroubleshooting::createDiagnosticPanel(
const FString& name,
const TArray<TSharedRef<SWidget>>& diagnostics) {
TSharedRef<SVerticalBox> pRows =
SNew(SVerticalBox) +
SVerticalBox::Slot().Padding(
0.0f,
5.0f,
0.0f,
5.0f)[SNew(SHeader).Content()
[SNew(STextBlock)
.TextStyle(FCesiumEditorModule::GetStyle(), "Heading")
.Text(FText::FromString(name))]];
for (const TSharedRef<SWidget>& diagnostic : diagnostics) {
pRows->AddSlot().Padding(0.0f, 5.0f, 0.0f, 5.0f)[diagnostic];
}
return pRows;
}
TSharedRef<SWidget> CesiumIonTokenTroubleshooting::createTokenPanel(
const CesiumIonObject& pIonObject,
TokenState& state) {
CesiumIonSession& ionSession = getSession(pIonObject);
int64 assetID = getIonAssetID(pIonObject);
auto pConnection = std::make_shared<Connection>(
ionSession.getAsyncSystem(),
ionSession.getAssetAccessor(),
TCHAR_TO_UTF8(*state.token),
ionSession.getAppData(),
TCHAR_TO_UTF8(*getCesiumIonServer(pIonObject)->ApiUrl));
// Don't let this panel be destroyed while the async operations below are in
// progress.
TSharedRef<CesiumIonTokenTroubleshooting> pPanel =
StaticCastSharedRef<CesiumIonTokenTroubleshooting>(this->AsShared());
pConnection->me()
.thenInMainThread(
[pPanel, pConnection, assetID, &state](Response<Profile>&& profile) {
state.isValid = profile.value.has_value();
if (pPanel->IsVisible()) {
return pConnection->asset(assetID);
} else {
return pConnection->getAsyncSystem().createResolvedFuture(
Response<Asset>{});
}
})
.thenInMainThread([pPanel, pConnection, &state](Response<Asset>&& asset) {
state.allowsAccessToAsset = asset.value.has_value();
if (pPanel->IsVisible()) {
// Query the tokens using the user's connection (_not_ the token
// connection created above).
CesiumIonSession& ionSession = getSession(pPanel->_pIonObject);
ionSession.resume();
const std::optional<Connection>& userConnection =
ionSession.getConnection();
if (!userConnection) {
Response<TokenList> result{};
return ionSession.getAsyncSystem().createResolvedFuture(
std::move(result));
}
return userConnection->tokens();
} else {
return pConnection->getAsyncSystem().createResolvedFuture(
Response<TokenList>{});
}
})
.thenInMainThread(
[pPanel, pConnection, &state](Response<TokenList>&& tokens) {
state.associatedWithUserAccount = false;
if (tokens.value.has_value()) {
auto it = std::find_if(
tokens.value->items.begin(),
tokens.value->items.end(),
[&pConnection](const Token& token) {
return token.token == pConnection->getAccessToken();
});
state.associatedWithUserAccount = it != tokens.value->items.end();
}
});
return this->createDiagnosticPanel(
state.name,
{addTokenCheck(TEXT("Is a valid Cesium ion token"), state.isValid),
addTokenCheck(
TEXT("Allows access to this asset"),
state.allowsAccessToAsset),
addTokenCheck(
TEXT("Is associated with your user account"),
state.associatedWithUserAccount)});
}
void CesiumIonTokenTroubleshooting::addRemedyButton(
const TSharedRef<SVerticalBox>& pParent,
const FString& name,
bool (CesiumIonTokenTroubleshooting::*isAvailableCallback)() const,
void (CesiumIonTokenTroubleshooting::*clickCallback)()) {
pParent->AddSlot().AutoHeight().Padding(
0.0f,
20.0f,
0.0f,
5.0f)[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText")
.OnClicked_Lambda([this, clickCallback]() {
std::invoke(clickCallback, *this);
this->RequestDestroyWindow();
return FReply::Handled();
})
.Text(FText::FromString(name))
.Visibility_Lambda([this, isAvailableCallback]() {
return std::invoke(isAvailableCallback, *this)
? EVisibility::Visible
: EVisibility::Collapsed;
})];
}
bool CesiumIonTokenTroubleshooting::canConnectToCesiumIon() const {
return !getSession(this->_pIonObject).isConnected();
}
void CesiumIonTokenTroubleshooting::connectToCesiumIon() {
// Pop up the Cesium panel to show the status.
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
TSharedPtr<FTabManager> pTabManager =
pLevelEditorModule ? pLevelEditorModule->GetLevelEditorTabManager()
: FGlobalTabmanager::Get();
pTabManager->TryInvokeTab(FTabId(TEXT("Cesium")));
// Pop up a browser window to sign in to ion.
getSession(this->_pIonObject).connect();
}
bool CesiumIonTokenTroubleshooting::canUseProjectDefaultToken() const {
const TokenState& state = this->_projectDefaultTokenState;
return !isNull(this->_pIonObject) &&
!getIonAccessToken(this->_pIonObject).IsEmpty() &&
state.isValid == true && state.allowsAccessToAsset == true;
}
void CesiumIonTokenTroubleshooting::useProjectDefaultToken() {
if (isNull(this->_pIonObject)) {
return;
}
FScopedTransaction transaction(
FText::FromString("Use Project Default Token"));
setIonAccessToken(this->_pIonObject, FString());
}
bool CesiumIonTokenTroubleshooting::canAuthorizeAssetToken() const {
const TokenState& state = this->_assetTokenState;
return this->_assetExistsInUserAccount == true && state.isValid == true &&
state.allowsAccessToAsset == false &&
state.associatedWithUserAccount == true;
}
void CesiumIonTokenTroubleshooting::authorizeAssetToken() {
if (isNull(this->_pIonObject)) {
return;
}
this->authorizeToken(getIonAccessToken(this->_pIonObject), false);
}
bool CesiumIonTokenTroubleshooting::canAuthorizeProjectDefaultToken() const {
const TokenState& state = this->_projectDefaultTokenState;
return this->_assetExistsInUserAccount == true && state.isValid == true &&
state.allowsAccessToAsset == false &&
state.associatedWithUserAccount == true;
}
void CesiumIonTokenTroubleshooting::authorizeProjectDefaultToken() {
UCesiumIonServer* pServer = getCesiumIonServer(this->_pIonObject);
this->authorizeToken(pServer->DefaultIonAccessToken, true);
}
bool CesiumIonTokenTroubleshooting::canSelectNewProjectDefaultToken() const {
if (this->_assetExistsInUserAccount == false) {
return false;
}
const TokenState& state = this->_projectDefaultTokenState;
return getSession(this->_pIonObject).isConnected() &&
(state.isValid == false || (state.allowsAccessToAsset == false &&
state.associatedWithUserAccount == false));
}
void CesiumIonTokenTroubleshooting::selectNewProjectDefaultToken() {
if (isNull(this->_pIonObject)) {
return;
}
CesiumIonSession& session = getSession(this->_pIonObject);
const std::optional<Connection>& maybeConnection = session.getConnection();
if (!session.isConnected() || !maybeConnection) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT(
"Cannot create a new project default token because you are not signed in to Cesium ion."));
return;
}
// Don't let this panel be destroyed while the async operations below are in
// progress.
TSharedRef<CesiumIonTokenTroubleshooting> pPanel =
StaticCastSharedRef<CesiumIonTokenTroubleshooting>(this->AsShared());
SelectCesiumIonToken::SelectNewToken(getCesiumIonServer(this->_pIonObject))
.thenInMainThread([pPanel](const std::optional<Token>& newToken) {
if (!newToken) {
return;
}
pPanel->useProjectDefaultToken();
});
}
bool CesiumIonTokenTroubleshooting::canOpenCesiumIon() const {
return getSession(this->_pIonObject).isConnected();
}
void CesiumIonTokenTroubleshooting::openCesiumIon() {
UCesiumIonServer* pServer = getCesiumIonServer(this->_pIonObject);
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(CesiumUtility::Uri::resolve(
TCHAR_TO_UTF8(*pServer->ServerUrl),
"tokens")
.c_str()),
NULL,
NULL);
}
void CesiumIonTokenTroubleshooting::authorizeToken(
const FString& token,
bool removeObjectToken) {
if (isNull(this->_pIonObject)) {
return;
}
CesiumIonSession& session = getSession(this->_pIonObject);
const std::optional<Connection>& maybeConnection = session.getConnection();
if (!session.isConnected() || !maybeConnection) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT(
"Cannot grant a token access to an asset because you are not signed in to Cesium ion."));
return;
}
TWeakObjectPtr<UObject> pStillAlive = asUObject(this->_pIonObject);
session.findToken(token).thenInMainThread([pStillAlive,
removeObjectToken,
pIonObject = this->_pIonObject,
ionAssetID = getIonAssetID(
this->_pIonObject),
connection = *maybeConnection](
Response<Token>&& response) {
if (!pStillAlive.IsValid()) {
// UObject has been destroyed
return connection.getAsyncSystem().createResolvedFuture();
}
if (!response.value) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT(
"Cannot grant a token access to an asset because the token was not found in the signed-in Cesium ion account."));
return connection.getAsyncSystem().createResolvedFuture();
}
if (!response.value->assetIds) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT(
"Cannot grant a token access to an asset because the token appears to already have access to all assets."));
return connection.getAsyncSystem().createResolvedFuture();
}
auto it = std::find(
response.value->assetIds->begin(),
response.value->assetIds->end(),
ionAssetID);
if (it != response.value->assetIds->end()) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT(
"Cannot grant a token access to an asset because the token appears to already have access to the asset."));
return connection.getAsyncSystem().createResolvedFuture();
}
response.value->assetIds->emplace_back(ionAssetID);
return connection
.modifyToken(
response.value->id,
response.value->name,
response.value->assetIds,
response.value->scopes,
response.value->allowedUrls)
.thenInMainThread([pIonObject, pStillAlive, removeObjectToken](
Response<NoValue>&& result) {
if (result.value) {
// Refresh the object now that the token is valid (hopefully).
if (pStillAlive.IsValid()) {
if (removeObjectToken) {
setIonAccessToken(pIonObject, FString());
} else {
// Set the token to the same value to force a refresh.
setIonAccessToken(pIonObject, getIonAccessToken(pIonObject));
}
}
} else {
UE_LOG(
LogCesiumEditor,
Error,
TEXT(
"An error occurred while attempting to modify a token to grant it access to an asset. Please visit https://cesium.com/ion/tokens to modify the token manually."));
}
});
});
}

View File

@ -0,0 +1,86 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/SWindow.h"
#include <optional>
#include <swl/variant.hpp>
#include <vector>
class ACesium3DTileset;
class UCesiumRasterOverlay;
using CesiumIonObject = swl::variant<
TWeakObjectPtr<ACesium3DTileset>,
TWeakObjectPtr<UCesiumRasterOverlay>>;
class CesiumIonTokenTroubleshooting : public SWindow {
SLATE_BEGIN_ARGS(CesiumIonTokenTroubleshooting) {}
/**
* The tileset being troubleshooted.
*/
SLATE_ARGUMENT(CesiumIonObject, IonObject)
/** Whether this troubleshooting panel was opened in response to an error,
* versus opened manually by the user. */
SLATE_ARGUMENT(bool, TriggeredByError)
SLATE_END_ARGS()
public:
static void Open(CesiumIonObject ionObject, bool triggeredByError);
void Construct(const FArguments& InArgs);
private:
struct ExistingPanel {
CesiumIonObject pObject;
TSharedRef<CesiumIonTokenTroubleshooting> pPanel;
};
static std::vector<ExistingPanel> _existingPanels;
struct TokenState {
FString name;
FString token;
std::optional<bool> isValid;
std::optional<bool> allowsAccessToAsset;
std::optional<bool> associatedWithUserAccount;
};
CesiumIonObject _pIonObject{};
TokenState _assetTokenState{};
TokenState _projectDefaultTokenState{};
std::optional<bool> _assetExistsInUserAccount;
TSharedRef<SWidget> createDiagnosticPanel(
const FString& name,
const TArray<TSharedRef<SWidget>>& diagnostics);
TSharedRef<SWidget>
createTokenPanel(const CesiumIonObject& pIonObject, TokenState& state);
void addRemedyButton(
const TSharedRef<SVerticalBox>& pParent,
const FString& name,
bool (CesiumIonTokenTroubleshooting::*isAvailableCallback)() const,
void (CesiumIonTokenTroubleshooting::*clickCallback)());
bool canConnectToCesiumIon() const;
void connectToCesiumIon();
bool canUseProjectDefaultToken() const;
void useProjectDefaultToken();
bool canAuthorizeAssetToken() const;
void authorizeAssetToken();
bool canAuthorizeProjectDefaultToken() const;
void authorizeProjectDefaultToken();
bool canSelectNewProjectDefaultToken() const;
void selectNewProjectDefaultToken();
bool canOpenCesiumIon() const;
void openCesiumIon();
void authorizeToken(const FString& token, bool removeObjectToken);
};

View File

@ -0,0 +1,328 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumPanel.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Cesium3DTileset.h"
#include "CesiumCommands.h"
#include "CesiumEditor.h"
#include "CesiumIonPanel.h"
#include "CesiumIonServer.h"
#include "CesiumIonServerSelector.h"
#include "CesiumRuntime.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumUtility/Uri.h"
#include "Editor.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Interfaces/IPluginManager.h"
#include "IonLoginPanel.h"
#include "IonQuickAddPanel.h"
#include "LevelEditor.h"
#include "SelectCesiumIonToken.h"
#include "Styling/SlateStyleRegistry.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/Layout/SScrollBox.h"
CesiumPanel::CesiumPanel() : _pQuickAddPanel(nullptr), _pLastServer(nullptr) {
this->_serverChangedDelegateHandle =
FCesiumEditorModule::serverManager().CurrentServerChanged.AddRaw(
this,
&CesiumPanel::OnServerChanged);
this->OnServerChanged();
}
CesiumPanel::~CesiumPanel() {
this->Subscribe(nullptr);
FCesiumEditorModule::serverManager().CurrentServerChanged.Remove(
this->_serverChangedDelegateHandle);
}
void CesiumPanel::Construct(const FArguments& InArgs) {
FCesiumEditorModule::serverManager().ResumeAll();
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetCurrentSession();
pSession->refreshDefaultsIfNeeded();
ChildSlot
[SNew(SVerticalBox) +
SVerticalBox::Slot().AutoHeight().Padding(
5.0f)[SNew(CesiumIonServerSelector)] +
SVerticalBox::Slot().AutoHeight()[Toolbar()] +
SVerticalBox::Slot().VAlign(VAlign_Fill)
[SNew(SScrollBox) + SScrollBox::Slot()[BasicQuickAddPanel()] +
SScrollBox::Slot()[LoginPanel()] +
SScrollBox::Slot()[MainIonQuickAddPanel()]] +
SVerticalBox::Slot()
.AutoHeight()
.VAlign(VAlign_Bottom)
.HAlign(HAlign_Right)[Version()]];
}
void CesiumPanel::Tick(
const FGeometry& AllottedGeometry,
const double InCurrentTime,
const float InDeltaTime) {
getAsyncSystem().dispatchMainThreadTasks();
SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
}
void CesiumPanel::Refresh() {
if (!this->_pQuickAddPanel)
return;
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetCurrentSession();
this->_pQuickAddPanel->ClearItems();
if (pSession->isLoadingDefaults()) {
this->_pQuickAddPanel->SetMessage(FText::FromString("Loading..."));
} else if (!pSession->isDefaultsLoaded()) {
this->_pQuickAddPanel->SetMessage(
FText::FromString("This server does not define any Quick Add assets."));
} else {
const CesiumIonClient::Defaults& defaults = pSession->getDefaults();
this->_pQuickAddPanel->SetMessage(FText());
for (const CesiumIonClient::QuickAddAsset& asset :
defaults.quickAddAssets) {
if (asset.type == "3DTILES" ||
(asset.type == "TERRAIN" && !asset.rasterOverlays.empty())) {
this->_pQuickAddPanel->AddItem(QuickAddItem{
QuickAddItemType::TILESET,
asset.name,
asset.description,
asset.objectName,
asset.assetId,
asset.rasterOverlays.empty() ? "" : asset.rasterOverlays[0].name,
asset.rasterOverlays.empty() ? -1
: asset.rasterOverlays[0].assetId});
}
}
}
this->_pQuickAddPanel->Refresh();
}
void CesiumPanel::Subscribe(UCesiumIonServer* pNewServer) {
if (this->_pLastServer) {
std::shared_ptr<CesiumIonSession> pLastSession =
FCesiumEditorModule::serverManager().GetSession(this->_pLastServer);
if (pLastSession) {
pLastSession->ConnectionUpdated.RemoveAll(this);
pLastSession->DefaultsUpdated.RemoveAll(this);
}
}
this->_pLastServer = pNewServer;
if (pNewServer) {
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(pNewServer);
pSession->ConnectionUpdated.AddRaw(this, &CesiumPanel::OnConnectionUpdated);
pSession->DefaultsUpdated.AddRaw(this, &CesiumPanel::OnDefaultsUpdated);
}
}
void CesiumPanel::OnServerChanged() {
UCesiumIonServer* pNewServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
this->Subscribe(pNewServer);
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetCurrentSession();
if (pSession) {
pSession->refreshDefaultsIfNeeded();
}
this->Refresh();
}
static bool isSignedIn() {
return FCesiumEditorModule::serverManager()
.GetCurrentSession()
->isConnected();
}
static bool requiresTokenForServer() {
return FCesiumEditorModule::serverManager()
.GetCurrentSession()
->getAppData()
.needsOauthAuthentication();
}
TSharedRef<SWidget> CesiumPanel::Toolbar() {
TSharedRef<FUICommandList> commandList = MakeShared<FUICommandList>();
commandList->MapAction(
FCesiumCommands::Get().AddFromIon,
FExecuteAction::CreateSP(this, &CesiumPanel::addFromIon),
FCanExecuteAction::CreateStatic(isSignedIn));
commandList->MapAction(
FCesiumCommands::Get().UploadToIon,
FExecuteAction::CreateSP(this, &CesiumPanel::uploadToIon),
FCanExecuteAction::CreateStatic(isSignedIn));
commandList->MapAction(
FCesiumCommands::Get().OpenTokenSelector,
FExecuteAction::CreateSP(this, &CesiumPanel::openTokenSelector),
FCanExecuteAction::CreateStatic(requiresTokenForServer));
commandList->MapAction(
FCesiumCommands::Get().SignOut,
FExecuteAction::CreateSP(this, &CesiumPanel::signOut),
FCanExecuteAction::CreateStatic(isSignedIn));
commandList->MapAction(
FCesiumCommands::Get().OpenDocumentation,
FExecuteAction::CreateSP(this, &CesiumPanel::openDocumentation));
commandList->MapAction(
FCesiumCommands::Get().OpenSupport,
FExecuteAction::CreateSP(this, &CesiumPanel::openSupport));
FToolBarBuilder builder(commandList, FMultiBoxCustomization::None);
builder.AddToolBarButton(FCesiumCommands::Get().AddFromIon);
builder.AddToolBarButton(FCesiumCommands::Get().UploadToIon);
builder.AddToolBarButton(FCesiumCommands::Get().OpenTokenSelector);
builder.AddToolBarButton(FCesiumCommands::Get().OpenDocumentation);
builder.AddToolBarButton(FCesiumCommands::Get().OpenSupport);
builder.AddToolBarButton(FCesiumCommands::Get().SignOut);
return builder.MakeWidget();
}
TSharedRef<SWidget> CesiumPanel::LoginPanel() {
return SNew(IonLoginPanel).Visibility_Lambda([]() {
return isSignedIn() ? EVisibility::Collapsed : EVisibility::Visible;
});
}
TSharedRef<SWidget> CesiumPanel::MainIonQuickAddPanel() {
FCesiumEditorModule::serverManager()
.GetCurrentSession()
->refreshDefaultsIfNeeded();
this->_pQuickAddPanel =
SNew(IonQuickAddPanel)
.Title(FText::FromString("Quick Add Cesium ion Assets"))
.Visibility_Lambda([]() {
return isSignedIn() ? EVisibility::Visible : EVisibility::Collapsed;
});
this->Refresh();
return this->_pQuickAddPanel.ToSharedRef();
}
TSharedRef<SWidget> CesiumPanel::BasicQuickAddPanel() {
TSharedPtr<IonQuickAddPanel> quickAddPanel =
SNew(IonQuickAddPanel).Title(FText::FromString("Quick Add Basic Actors"));
quickAddPanel->AddItem(
{QuickAddItemType::TILESET,
"Blank 3D Tiles Tileset",
"An empty tileset that can be configured to show Cesium ion assets or tilesets from other sources.",
"Blank Tileset",
-1,
"",
-1});
quickAddPanel->AddItem(
{QuickAddItemType::SUNSKY,
"Cesium SunSky",
"An actor that represents a geospatially accurate sun and sky.",
"",
-1,
"",
-1});
quickAddPanel->AddItem(QuickAddItem{
QuickAddItemType::DYNAMIC_PAWN,
"Dynamic Pawn",
"A pawn that can be used to intuitively navigate in a geospatial environment.",
"",
-1,
"",
-1});
quickAddPanel->AddItem(QuickAddItem{
QuickAddItemType::CARTOGRAPHIC_POLYGON,
"Cesium Cartographic Polygon",
"An actor that can be used to draw out regions for use with clipping or other material effects.",
"",
-1,
"",
-1});
return quickAddPanel.ToSharedRef();
}
TSharedRef<SWidget> CesiumPanel::Version() {
IPluginManager& PluginManager = IPluginManager::Get();
TSharedPtr<IPlugin> Plugin =
PluginManager.FindPlugin(TEXT("CesiumForUnreal"));
FString Version = Plugin ? TEXT("v") + Plugin->GetDescriptor().VersionName
: TEXT("Unknown Version");
return SNew(SHyperlink)
.Text(FText::FromString(Version))
.ToolTipText(FText::FromString(
TEXT("Open the Cesium for Unreal changelog in your web browser")))
.OnNavigate_Lambda([]() {
FPlatformProcess::LaunchURL(
TEXT(
"https://github.com/CesiumGS/cesium-unreal/blob/main/CHANGES.md"),
NULL,
NULL);
});
}
void CesiumPanel::OnConnectionUpdated() {
FCesiumEditorModule::serverManager().GetCurrentSession()->refreshDefaults();
this->Refresh();
}
void CesiumPanel::OnDefaultsUpdated() { this->Refresh(); }
void CesiumPanel::addFromIon() {
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(
FName(TEXT("LevelEditor")));
TSharedPtr<FTabManager> pTabManager =
pLevelEditorModule ? pLevelEditorModule->GetLevelEditorTabManager()
: FGlobalTabmanager::Get();
pTabManager->TryInvokeTab(FTabId(TEXT("CesiumIon")));
}
void CesiumPanel::uploadToIon() {
UCesiumIonServer* pServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(CesiumUtility::Uri::resolve(
TCHAR_TO_UTF8(*pServer->ServerUrl),
"addasset")
.c_str()),
NULL,
NULL);
}
void CesiumPanel::visitIon() {
UCesiumIonServer* pServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
FPlatformProcess::LaunchURL(*pServer->ServerUrl, NULL, NULL);
}
void CesiumPanel::signOut() {
FCesiumEditorModule::serverManager().GetCurrentSession()->disconnect();
}
void CesiumPanel::openDocumentation() {
FPlatformProcess::LaunchURL(TEXT("https://cesium.com/docs"), NULL, NULL);
}
void CesiumPanel::openSupport() {
FPlatformProcess::LaunchURL(
TEXT("https://community.cesium.com/"),
NULL,
NULL);
}
void CesiumPanel::openTokenSelector() {
SelectCesiumIonToken::SelectNewToken(
FCesiumEditorModule::serverManager().GetCurrentServer());
}

View File

@ -0,0 +1,50 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/SCompoundWidget.h"
class FArguments;
class UCesiumIonServer;
class IonQuickAddPanel;
class CesiumPanel : public SCompoundWidget {
SLATE_BEGIN_ARGS(CesiumPanel) {}
SLATE_END_ARGS()
CesiumPanel();
virtual ~CesiumPanel();
void Construct(const FArguments& InArgs);
virtual void Tick(
const FGeometry& AllottedGeometry,
const double InCurrentTime,
const float InDeltaTime) override;
void Refresh();
void Subscribe(UCesiumIonServer* pNewServer);
void OnServerChanged();
private:
TSharedRef<SWidget> Toolbar();
TSharedRef<SWidget> LoginPanel();
TSharedRef<SWidget> MainIonQuickAddPanel();
TSharedRef<SWidget> BasicQuickAddPanel();
TSharedRef<SWidget> Version();
void OnConnectionUpdated();
void OnDefaultsUpdated();
void addFromIon();
void uploadToIon();
void visitIon();
void signOut();
void openDocumentation();
void openSupport();
void openTokenSelector();
TSharedPtr<IonQuickAddPanel> _pQuickAddPanel;
TObjectPtr<UCesiumIonServer> _pLastServer;
FDelegateHandle _serverChangedDelegateHandle;
};

View File

@ -0,0 +1,57 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumSourceControl.h"
#include "Framework/Notifications/NotificationManager.h"
#include "HAL/PlatformFileManager.h"
#include "ISourceControlModule.h"
#include "ISourceControlProvider.h"
#include "Misc/MessageDialog.h"
#include "SourceControlOperations.h"
#include "Widgets/Notifications/SNotificationList.h"
void CesiumSourceControl::PromptToCheckoutConfigFile(
const FString& RelativeConfigFilePath) {
if (ISourceControlModule::Get().IsEnabled()) {
FString ConfigFilePath =
FPaths::ConvertRelativePathToFull(RelativeConfigFilePath);
FText ConfigFilename =
FText::FromString(FPaths::GetCleanFilename(ConfigFilePath));
ISourceControlProvider& SourceControlProvider =
ISourceControlModule::Get().GetProvider();
FSourceControlStatePtr SourceControlState =
SourceControlProvider.GetState(ConfigFilePath, EStateCacheUsage::Use);
if (SourceControlState.IsValid() &&
SourceControlState->IsSourceControlled()) {
TArray<FString> FilesToBeCheckedOut;
FilesToBeCheckedOut.Add(ConfigFilePath);
if (SourceControlState->CanCheckout() ||
SourceControlState->IsCheckedOutOther() ||
FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(
*ConfigFilePath)) {
FString Message = FString::Format(
TEXT(
"The default access token is saved in {0} which is currently not checked out. Would you like to check it out from source control?"),
{ConfigFilename.ToString()});
if (FMessageDialog::Open(
EAppMsgType::YesNo,
FText::FromString(Message)) == EAppReturnType::Yes) {
ECommandResult::Type CommandResult = SourceControlProvider.Execute(
ISourceControlOperation::Create<FCheckOut>(),
FilesToBeCheckedOut);
if (CommandResult != ECommandResult::Succeeded) {
// Show a notification that the file could not be checked out
FNotificationInfo CheckOutError(FText::FromString(
TEXT("Error: Failed to check out the configuration file.")));
CheckOutError.ExpireDuration = 3.0f;
FSlateNotificationManager::Get().AddNotification(CheckOutError);
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Containers/UnrealString.h"
class CesiumSourceControl {
public:
static void PromptToCheckoutConfigFile(const FString& RelativeConfigFilePath);
};

View File

@ -0,0 +1,229 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "IonLoginPanel.h"
#include "CesiumEditor.h"
#include "CesiumIonClient/Connection.h"
#include "CesiumIonClient/Token.h"
#include "CesiumIonServer.h"
#include "HAL/PlatformApplicationMisc.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Misc/App.h"
#include "Styling/SlateStyle.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Images/SThrobber.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
using namespace CesiumIonClient;
void IonLoginPanel::Construct(const FArguments& InArgs) {
auto visibleWhenConnecting = [this]() {
return FCesiumEditorModule::serverManager()
.GetCurrentSession()
->isConnecting() &&
!FCesiumEditorModule::serverManager()
.GetCurrentServer()
->ApiUrl.IsEmpty()
? EVisibility::Visible
: EVisibility::Collapsed;
};
auto visibleWhenResuming = [this]() {
return FCesiumEditorModule::serverManager()
.GetCurrentSession()
->isResuming()
? EVisibility::Visible
: EVisibility::Collapsed;
};
auto visibleWhenNotConnectingOrResuming = []() {
return FCesiumEditorModule::serverManager()
.GetCurrentSession()
->isConnecting() ||
FCesiumEditorModule::serverManager()
.GetCurrentSession()
->isResuming()
? EVisibility::Collapsed
: EVisibility::Visible;
};
// TODO Format this, and disable clang format here
TSharedPtr<SVerticalBox> connectionStatusWidget =
SNew(SVerticalBox).Visibility_Lambda(visibleWhenConnecting) +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(5, 15, 5, 5)
.AutoHeight()
[SNew(STextBlock)
.Text(FText::FromString(TEXT(
"Waiting for you to sign into Cesium ion with your web browser...")))
.AutoWrapText(true)] +
SVerticalBox::Slot()
.HAlign(HAlign_Center)
.Padding(5)[SNew(SThrobber).Animate(SThrobber::Horizontal)] +
SVerticalBox::Slot()
.HAlign(HAlign_Center)
.Padding(5)
.AutoHeight()
[SNew(SHyperlink)
.OnNavigate(this, &IonLoginPanel::LaunchBrowserAgain)
.Text(FText::FromString(TEXT("Open web browser again")))] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(5)
.AutoHeight()[SNew(STextBlock)
.Text(FText::FromString(TEXT(
"Or copy the URL below into your web browser")))
.AutoWrapText(true)] +
SVerticalBox::Slot()
.HAlign(HAlign_Center)
.AutoHeight()
[SNew(SHorizontalBox) +
SHorizontalBox::Slot().VAlign(VAlign_Center)[SNew(
SBorder)[SNew(SEditableText)
.IsReadOnly(true)
.Text_Lambda([this]() {
return FText::FromString(UTF8_TO_TCHAR(
FCesiumEditorModule::serverManager()
.GetCurrentSession()
->getAuthorizeUrl()
.c_str()));
})]] +
SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.HAlign(HAlign_Right)
.AutoWidth()
[SNew(SButton)
.OnClicked(
this,
&IonLoginPanel::CopyAuthorizeUrlToClipboard)
.Text(
FText::FromString(TEXT("Copy to clipboard")))]];
TSharedPtr<SVerticalBox> connectionWidget =
SNew(SVerticalBox) +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.HAlign(HAlign_Center)
.Padding(5)
.AutoHeight()
[SNew(SButton)
.Visibility_Lambda(visibleWhenNotConnectingOrResuming)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.OnClicked(this, &IonLoginPanel::SignIn)
.Text(FText::FromString(TEXT("Connect to Cesium ion")))] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.HAlign(HAlign_Center)
.Padding(5)
.AutoHeight()
[SNew(SButton)
.Visibility_Lambda(visibleWhenConnecting)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(
FCesiumEditorModule::GetStyle(),
"CesiumButtonText")
.OnClicked(this, &IonLoginPanel::CancelSignIn)
.Text(FText::FromString(TEXT("Cancel Connecting")))] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(10, 0, 10, 5)
.AutoHeight()
[SNew(STextBlock)
.Visibility_Lambda([visibleWhenNotConnectingOrResuming]() {
// Only show this message for the SaaS server.
UCesiumIonServer* Server =
FCesiumEditorModule::serverManager()
.GetCurrentServer();
if (Server->GetName() != TEXT("CesiumIonSaaS"))
return EVisibility::Collapsed;
return visibleWhenNotConnectingOrResuming();
})
.AutoWrapText(true)
.TextStyle(FCesiumEditorModule::GetStyle(), "BodyBold")
.Text(FText::FromString(TEXT(
"You can now sign in with your Epic Games account!")))] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(5, 15, 5, 5)
.AutoHeight()[SNew(STextBlock)
.Text(FText::FromString(
TEXT("Resuming the previous connection...")))
.Visibility_Lambda(visibleWhenResuming)
.AutoWrapText(true)] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()[connectionStatusWidget.ToSharedRef()];
ChildSlot
[SNew(SScrollBox) +
SScrollBox::Slot()
.VAlign(VAlign_Top)
.HAlign(HAlign_Center)
.Padding(20, 0, 20, 5)
[SNew(SScaleBox)
.Stretch(EStretch::ScaleToFit)
.HAlign(HAlign_Center)
.VAlign(VAlign_Top)[SNew(SImage).Image(
FCesiumEditorModule::GetStyle()->GetBrush(
TEXT("Cesium.Logo")))]] +
SScrollBox::Slot()
.VAlign(VAlign_Top)
.Padding(30, 10, 30, 10)
[SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(TEXT(
"Access global high-resolution 3D content, including photogrammetry, terrain, imagery, and buildings. Bring your own data for tiling, hosting, and streaming to Unreal Engine.")))] +
SScrollBox::Slot()
.VAlign(VAlign_Top)
.HAlign(HAlign_Center)
.Padding(20)[connectionWidget.ToSharedRef()]];
}
FReply IonLoginPanel::SignIn() {
FCesiumEditorModule::serverManager().GetCurrentSession()->connect();
return FReply::Handled();
}
FReply IonLoginPanel::CopyAuthorizeUrlToClipboard() {
FText url =
FText::FromString(UTF8_TO_TCHAR(FCesiumEditorModule::serverManager()
.GetCurrentSession()
->getAuthorizeUrl()
.c_str()));
FPlatformApplicationMisc::ClipboardCopy(*url.ToString());
return FReply::Handled();
}
void IonLoginPanel::LaunchBrowserAgain() {
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(FCesiumEditorModule::serverManager()
.GetCurrentSession()
->getAuthorizeUrl()
.c_str()),
NULL,
NULL);
}
FReply IonLoginPanel::CancelSignIn() {
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> pRequest =
FHttpModule::Get().CreateRequest();
pRequest->SetURL(UTF8_TO_TCHAR(FCesiumEditorModule::serverManager()
.GetCurrentSession()
->getRedirectUrl()
.c_str()));
pRequest->ProcessRequest();
return FReply::Handled();
}

View File

@ -0,0 +1,21 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/SCompoundWidget.h"
class FArguments;
class IonLoginPanel : public SCompoundWidget {
SLATE_BEGIN_ARGS(IonLoginPanel) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
void LaunchBrowserAgain();
FReply SignIn();
FReply CancelSignIn();
FReply CopyAuthorizeUrlToClipboard();
};

View File

@ -0,0 +1,386 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "IonQuickAddPanel.h"
#include "Cesium3DTileset.h"
#include "CesiumCartographicPolygon.h"
#include "CesiumEditor.h"
#include "CesiumIonClient/Connection.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServer.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumUtility/Uri.h"
#include "Editor.h"
#include "PropertyCustomizationHelpers.h"
#include "SelectCesiumIonToken.h"
#include "Styling/SlateStyle.h"
#include "Widgets/Images/SThrobber.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/Layout/SHeader.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/SListView.h"
using namespace CesiumIonClient;
void IonQuickAddPanel::Construct(const FArguments& InArgs) {
ChildSlot
[SNew(SVerticalBox) +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
.Padding(FMargin(5.0f, 20.0f, 5.0f, 10.0f))
[SNew(SHeader).Content()
[SNew(STextBlock)
.TextStyle(FCesiumEditorModule::GetStyle(), "Heading")
.Text(InArgs._Title)]] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()[SNew(STextBlock)
.Visibility_Lambda([this]() {
return this->_message.IsEmpty()
? EVisibility::Collapsed
: EVisibility::Visible;
})
.Text_Lambda([this]() { return this->_message; })
.AutoWrapText(true)] +
SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(FMargin(5.0f, 0.0f, 5.0f, 20.0f))[this->QuickAddList()]];
}
void IonQuickAddPanel::AddItem(const QuickAddItem& item) {
_quickAddItems.Add(MakeShared<QuickAddItem>(item));
}
void IonQuickAddPanel::ClearItems() { this->_quickAddItems.Empty(); }
void IonQuickAddPanel::Refresh() {
if (!this->_pQuickAddList)
return;
this->_pQuickAddList->RequestListRefresh();
}
const FText& IonQuickAddPanel::GetMessage() const { return this->_message; }
void IonQuickAddPanel::SetMessage(const FText& message) {
this->_message = message;
}
TSharedRef<SWidget> IonQuickAddPanel::QuickAddList() {
this->_pQuickAddList =
SNew(SListView<TSharedRef<QuickAddItem>>)
.SelectionMode(ESelectionMode::None)
.ListItemsSource(&_quickAddItems)
.OnMouseButtonDoubleClick(this, &IonQuickAddPanel::AddItemToLevel)
.OnGenerateRow(this, &IonQuickAddPanel::CreateQuickAddItemRow);
return this->_pQuickAddList.ToSharedRef();
}
TSharedRef<ITableRow> IonQuickAddPanel::CreateQuickAddItemRow(
TSharedRef<QuickAddItem> item,
const TSharedRef<STableViewBase>& list) {
// clang-format off
return SNew(STableRow<TSharedRef<QuickAddItem>>, list).Content()
[
SNew(SBox)
.HAlign(EHorizontalAlignment::HAlign_Fill)
.HeightOverride(40.0f)
.Content()
[
SNew(SHorizontalBox) +
SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(5.0f)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(UTF8_TO_TCHAR(item->name.c_str())))
.ToolTipText(FText::FromString(UTF8_TO_TCHAR(item->description.c_str())))
] +
SHorizontalBox::Slot()
.AutoWidth()
.VAlign(EVerticalAlignment::VAlign_Center)
[
PropertyCustomizationHelpers::MakeNewBlueprintButton(
FSimpleDelegate::CreateLambda(
[this, item]() { this->AddItemToLevel(item); }),
FText::FromString(
TEXT("Add this item to the level")),
TAttribute<bool>::Create([this, item]() {
return this->_itemsBeingAdded.find(item->name) ==
this->_itemsBeingAdded.end();
})
)
]
]
];
// clang-format on
}
namespace {
void showAssetDepotConfirmWindow(
const FString& itemName,
int64_t missingAsset) {
UCesiumIonServer* pServer =
FCesiumEditorModule::serverManager().GetCurrentServer();
std::string url = CesiumUtility::Uri::resolve(
TCHAR_TO_UTF8(*pServer->ServerUrl),
"assetdepot/" + std::to_string(missingAsset));
// clang-format off
TSharedRef<SWindow> AssetDepotConfirmWindow =
SNew(SWindow)
.Title(FText::FromString(TEXT("Asset is not available in My Assets")))
.ClientSize(FVector2D(400.0f, 200.0f))
.Content()
[
SNew(SVerticalBox) +
SVerticalBox::Slot().AutoHeight().Padding(10.0f)
[
SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(TEXT("Before " + itemName +
" can be added to your level, it must be added to \"My Assets\" in your Cesium ion account.")))
] +
SVerticalBox::Slot()
.AutoHeight()
.HAlign(EHorizontalAlignment::HAlign_Left)
.Padding(10.0f, 5.0f)
[
SNew(SHyperlink)
.OnNavigate_Lambda([url]() {
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(url.c_str()),
NULL,
NULL);
})
.Text(FText::FromString(TEXT(
"Open this asset in the Cesium ion Asset Depot")))
] +
SVerticalBox::Slot()
.AutoHeight()
.HAlign(EHorizontalAlignment::HAlign_Left)
.Padding(10.0f, 5.0f)
[
SNew(STextBlock).Text(FText::FromString(TEXT(
"Click \"Add to my assets\" in the Cesium ion web page")))
] +
SVerticalBox::Slot()
.AutoHeight()
.HAlign(EHorizontalAlignment::HAlign_Left)
.Padding(10.0f, 5.0f)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT(
"Return to Cesium for Unreal and try adding this asset again")))
] +
SVerticalBox::Slot()
.AutoHeight()
.HAlign(EHorizontalAlignment::HAlign_Center)
.Padding(10.0f, 25.0f)
[
SNew(SButton)
.OnClicked_Lambda(
[&AssetDepotConfirmWindow]() {
AssetDepotConfirmWindow
->RequestDestroyWindow();
return FReply::Handled();
})
.Text(FText::FromString(TEXT("Close")))
]
];
// clang-format on
GEditor->EditorAddModalWindow(AssetDepotConfirmWindow);
}
} // namespace
void IonQuickAddPanel::AddIonTilesetToLevel(TSharedRef<QuickAddItem> item) {
const std::optional<CesiumIonClient::Connection>& connection =
FCesiumEditorModule::serverManager().GetCurrentSession()->getConnection();
if (!connection) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Cannot add an ion asset without an active connection"));
return;
}
std::vector<int64_t> assetIDs{item->tilesetID};
if (item->overlayID > 0) {
assetIDs.push_back(item->overlayID);
}
SelectCesiumIonToken::SelectAndAuthorizeToken(
FCesiumEditorModule::serverManager().GetCurrentServer(),
assetIDs)
.thenInMainThread([connection, tilesetID = item->tilesetID](
const std::optional<Token>& /*maybeToken*/) {
// If token selection was canceled, or if an error occurred while
// selecting the token, ignore it and create the tileset anyway. It's
// already been logged if necessary, and we can let the user sort out
// the problem using the resulting Troubleshooting panel.
return connection->asset(tilesetID);
})
.thenInMainThread([item, connection](Response<Asset>&& response) {
if (!response.value.has_value()) {
return connection->getAsyncSystem().createResolvedFuture<int64_t>(
std::move(int64_t(item->tilesetID)));
}
if (item->overlayID >= 0) {
return connection->asset(item->overlayID)
.thenInMainThread([item](Response<Asset>&& overlayResponse) {
return overlayResponse.value.has_value()
? int64_t(-1)
: int64_t(item->overlayID);
});
} else {
return connection->getAsyncSystem().createResolvedFuture<int64_t>(-1);
}
})
.thenInMainThread([this, item](int64_t missingAsset) {
if (missingAsset != -1) {
FString itemName(UTF8_TO_TCHAR(item->name.c_str()));
showAssetDepotConfirmWindow(itemName, missingAsset);
} else {
ACesium3DTileset* pTileset =
FCesiumEditorModule::FindFirstTilesetWithAssetID(item->tilesetID);
if (!pTileset) {
pTileset = FCesiumEditorModule::CreateTileset(
item->tilesetName,
item->tilesetID);
}
FCesiumEditorModule::serverManager().GetCurrentSession()->getAssets();
if (item->overlayID > 0) {
FCesiumEditorModule::AddBaseOverlay(
pTileset,
item->overlayName,
item->overlayID);
}
pTileset->RerunConstructionScripts();
GEditor->SelectNone(true, false);
GEditor->SelectActor(pTileset, true, true, true, true);
}
this->_itemsBeingAdded.erase(item->name);
});
}
void IonQuickAddPanel::AddCesiumSunSkyToLevel() {
AActor* pActor = FCesiumEditorModule::GetCurrentLevelCesiumSunSky();
if (!pActor) {
pActor = FCesiumEditorModule::SpawnCesiumSunSky();
}
if (pActor) {
GEditor->SelectNone(true, false);
GEditor->SelectActor(pActor, true, true, true, true);
}
}
namespace {
/**
* Set a byte property value in the given object.
*
* This will search the class of the given object for a property
* with the given name, which is assumed to be a byte property,
* and assign the given value to this property.
*
* If the object is `nullptr`, nothing will be done.
* If the class does not contain a property with the given name,
* or it is not a byte property, a warning will be printed.
*
* @param object The object to set the value in
* @param name The name of the property
* @param value The value to set for the property.
*/
void SetBytePropertyValue(
UObjectBase* object,
const std::string& name,
uint8 value) {
if (!object) {
return;
}
UClass* cls = object->GetClass();
FString nameString(name.c_str());
FName propName = FName(*nameString);
FProperty* property = cls->FindPropertyByName(propName);
if (!property) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Property with name %s not found"),
*nameString);
return;
}
FByteProperty* byteProperty = CastField<FByteProperty>(property);
if (!byteProperty) {
UE_LOG(
LogCesiumEditor,
Warning,
TEXT("Property is not an FByteProperty: %s"),
*nameString);
return;
}
byteProperty->SetPropertyValue_InContainer(object, value);
}
} // namespace
void IonQuickAddPanel::AddDynamicPawnToLevel() {
AActor* pActor = FCesiumEditorModule::GetCurrentLevelDynamicPawn();
if (!pActor) {
pActor = FCesiumEditorModule::SpawnDynamicPawn();
}
if (pActor) {
uint8 autoPossessValue =
static_cast<uint8>(EAutoReceiveInput::Type::Player0);
SetBytePropertyValue(pActor, "AutoPossessPlayer", autoPossessValue);
GEditor->SelectNone(true, false);
GEditor->SelectActor(pActor, true, true, true, true);
}
}
void IonQuickAddPanel::AddItemToLevel(TSharedRef<QuickAddItem> item) {
if (this->_itemsBeingAdded.find(item->name) != this->_itemsBeingAdded.end()) {
// Add is already in progress.
return;
}
this->_itemsBeingAdded.insert(item->name);
if (item->type == QuickAddItemType::TILESET) {
// The blank tileset (identified by the tileset and overlay ID being -1)
// can be added directly. All ion tilesets are added via
// AddIonTilesetToLevel, which requires an active connection.
bool isBlankTileset = item->type == QuickAddItemType::TILESET &&
item->tilesetID == -1 && item->overlayID == -1;
if (isBlankTileset) {
FCesiumEditorModule::SpawnBlankTileset();
this->_itemsBeingAdded.erase(item->name);
} else {
AddIonTilesetToLevel(item);
}
} else if (item->type == QuickAddItemType::SUNSKY) {
AddCesiumSunSkyToLevel();
this->_itemsBeingAdded.erase(item->name);
} else if (item->type == QuickAddItemType::DYNAMIC_PAWN) {
AddDynamicPawnToLevel();
this->_itemsBeingAdded.erase(item->name);
} else if (item->type == QuickAddItemType::CARTOGRAPHIC_POLYGON) {
FCesiumEditorModule::SpawnCartographicPolygon();
this->_itemsBeingAdded.erase(item->name);
}
}

View File

@ -0,0 +1,61 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Views/STableRow.h"
#include <string>
#include <unordered_set>
class FArguments;
enum class QuickAddItemType {
TILESET,
SUNSKY,
DYNAMIC_PAWN,
CARTOGRAPHIC_POLYGON
};
struct QuickAddItem {
QuickAddItemType type;
std::string name{};
std::string description;
std::string tilesetName{};
int64_t tilesetID = -1;
std::string overlayName{};
int64_t overlayID = -1;
};
class IonQuickAddPanel : public SCompoundWidget {
SLATE_BEGIN_ARGS(IonQuickAddPanel) {}
/**
* The tile shown over the elements of the list
*/
SLATE_ARGUMENT(FText, Title)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
void AddItem(const QuickAddItem& item);
void ClearItems();
void Refresh();
const FText& GetMessage() const;
void SetMessage(const FText& message);
private:
TSharedRef<SWidget> QuickAddList();
TSharedRef<ITableRow> CreateQuickAddItemRow(
TSharedRef<QuickAddItem> item,
const TSharedRef<STableViewBase>& list);
void AddItemToLevel(TSharedRef<QuickAddItem> item);
void AddIonTilesetToLevel(TSharedRef<QuickAddItem> item);
void AddCesiumSunSkyToLevel();
void AddDynamicPawnToLevel();
TArray<TSharedRef<QuickAddItem>> _quickAddItems;
std::unordered_set<std::string> _itemsBeingAdded;
TSharedPtr<SListView<TSharedRef<QuickAddItem>>> _pQuickAddList;
FText _message;
};

View File

@ -0,0 +1,621 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "SelectCesiumIonToken.h"
#include "Cesium3DTileset.h"
#include "CesiumEditor.h"
#include "CesiumIonRasterOverlay.h"
#include "CesiumIonServerDisplay.h"
#include "CesiumRuntime.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumSourceControl.h"
#include "CesiumUtility/joinToString.h"
#include "Editor.h"
#include "EditorStyleSet.h"
#include "EngineUtils.h"
#include "Framework/Application/SlateApplication.h"
#include "Misc/App.h"
#include "PropertyCustomizationHelpers.h"
#include "Runtime/Launch/Resources/Version.h"
#include "ScopedTransaction.h"
#include "Widgets/Images/SThrobber.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Text/STextBlock.h"
using namespace CesiumAsync;
using namespace CesiumIonClient;
using namespace CesiumUtility;
/*static*/ TSharedPtr<SelectCesiumIonToken>
SelectCesiumIonToken::_pExistingPanel{};
/*static*/ SharedFuture<std::optional<Token>>
SelectCesiumIonToken::SelectNewToken(UCesiumIonServer* pServer) {
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(pServer);
// If the current server doesn't require tokens, don't bother opening the
// window to create one.
if (!pSession->isAuthenticationRequired()) {
return getAsyncSystem()
.createResolvedFuture(std::optional<Token>(Token()))
.share();
}
if (SelectCesiumIonToken::_pExistingPanel.IsValid()) {
SelectCesiumIonToken::_pExistingPanel->BringToFront();
} else {
TSharedRef<SelectCesiumIonToken> Panel =
SNew(SelectCesiumIonToken).Server(pServer);
SelectCesiumIonToken::_pExistingPanel = Panel;
Panel->_promise = getAsyncSystem().createPromise<std::optional<Token>>();
Panel->_future = Panel->_promise->getFuture().share();
Panel->GetOnWindowClosedEvent().AddLambda(
[Panel](const TSharedRef<SWindow>& pWindow) {
if (Panel->_promise) {
// Promise is still outstanding, so resolve it now (no token was
// selected).
Panel->_promise->resolve(std::nullopt);
}
SelectCesiumIonToken::_pExistingPanel.Reset();
});
FSlateApplication::Get().AddWindow(Panel);
}
return *SelectCesiumIonToken::_pExistingPanel->_future;
}
Future<std::optional<Token>>
SelectCesiumIonToken::SelectTokenIfNecessary(UCesiumIonServer* pServer) {
return FCesiumEditorModule::serverManager()
.GetSession(pServer)
->getProjectDefaultTokenDetails()
.thenInMainThread([pServer](const Token& token) {
if (token.token.empty()) {
return SelectCesiumIonToken::SelectNewToken(pServer).thenImmediately(
[](const std::optional<Token>& maybeToken) {
return maybeToken;
});
} else {
return getAsyncSystem().createResolvedFuture(
std::make_optional(token));
}
});
}
namespace {
std::vector<int64_t> findUnauthorizedAssets(
const std::vector<int64_t>& authorizedAssets,
const std::vector<int64_t>& requiredAssets) {
std::vector<int64_t> missingAssets;
for (int64_t assetID : requiredAssets) {
auto it =
std::find(authorizedAssets.begin(), authorizedAssets.end(), assetID);
if (it == authorizedAssets.end()) {
missingAssets.emplace_back(assetID);
}
}
return missingAssets;
}
} // namespace
Future<std::optional<Token>> SelectCesiumIonToken::SelectAndAuthorizeToken(
UCesiumIonServer* pServer,
const std::vector<int64_t>& assetIDs) {
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(pServer);
// If the current server doesn't require tokens, don't try to create or
// authorize one.
if (!pSession->isAuthenticationRequired()) {
return getAsyncSystem().createResolvedFuture(std::optional<Token>(Token()));
}
return SelectTokenIfNecessary(pServer).thenInMainThread([pSession, assetIDs](
const std::optional<
Token>&
maybeToken) {
const std::optional<Connection>& maybeConnection =
pSession->getConnection();
if (maybeConnection && maybeToken && !maybeToken->id.empty() &&
maybeToken->assetIds) {
std::vector<int64_t> missingAssets =
findUnauthorizedAssets(*maybeToken->assetIds, assetIDs);
if (!missingAssets.empty()) {
// Refresh the token details. We don't want to update the token based
// on stale information.
return maybeConnection->token(maybeToken->id)
.thenInMainThread([pSession, maybeToken, assetIDs](
Response<Token>&& response) {
if (response.value) {
std::vector<int64_t> missingAssets =
findUnauthorizedAssets(*maybeToken->assetIds, assetIDs);
if (!missingAssets.empty()) {
std::vector<std::string> idStrings(missingAssets.size());
std::transform(
missingAssets.begin(),
missingAssets.end(),
idStrings.begin(),
[](int64_t id) { return std::to_string(id); });
UE_LOG(
LogCesiumEditor,
Warning,
TEXT(
"Authorizing the project's default Cesium ion token to access the following asset IDs: %s"),
UTF8_TO_TCHAR(joinToString(idStrings, ", ").c_str()));
Token newToken = *maybeToken;
size_t destinationIndex = newToken.assetIds->size();
newToken.assetIds->resize(
newToken.assetIds->size() + missingAssets.size());
std::copy(
missingAssets.begin(),
missingAssets.end(),
newToken.assetIds->begin() + destinationIndex);
return pSession->getConnection()
->modifyToken(
newToken.id,
newToken.name,
newToken.assetIds,
newToken.scopes,
newToken.allowedUrls)
.thenImmediately([maybeToken](Response<NoValue>&&) {
return maybeToken;
});
}
}
return getAsyncSystem().createResolvedFuture(
std::optional<Token>(maybeToken));
});
}
}
return getAsyncSystem().createResolvedFuture(
std::optional<Token>(maybeToken));
});
}
void SelectCesiumIonToken::Construct(const FArguments& InArgs) {
UCesiumIonServer* pServer = InArgs._Server;
if (this->_pServer.IsValid() &&
this->_tokensUpdatedDelegateHandle.IsValid()) {
FCesiumEditorModule::serverManager()
.GetSession(this->_pServer.Get())
->TokensUpdated.Remove(this->_tokensUpdatedDelegateHandle);
}
this->_pServer = pServer;
std::shared_ptr<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(this->_pServer.Get());
this->_tokensUpdatedDelegateHandle = pSession->TokensUpdated.AddRaw(
this,
&SelectCesiumIonToken::RefreshTokens);
TSharedRef<SVerticalBox> pLoaderOrContent =
SNew(SVerticalBox).Visibility_Lambda([pSession]() {
return pSession->getAppData().needsOauthAuthentication()
? EVisibility::Visible
: EVisibility::Collapsed;
});
TSharedRef<SVerticalBox> pSingleUserText =
SNew(SVerticalBox).Visibility_Lambda([pSession]() {
return !pSession->getAppData().needsOauthAuthentication()
? EVisibility::Visible
: EVisibility::Collapsed;
});
pSingleUserText->AddSlot().AutoHeight()
[SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(TEXT(
"Cesium for Unreal is currently connected to a Cesium ion server running in single-user authentication mode. Tokens are not used in this mode.")))];
pLoaderOrContent->AddSlot().AutoHeight()
[SNew(STextBlock)
.AutoWrapText(true)
.Text(FText::FromString(TEXT(
"Cesium for Unreal embeds a Cesium ion token in your project in order to allow it to access the assets you add to your levels. Select the Cesium ion token to use.")))];
pLoaderOrContent->AddSlot().AutoHeight().Padding(
5.0f)[SNew(CesiumIonServerDisplay).Server(pServer)];
pLoaderOrContent->AddSlot()
.Padding(0.0f, 10.0f, 0.0, 10.0f)
.AutoHeight()
[SNew(STextBlock)
.Visibility_Lambda([pSession]() {
return pSession->isConnected() ? EVisibility::Collapsed
: EVisibility::Visible;
})
.AutoWrapText(true)
.Text(FText::FromString(TEXT(
"Please connect to Cesium ion to select a token from your account or to create a new token.")))];
pLoaderOrContent->AddSlot()
.AutoHeight()[SNew(SThrobber).Visibility_Lambda([pSession]() {
return pSession->isLoadingTokenList() ? EVisibility::Visible
: EVisibility::Collapsed;
})];
TSharedRef<SVerticalBox> pMainVerticalBox =
SNew(SVerticalBox).Visibility_Lambda([pSession]() {
return pSession->isLoadingTokenList() ? EVisibility::Collapsed
: EVisibility::Visible;
});
pLoaderOrContent->AddSlot().AutoHeight()[pMainVerticalBox];
this->_createNewToken.name =
FString(FApp::GetProjectName()) + TEXT(" (Created by Cesium for Unreal)");
this->_useExistingToken.token.id =
TCHAR_TO_UTF8(*pServer->DefaultIonAccessTokenId);
this->_useExistingToken.token.token =
TCHAR_TO_UTF8(*pServer->DefaultIonAccessToken);
this->_specifyToken.token = pServer->DefaultIonAccessToken;
this->_tokenSource =
pServer->DefaultIonAccessToken.IsEmpty() && pSession->isConnected()
? TokenSource::Create
: TokenSource::Specify;
this->createRadioButton(
pSession,
pMainVerticalBox,
this->_tokenSource,
TokenSource::Create,
TEXT("Create a new token"),
true,
SNew(SHorizontalBox) +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
.Padding(5.0f)[SNew(STextBlock)
.Text(FText::FromString(TEXT("Name:")))] +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
.MaxWidth(500.0f)
.Padding(
5.0f)[SNew(SEditableTextBox)
.Text(this, &SelectCesiumIonToken::GetNewTokenName)
.MinDesiredWidth(200.0f)
.OnTextChanged(
this,
&SelectCesiumIonToken::SetNewTokenName)]);
SAssignNew(this->_pTokensCombo, SComboBox<TSharedPtr<Token>>)
.OptionsSource(&this->_tokens)
.OnGenerateWidget(
this,
&SelectCesiumIonToken::OnGenerateTokenComboBoxEntry)
.OnSelectionChanged(this, &SelectCesiumIonToken::OnSelectExistingToken)
.Content()[SNew(STextBlock).MinDesiredWidth(200.0f).Text_Lambda([this]() {
return this->_pTokensCombo.IsValid() &&
this->_pTokensCombo->GetSelectedItem().IsValid()
? FText::FromString(UTF8_TO_TCHAR(
this->_pTokensCombo->GetSelectedItem()->name.c_str()))
: FText::FromString(TEXT(""));
})];
this->createRadioButton(
pSession,
pMainVerticalBox,
this->_tokenSource,
TokenSource::UseExisting,
TEXT("Use an existing token"),
true,
SNew(SHorizontalBox) +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
.MaxWidth(500.0f)
.Padding(5.0f)[SNew(STextBlock)
.Text(FText::FromString(TEXT("Token:")))] +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(5.0f)
.AutoWidth()[this->_pTokensCombo.ToSharedRef()]);
this->createRadioButton(
pSession,
pMainVerticalBox,
this->_tokenSource,
TokenSource::Specify,
TEXT("Specify a token"),
false,
SNew(SHorizontalBox) +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
.Padding(5.0f)[SNew(STextBlock)
.Text(FText::FromString(TEXT("Token:")))] +
SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.Padding(5.0f)
.AutoWidth()
.MaxWidth(500.0f)
[SNew(SEditableTextBox)
.Text(this, &SelectCesiumIonToken::GetSpecifiedToken)
.OnTextChanged(
this,
&SelectCesiumIonToken::SetSpecifiedToken)
.MinDesiredWidth(500.0f)]);
pMainVerticalBox->AddSlot().AutoHeight().Padding(
5.0f,
20.0f,
5.0f,
5.0f)[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText")
.Visibility_Lambda([this]() {
return this->_tokenSource == TokenSource ::Create
? EVisibility::Collapsed
: EVisibility::Visible;
})
.OnClicked(this, &SelectCesiumIonToken::UseOrCreate, pSession)
.Text(FText::FromString(TEXT("Use as Project Default Token")))];
pMainVerticalBox->AddSlot().AutoHeight().Padding(
5.0f,
20.0f,
5.0f,
5.0f)[SNew(SButton)
.ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton")
.TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText")
.Visibility_Lambda([this]() {
return this->_tokenSource == TokenSource ::Create
? EVisibility::Visible
: EVisibility::Collapsed;
})
.OnClicked(this, &SelectCesiumIonToken::UseOrCreate, pSession)
.Text(FText::FromString(
TEXT("Create New Project Default Token")))];
TSharedRef<SVerticalBox> totalBox = SNew(SVerticalBox);
totalBox->AddSlot().AutoHeight()[pLoaderOrContent];
totalBox->AddSlot().AutoHeight()[pSingleUserText];
SWindow::Construct(
SWindow::FArguments()
.Title(FText::FromString(TEXT("Select a Cesium ion Token")))
.AutoCenter(EAutoCenter::PreferredWorkArea)
.SizingRule(ESizingRule::UserSized)
.ClientSize(FVector2D(
635,
500))[SNew(SBorder)
.Visibility(EVisibility::Visible)
.Padding(
FMargin(10.0f, 10.0f, 10.0f, 10.0f))[totalBox]]);
pSession->refreshTokens();
}
void SelectCesiumIonToken::createRadioButton(
const std::shared_ptr<CesiumIonSession>& pSession,
const TSharedRef<SVerticalBox>& pVertical,
TokenSource& tokenSource,
TokenSource thisValue,
const FString& label,
bool requiresIonConnection,
const TSharedRef<SWidget>& pWidget) {
auto visibility = [pSession, requiresIonConnection]() {
if (!requiresIonConnection) {
return EVisibility::Visible;
} else if (pSession->isConnected()) {
return EVisibility::Visible;
} else {
return EVisibility::Collapsed;
}
};
pVertical->AddSlot().AutoHeight().Padding(5.0f, 10.0f, 5.0f, 10.0f)
[SNew(SCheckBox)
.Visibility_Lambda(visibility)
.Padding(5.0f)
.Style(FAppStyle::Get(), "RadioButton")
.IsChecked_Lambda([&tokenSource, thisValue]() {
return tokenSource == thisValue ? ECheckBoxState::Checked
: ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([&tokenSource,
thisValue](ECheckBoxState newState) {
if (newState == ECheckBoxState::Checked) {
tokenSource = thisValue;
}
})[SNew(SBorder)
[SNew(SVerticalBox) +
SVerticalBox::Slot().Padding(5.0f).AutoHeight()
[SNew(STextBlock)
.TextStyle(
FCesiumEditorModule::GetStyle(),
"BodyBold")
.Text(FText::FromString(label))] +
SVerticalBox::Slot().Padding(5.0f).AutoHeight()[pWidget]]]];
}
FReply
SelectCesiumIonToken::UseOrCreate(std::shared_ptr<CesiumIonSession> pSession) {
if (!this->_promise || !this->_future) {
return FReply::Handled();
}
Promise<std::optional<Token>> promise = std::move(*this->_promise);
this->_promise.reset();
TSharedRef<SelectCesiumIonToken> pPanel =
StaticCastSharedRef<SelectCesiumIonToken>(this->AsShared());
auto getToken = [pPanel, pSession]() {
const AsyncSystem& asyncSystem = getAsyncSystem();
if (pPanel->_tokenSource == TokenSource::Create) {
if (pPanel->_createNewToken.name.IsEmpty()) {
return asyncSystem.createResolvedFuture(Response<Token>());
}
// Create a new token, initially only with access to asset ID 1 (Cesium
// World Terrain).
return pSession->getConnection()->createToken(
TCHAR_TO_UTF8(*pPanel->_createNewToken.name),
{"assets:read"},
std::vector<int64_t>{1},
std::nullopt);
} else if (pPanel->_tokenSource == TokenSource::UseExisting) {
return asyncSystem.createResolvedFuture(
Response<Token>(Token(pPanel->_useExistingToken.token), 200, "", ""));
} else if (pPanel->_tokenSource == TokenSource::Specify) {
// Check if this is a known token, and use it if so.
return pSession->findToken(pPanel->_specifyToken.token)
.thenInMainThread([pPanel](Response<Token>&& response) {
if (response.value) {
return std::move(response);
} else {
Token t;
t.token = TCHAR_TO_UTF8(*pPanel->_specifyToken.token);
return Response(std::move(t), 200, "", "");
}
});
} else {
return asyncSystem.createResolvedFuture(
Response<Token>(0, "UNKNOWNSOURCE", "The token source is unknown."));
}
};
getToken().thenInMainThread([pPanel, pSession, promise = std::move(promise)](
Response<Token>&& response) {
if (response.value) {
pSession->invalidateProjectDefaultTokenDetails();
UCesiumIonServer* pServer = pPanel->_pServer.Get();
FScopedTransaction transaction(
FText::FromString("Set Project Default Token"));
pServer->DefaultIonAccessTokenId =
UTF8_TO_TCHAR(response.value->id.c_str());
pServer->DefaultIonAccessToken =
UTF8_TO_TCHAR(response.value->token.c_str());
pServer->Modify();
// Refresh all tilesets and overlays that are using the project
// default token.
UWorld* pWorld = GEditor->GetEditorWorldContext().World();
for (auto it = TActorIterator<ACesium3DTileset>(pWorld); it; ++it) {
if (it->GetTilesetSource() == ETilesetSource::FromCesiumIon &&
it->GetIonAccessToken().IsEmpty() &&
it->GetCesiumIonServer() == pServer) {
it->RefreshTileset();
} else {
// Tileset itself does not need to be refreshed, but maybe some
// overlays do.
TArray<UCesiumIonRasterOverlay*> rasterOverlays;
it->GetComponents<UCesiumIonRasterOverlay>(rasterOverlays);
for (UCesiumIonRasterOverlay* pOverlay : rasterOverlays) {
if (pOverlay->IonAccessToken.IsEmpty() &&
pOverlay->CesiumIonServer == pServer) {
pOverlay->Refresh();
}
}
}
}
} else {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("An error occurred while selecting a token: %s"),
UTF8_TO_TCHAR(response.errorMessage.c_str()));
}
promise.resolve(std::move(response.value));
pPanel->RequestDestroyWindow();
});
while (!this->_future->isReady()) {
getAssetAccessor()->tick();
getAsyncSystem().dispatchMainThreadTasks();
}
return FReply::Handled();
}
void SelectCesiumIonToken::RefreshTokens() {
const std::vector<Token>& tokens = FCesiumEditorModule::serverManager()
.GetSession(this->_pServer.Get())
->getTokens();
this->_tokens.SetNum(tokens.size());
std::string createName = TCHAR_TO_UTF8(*this->_createNewToken.name);
std::string specifiedToken = TCHAR_TO_UTF8(*this->_specifyToken.token);
for (size_t i = 0; i < tokens.size(); ++i) {
if (this->_tokens[i]) {
*this->_tokens[i] = std::move(tokens[i]);
} else {
this->_tokens[i] = MakeShared<Token>(std::move(tokens[i]));
}
if (this->_tokens[i]->id == this->_useExistingToken.token.id) {
this->_pTokensCombo->SetSelectedItem(this->_tokens[i]);
this->_tokenSource = TokenSource::UseExisting;
}
// If there's already a token with the default name we would use to create a
// new one, default to selecting that rather than creating a new one.
if (this->_tokenSource == TokenSource::Create &&
this->_tokens[i]->name == createName) {
this->_pTokensCombo->SetSelectedItem(this->_tokens[i]);
this->_tokenSource = TokenSource::UseExisting;
}
// If this happens to be the specified token, select it.
if (this->_tokenSource == TokenSource::Specify &&
this->_tokens[i]->token == specifiedToken) {
this->_pTokensCombo->SetSelectedItem(this->_tokens[i]);
this->_tokenSource = TokenSource::UseExisting;
}
}
this->_pTokensCombo->RefreshOptions();
}
TSharedRef<SWidget> SelectCesiumIonToken::OnGenerateTokenComboBoxEntry(
TSharedPtr<CesiumIonClient::Token> pToken) {
return SNew(STextBlock)
.Text(FText::FromString(UTF8_TO_TCHAR(pToken->name.c_str())));
}
FText SelectCesiumIonToken::GetNewTokenName() const {
return FText::FromString(this->_createNewToken.name);
}
void SelectCesiumIonToken::SetNewTokenName(const FText& text) {
this->_createNewToken.name = text.ToString();
}
void SelectCesiumIonToken::OnSelectExistingToken(
TSharedPtr<CesiumIonClient::Token> pToken,
ESelectInfo::Type type) {
if (pToken) {
this->_useExistingToken.token = *pToken;
}
}
FText SelectCesiumIonToken::GetSpecifiedToken() const {
return FText::FromString(this->_specifyToken.token);
}
void SelectCesiumIonToken::SetSpecifiedToken(const FText& text) {
this->_specifyToken.token = text.ToString();
}

View File

@ -0,0 +1,115 @@
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#pragma once
#include "CesiumAsync/AsyncSystem.h"
#include "CesiumIonClient/Token.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/SWindow.h"
#include <optional>
#include <string>
#include <variant>
class UCesiumIonServer;
class CesiumIonSession;
class SelectCesiumIonToken : public SWindow {
SLATE_BEGIN_ARGS(SelectCesiumIonToken) {}
SLATE_ARGUMENT(UCesiumIonServer*, Server)
SLATE_END_ARGS()
public:
/**
* Opens a panel to allow the user to select a new token.
*
* @return A future that resolves when the panel is closed. It resolves to the
* selected token if there was one, or to std::nullopt if the panel was closed
* without selecting a token.
*/
static CesiumAsync::SharedFuture<std::optional<CesiumIonClient::Token>>
SelectNewToken(UCesiumIonServer* pServer);
/**
* Opens a panel to allow the user to select a new token if a project default
* token is not already set. If the project default token _is_ set, the future
* immediately resolves to the previously-set token.
*
* @return A future that resolves when the panel is closed or when it does not
* need to be opened in the first place. It resolves to the selected token if
* there was one, or to std::nullopt if the panel was closed without selecting
* a token.
*/
static CesiumAsync::Future<std::optional<CesiumIonClient::Token>>
SelectTokenIfNecessary(UCesiumIonServer* pServer);
/**
* Authorizes the project default token to access a list of asset IDs. If the
* project default token is not set, a panel is opened to allow the token to
* be selected. Then, if possible, the token is modified to allow access to
* the list of asset IDs.
*
* @param assetIDs The asset IDs to be accessed.
* @return A future that resolves when the panel is closed or when it does not
* need to be opened in the first place. It resolves to the selected token if
* there was one, or to std::nullopt if the panel was closed without selecting
* a token.
*/
static CesiumAsync::Future<std::optional<CesiumIonClient::Token>>
SelectAndAuthorizeToken(
UCesiumIonServer* pServer,
const std::vector<int64_t>& assetIDs);
void Construct(const FArguments& InArgs);
private:
enum class TokenSource { Create, UseExisting, Specify };
struct CreateNewToken {
FString name;
};
struct UseExistingToken {
CesiumIonClient::Token token;
};
struct SpecifyToken {
FString token;
};
void createRadioButton(
const std::shared_ptr<CesiumIonSession>& pSession,
const TSharedRef<SVerticalBox>& pVertical,
TokenSource& tokenSource,
TokenSource thisValue,
const FString& label,
bool requiresIonConnection,
const TSharedRef<SWidget>& pWidget);
FReply UseOrCreate(std::shared_ptr<CesiumIonSession> pSession);
void RefreshTokens();
TSharedRef<SWidget>
OnGenerateTokenComboBoxEntry(TSharedPtr<CesiumIonClient::Token> pToken);
FText GetNewTokenName() const;
void SetNewTokenName(const FText& text);
void OnSelectExistingToken(
TSharedPtr<CesiumIonClient::Token> pToken,
ESelectInfo::Type type);
FText GetSpecifiedToken() const;
void SetSpecifiedToken(const FText& text);
static TSharedPtr<SelectCesiumIonToken> _pExistingPanel;
std::optional<CesiumAsync::Promise<std::optional<CesiumIonClient::Token>>>
_promise;
std::optional<
CesiumAsync::SharedFuture<std::optional<CesiumIonClient::Token>>>
_future;
TokenSource _tokenSource = TokenSource::Create;
CreateNewToken _createNewToken;
UseExistingToken _useExistingToken;
SpecifyToken _specifyToken;
FDelegateHandle _tokensUpdatedDelegateHandle;
TArray<TSharedPtr<CesiumIonClient::Token>> _tokens;
TSharedPtr<SComboBox<TSharedPtr<CesiumIonClient::Token>>> _pTokensCombo;
TWeakObjectPtr<UCesiumIonServer> _pServer;
};