// Copyright 2020-2025 CesiumGS, Inc. and Contributors #include "CesiumFeaturesMetadataViewer.h" #include "Cesium3DTileset.h" #include "CesiumCommon.h" #include "CesiumEditor.h" #include "CesiumFeaturesMetadataComponent.h" #include "CesiumMetadataEncodingDetails.h" #include "CesiumModelMetadata.h" #include "CesiumPrimitiveFeatures.h" #include "CesiumPrimitiveMetadata.h" #include "CesiumRuntimeSettings.h" #include "EditorStyleSet.h" #include "LevelEditor.h" #include "PropertyCustomizationHelpers.h" #include "ScopedTransaction.h" #include "Widgets/Images/SImage.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SExpandableArea.h" #include "Widgets/Layout/SHeader.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" THIRD_PARTY_INCLUDES_START #include #include #include #include THIRD_PARTY_INCLUDES_END /*static*/ TSharedPtr CesiumFeaturesMetadataViewer::_pExistingWindow = nullptr; /*static*/ TArray> CesiumFeaturesMetadataViewer::_conversionOptions = {}; /*static*/ TArray> CesiumFeaturesMetadataViewer::_encodedTypeOptions = {}; /*static*/ TArray> CesiumFeaturesMetadataViewer::_encodedComponentTypeOptions = {}; /*static*/ TMap> CesiumFeaturesMetadataViewer::_stringMap = {}; /*static*/ void CesiumFeaturesMetadataViewer::Open(TWeakObjectPtr pTileset) { if (_pExistingWindow.IsValid()) { _pExistingWindow->_pTileset = pTileset; } else { // Open a new panel TSharedRef viewer = SNew(CesiumFeaturesMetadataViewer).Tileset(pTileset); _pExistingWindow = viewer; _pExistingWindow->GetOnWindowClosedEvent().AddLambda( [&pExistingWindow = CesiumFeaturesMetadataViewer::_pExistingWindow]( const TSharedRef& pWindow) { pExistingWindow = nullptr; }); FSlateApplication::Get().AddWindow(viewer); } if (pTileset.IsValid()) { _pExistingWindow->_pFeaturesMetadataComponent = pTileset->GetComponentByClass(); } _pExistingWindow->SyncAndRebuildUI(); _pExistingWindow->BringToFront(); } void CesiumFeaturesMetadataViewer::Construct(const FArguments& InArgs) { CesiumFeaturesMetadataViewer::initializeStaticVariables(); SAssignNew(this->_pContent, SVerticalBox); const TWeakObjectPtr& pTileset = InArgs._Tileset; FString label = pTileset.IsValid() ? pTileset->GetActorLabel() : TEXT("Unknown"); this->_pTileset = pTileset; SWindow::Construct( SWindow::FArguments() .Title(FText::FromString(FString::Format( TEXT("{0}: Features and Metadata Properties"), {label}))) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D( 800, 600))[SNew(SBorder) .Visibility(EVisibility::Visible) .BorderImage(FAppStyle::GetBrush("Menu.Background")) .Padding(FMargin(10.0f))[this->_pContent->AsShared()]]); } namespace { template void populateEnumOptions(TArray>& options) { UEnum* pEnum = StaticEnum(); if (pEnum) { // "NumEnums" also includes the "_MAX" value, which indicates the number of // different values in the enum. Exclude it here. const int32 num = pEnum->NumEnums() - 1; options.Reserve(num); for (int32 i = 0; i < num; i++) { TEnum value = TEnum(pEnum->GetValueByIndex(i)); options.Emplace(MakeShared(value)); } } } } // namespace void CesiumFeaturesMetadataViewer::SyncAndRebuildUI() { this->_metadataSources.Empty(); this->_featureIdSets.Empty(); this->_stringMap.Empty(); this->_propertyTextureNames.Empty(); this->gatherGltfFeaturesMetadata(); TSharedRef pContent = this->_pContent.ToSharedRef(); pContent->ClearChildren(); pContent->AddSlot().AutoHeight() [SNew(SHeader) .Content()[SNew(STextBlock) .TextStyle(FCesiumEditorModule::GetStyle(), "Heading") .Text(FText::FromString(TEXT("glTF Features"))) .Margin(FMargin(0.f, 10.f))]]; if (!this->_featureIdSets.IsEmpty()) { TSharedRef pGltfFeatures = SNew(SScrollBox); for (const FeatureIdSetView& featureIdSet : this->_featureIdSets) { this->createGltfFeatureIdSetDropdown(pGltfFeatures, featureIdSet); } pContent->AddSlot().MaxHeight(400.0f).AutoHeight()[pGltfFeatures]; } else { pContent->AddSlot().AutoHeight() [SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.05f) + SHorizontalBox::Slot() [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT( "This tileset does not contain any glTF features in this view.")))]]; } pContent->AddSlot().AutoHeight() [SNew(SHeader) .Content()[SNew(STextBlock) .TextStyle(FCesiumEditorModule::GetStyle(), "Heading") .Text(FText::FromString(TEXT("glTF Metadata"))) .Margin(FMargin(0.f, 10.f))]]; if (!this->_metadataSources.IsEmpty()) { TSharedRef pGltfContent = SNew(SScrollBox); for (const PropertySourceView& source : this->_metadataSources) { this->createGltfPropertySourceDropdown(pGltfContent, source); } pContent->AddSlot().MaxHeight(400.0f).AutoHeight()[pGltfContent]; } else { pContent->AddSlot().AutoHeight() [SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.05f) + SHorizontalBox::Slot() [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT( "This tileset does not contain any glTF metadata in this view.")))]]; } this->syncPropertyEncodingDetails(); pContent->AddSlot() .AutoHeight() .Padding(0.0f, 10.0f) .VAlign(VAlign_Bottom) .HAlign(HAlign_Center) [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 with Current View"))) .ToolTipText(FText::FromString(TEXT( "Refreshes the lists with the feature ID sets and metadata from currently loaded tiles in the ACesium3DTileset."))) .OnClicked_Lambda([this]() { this->SyncAndRebuildUI(); return FReply::Handled(); })]; } namespace { // These are copies of functions in EncodedFeaturesMetadata.h. The file is // unfortunately too entangled in Private code to pull into Public. FString getNameForPropertySource(const FCesiumPropertyTable& propertyTable) { FString propertyTableName = UCesiumPropertyTableBlueprintLibrary::GetPropertyTableName(propertyTable); if (propertyTableName.IsEmpty()) { // Substitute the name with the property table's class. propertyTableName = propertyTable.getClassName(); } return propertyTableName; } FString getNameForPropertySource(const FCesiumPropertyTexture& propertyTexture) { FString propertyTextureName = UCesiumPropertyTextureBlueprintLibrary::GetPropertyTextureName( propertyTexture); if (propertyTextureName.IsEmpty()) { // Substitute the name with the property texture's class. propertyTextureName = propertyTexture.getClassName(); } return propertyTextureName; } FString getNameForFeatureIdSet( const FCesiumFeatureIdSet& featureIDSet, int32& featureIdTextureCounter) { FString label = UCesiumFeatureIdSetBlueprintLibrary::GetLabel(featureIDSet); if (!label.IsEmpty()) { return label; } ECesiumFeatureIdSetType type = UCesiumFeatureIdSetBlueprintLibrary::GetFeatureIDSetType(featureIDSet); if (type == ECesiumFeatureIdSetType::Attribute) { FCesiumFeatureIdAttribute attribute = UCesiumFeatureIdSetBlueprintLibrary::GetAsFeatureIDAttribute( featureIDSet); ECesiumFeatureIdAttributeStatus status = UCesiumFeatureIdAttributeBlueprintLibrary::GetFeatureIDAttributeStatus( attribute); if (status == ECesiumFeatureIdAttributeStatus::Valid) { std::string generatedName = "_FEATURE_ID_" + std::to_string(attribute.getAttributeIndex()); return FString(generatedName.c_str()); } } if (type == ECesiumFeatureIdSetType::Instance) { FCesiumFeatureIdAttribute attribute = UCesiumFeatureIdSetBlueprintLibrary::GetAsFeatureIDAttribute( featureIDSet); ECesiumFeatureIdAttributeStatus status = UCesiumFeatureIdAttributeBlueprintLibrary::GetFeatureIDAttributeStatus( attribute); if (status == ECesiumFeatureIdAttributeStatus::Valid) { std::string generatedName = "_FEATURE_INSTANCE_ID_" + std::to_string(attribute.getAttributeIndex()); return FString(generatedName.c_str()); } } if (type == ECesiumFeatureIdSetType::Texture) { std::string generatedName = "_FEATURE_ID_TEXTURE_" + std::to_string(featureIdTextureCounter); featureIdTextureCounter++; return FString(generatedName.c_str()); } if (type == ECesiumFeatureIdSetType::Implicit) { return FString("_IMPLICIT_FEATURE_ID"); } if (type == ECesiumFeatureIdSetType::InstanceImplicit) { return FString("_IMPLICIT_FEATURE_INSTANCE_ID"); } // If for some reason an empty / invalid feature ID set was constructed, // return an empty name. return FString(); } } // namespace void CesiumFeaturesMetadataViewer::gatherGltfFeaturesMetadata() { if (!this->_pTileset.IsValid()) { return; } ACesium3DTileset& tileset = *this->_pTileset; for (const UActorComponent* pComponent : tileset.GetComponents()) { const auto* pPrimitive = Cast(pComponent); if (!pPrimitive) { continue; } const FCesiumModelMetadata& modelMetadata = UCesiumModelMetadataBlueprintLibrary::GetModelMetadata(pPrimitive); const TArray& propertyTables = UCesiumModelMetadataBlueprintLibrary::GetPropertyTables(modelMetadata); this->gatherGltfPropertySources< FCesiumPropertyTable, UCesiumPropertyTableBlueprintLibrary, FCesiumPropertyTableProperty, UCesiumPropertyTablePropertyBlueprintLibrary>(propertyTables); const TArray& propertyTextures = UCesiumModelMetadataBlueprintLibrary::GetPropertyTextures( modelMetadata); this->gatherGltfPropertySources< FCesiumPropertyTexture, UCesiumPropertyTextureBlueprintLibrary, FCesiumPropertyTextureProperty, UCesiumPropertyTexturePropertyBlueprintLibrary>(propertyTextures); const FCesiumPrimitiveMetadata& primitiveMetadata = UCesiumPrimitiveMetadataBlueprintLibrary::GetPrimitiveMetadata( pPrimitive); const TArray propertyTextureIndices = UCesiumPrimitiveMetadataBlueprintLibrary::GetPropertyTextureIndices( primitiveMetadata); for (int64 propertyTextureIndex : propertyTextureIndices) { if (!propertyTextures.IsValidIndex(propertyTextureIndex)) { continue; } const FCesiumPropertyTexture& propertyTexture = propertyTextures[propertyTextureIndex]; FString propertyTextureName = getNameForPropertySource(propertyTexture); this->_propertyTextureNames.Emplace(propertyTextureName); } const FCesiumPrimitiveFeatures& primitiveFeatures = UCesiumPrimitiveFeaturesBlueprintLibrary::GetPrimitiveFeatures( pPrimitive); const TArray& featureIdSets = UCesiumPrimitiveFeaturesBlueprintLibrary::GetFeatureIDSets( primitiveFeatures); int32 featureIdTextureCounter = 0; for (const FCesiumFeatureIdSet& featureIdSet : featureIdSets) { ECesiumFeatureIdSetType type = UCesiumFeatureIdSetBlueprintLibrary::GetFeatureIDSetType( featureIdSet); if (type == ECesiumFeatureIdSetType::None) { // Invalid feature ID set. Skip. continue; } FString name = getNameForFeatureIdSet(featureIdSet, featureIdTextureCounter); FeatureIdSetView* pFeatureIdSetView = this->_featureIdSets.FindByPredicate( [&name](const FeatureIdSetView& existing) { return *existing.pName == name; }); if (!pFeatureIdSetView) { int32 index = this->_featureIdSets.Emplace( FeatureIdSetView{.pName = getSharedRef(name)}); pFeatureIdSetView = &this->_featureIdSets[index]; } const int64 propertyTableIndex = UCesiumFeatureIdSetBlueprintLibrary::GetPropertyTableIndex( featureIdSet); FString propertyTableName; if (propertyTables.IsValidIndex(propertyTableIndex)) { propertyTableName = getNameForPropertySource(propertyTables[propertyTableIndex]); } FeatureIdSetInstance Instance{ .pFeatureIdSetName = pFeatureIdSetView->pName, .type = type, .pPropertyTableName = getSharedRef(propertyTableName)}; TSharedRef* pExistingInstance = pFeatureIdSetView->instances.FindByPredicate( [&Instance](const TSharedRef& pExisting) { return Instance == *pExisting; }); if (!pExistingInstance) { pFeatureIdSetView->instances.Emplace( MakeShared(std::move(Instance))); } } } } void CesiumFeaturesMetadataViewer::syncPropertyEncodingDetails() { if (!this->_pFeaturesMetadataComponent.IsValid()) { return; } const TArray& propertyTables = this->_pFeaturesMetadataComponent->Description.ModelMetadata .PropertyTables; for (const FCesiumPropertyTableDescription& propertyTable : propertyTables) { const PropertySourceView* pSourceView = this->_metadataSources.FindByPredicate( [&name = propertyTable.Name](const PropertySourceView& source) { return *source.pName == name; }); if (!pSourceView) { continue; } for (const FCesiumPropertyTablePropertyDescription& property : propertyTable.Properties) { const TSharedRef* ppPropertyView = pSourceView->properties.FindByPredicate( [&name = property.Name]( const TSharedRef& pPropertyView) { return *pPropertyView->pId == name; }); if (!ppPropertyView) { continue; } const PropertyView& propertyView = **ppPropertyView; for (const TSharedRef& pInstance : propertyView.instances) { if (!pInstance->encodingDetails) { // This is a property texture property; continue. continue; } auto& conversionMethods = pInstance->encodingDetails->conversionMethods; int32 conversionIndex = INDEX_NONE; for (int32 i = 0; i < conversionMethods.Num(); i++) { if (*pInstance->encodingDetails->conversionMethods[i] == property.EncodingDetails.Conversion) { conversionIndex = i; break; } } if (conversionIndex == INDEX_NONE) { // This conversion method isn't supported for the property; continue. continue; } int64 typeIndex = int64(property.EncodingDetails.Type); int64 componentTypeIndex = int64(property.EncodingDetails.ComponentType); // Here the combo boxes will still be nullptr because // OnGenerateRow is not executed right away, so save the selected // options. pInstance->encodingDetails->pConversionSelection = conversionMethods[conversionIndex].ToSharedPtr(); pInstance->encodingDetails->pEncodedTypeSelection = this->_encodedTypeOptions[typeIndex].ToSharedPtr(); pInstance->encodingDetails->pEncodedComponentTypeSelection = this->_encodedComponentTypeOptions[componentTypeIndex] .ToSharedPtr(); } } } } bool CesiumFeaturesMetadataViewer::PropertyInstance::operator==( const PropertyInstance& rhs) const { if (*pPropertyId != *rhs.pPropertyId || propertyDetails != rhs.propertyDetails) { return false; } if (encodingDetails.has_value() != rhs.encodingDetails.has_value()) { return false; } else if (encodingDetails) { return encodingDetails->conversionMethods == rhs.encodingDetails->conversionMethods; } return true; } bool CesiumFeaturesMetadataViewer::PropertyInstance::operator!=( const PropertyInstance& property) const { return !operator==(property); } bool CesiumFeaturesMetadataViewer::FeatureIdSetInstance::operator==( const FeatureIdSetInstance& rhs) const { return *pFeatureIdSetName == *rhs.pFeatureIdSetName && type == rhs.type && *pPropertyTableName == *rhs.pPropertyTableName; } bool CesiumFeaturesMetadataViewer::FeatureIdSetInstance::operator!=( const FeatureIdSetInstance& property) const { return !operator==(property); } namespace { template TArray> getSharedRefs( const TArray>& options, const TArray& selection) { TArray> result; UEnum* pEnum = StaticEnum(); if (!pEnum) { return result; } // Assumes populateEnumOptions will be initialized in enum order! for (TEnum value : selection) { int32 index = pEnum->GetIndexByValue(int64(value)); CESIUM_ASSERT(index >= 0 && index < options.Num()); result.Add(options[index]); } return result; } TArray getSupportedConversionsForProperty( const FCesiumMetadataPropertyDetails& PropertyDetails) { TArray result; if (PropertyDetails.Type == ECesiumMetadataType::Invalid) { return result; } result.Reserve(2); result.Add(ECesiumEncodedMetadataConversion::Coerce); if (PropertyDetails.Type == ECesiumMetadataType::String) { result.Add(ECesiumEncodedMetadataConversion::ParseColorFromString); } return result; } } // namespace template < typename TSource, typename TSourceBlueprintLibrary, typename TSourceProperty, typename TSourcePropertyBlueprintLibrary> void CesiumFeaturesMetadataViewer::gatherGltfPropertySources( const TArray& sources) { for (const TSource& source : sources) { FString sourceName = getNameForPropertySource(source); TSharedRef pSourceName = this->getSharedRef(sourceName); constexpr EPropertySource sourceType = std::is_same_v ? EPropertySource::PropertyTable : EPropertySource::PropertyTexture; PropertySourceView* pSource = this->_metadataSources.FindByPredicate( [&sourceName, sourceType](const PropertySourceView& existingSource) { return *existingSource.pName == sourceName && existingSource.type == sourceType; }); if (!pSource) { int32 index = this->_metadataSources.Emplace( PropertySourceView{.pName = pSourceName, .type = sourceType}); pSource = &this->_metadataSources[index]; } const TMap& properties = TSourceBlueprintLibrary::GetProperties(source); for (const auto& propertyIt : properties) { TSharedRef pPropertyId = this->getSharedRef(propertyIt.Key); TSharedRef* pProperty = pSource->properties.FindByPredicate( [&pPropertyId](const TSharedRef& pExistingProperty) { return **pExistingProperty->pId == *pPropertyId; }); if (!pProperty) { PropertyView newProperty{.pId = pPropertyId, .instances = {}}; int32 index = pSource->properties.Emplace( MakeShared(std::move(newProperty))); pProperty = &pSource->properties[index]; } PropertyView& property = **pProperty; FCesiumMetadataPropertyDetails propertyDetails; const FCesiumMetadataValueType valueType = TSourcePropertyBlueprintLibrary::GetValueType(propertyIt.Value); // Skip any invalid type properties. if (valueType.Type == ECesiumMetadataType::Invalid) continue; propertyDetails.Type = valueType.Type; propertyDetails.ComponentType = valueType.ComponentType; propertyDetails.bIsArray = valueType.bIsArray; propertyDetails.ArraySize = TSourcePropertyBlueprintLibrary::GetArraySize(propertyIt.Value); propertyDetails.bIsNormalized = TSourcePropertyBlueprintLibrary::IsNormalized(propertyIt.Value); FCesiumMetadataValue offset = TSourcePropertyBlueprintLibrary::GetOffset(propertyIt.Value); propertyDetails.bHasOffset = !UCesiumMetadataValueBlueprintLibrary::IsEmpty(offset); FCesiumMetadataValue scale = TSourcePropertyBlueprintLibrary::GetScale(propertyIt.Value); propertyDetails.bHasScale = !UCesiumMetadataValueBlueprintLibrary::IsEmpty(scale); FCesiumMetadataValue noData = TSourcePropertyBlueprintLibrary::GetNoDataValue(propertyIt.Value); propertyDetails.bHasNoDataValue = !UCesiumMetadataValueBlueprintLibrary::IsEmpty(scale); FCesiumMetadataValue defaultValue = TSourcePropertyBlueprintLibrary::GetDefaultValue(propertyIt.Value); propertyDetails.bHasDefaultValue = !UCesiumMetadataValueBlueprintLibrary::IsEmpty(scale); PropertyInstance instance{ .pPropertyId = pPropertyId, .propertyDetails = std::move(propertyDetails), .pSourceName = pSourceName}; if constexpr (std::is_same_v< TSourceProperty, FCesiumPropertyTableProperty>) { // Do some silly TSharedRef lookup since it's required by SComboBox. TArray> supportedConversions = getSharedRefs( this->_conversionOptions, getSupportedConversionsForProperty(propertyDetails)); instance.encodingDetails = PropertyInstanceEncodingDetails{ .conversionMethods = std::move(supportedConversions)}; } TSharedRef* pExistingInstance = property.instances.FindByPredicate( [&instance]( const TSharedRef& existingInstance) { return instance == *existingInstance; }); if (!pExistingInstance) { property.instances.Emplace( MakeShared(std::move(instance))); } } } } namespace { template FString enumToNameString(TEnum value) { const UEnum* pEnum = StaticEnum(); return pEnum ? pEnum->GetNameStringByValue((int64)value) : FString(); } } // namespace namespace { template FText getEnumDisplayNameText(TEnum value) { UEnum* pEnum = StaticEnum(); if (pEnum) { return pEnum->GetDisplayNameTextByValue(int64(value)); } return FText::FromString(FString()); } FCesiumMetadataEncodingDetails getSelectedEncodingDetails( const TSharedPtr>>& pConversionCombo, const TSharedPtr>>& pEncodedTypeCombo, const TSharedPtr< SComboBox>>& pEncodedComponentTypeCombo) { if (!pConversionCombo || !pEncodedTypeCombo || !pEncodedComponentTypeCombo) return FCesiumMetadataEncodingDetails(); TSharedPtr pConversion = pConversionCombo->GetSelectedItem(); TSharedPtr pEncodedType = pEncodedTypeCombo->GetSelectedItem(); TSharedPtr pEncodedComponentType = pEncodedComponentTypeCombo->GetSelectedItem(); return FCesiumMetadataEncodingDetails( pEncodedType.IsValid() ? *pEncodedType : ECesiumEncodedMetadataType::None, pEncodedComponentType.IsValid() ? *pEncodedComponentType : ECesiumEncodedMetadataComponentType::None, pConversion.IsValid() ? *pConversion : ECesiumEncodedMetadataConversion::None); } bool validateEncodingDetails(const FCesiumMetadataEncodingDetails& details) { switch (details.Conversion) { case ECesiumEncodedMetadataConversion::Coerce: case ECesiumEncodedMetadataConversion::ParseColorFromString: return details.HasValidType(); case ECesiumEncodedMetadataConversion::None: default: return false; } } } // namespace TSharedRef CesiumFeaturesMetadataViewer::createPropertyInstanceRow( TSharedRef pItem, const TSharedRef& list) { FString typeString = pItem->propertyDetails.GetValueType().ToString(); if (pItem->propertyDetails.bIsNormalized) { typeString += TEXT(" (Normalized)"); } if (pItem->propertyDetails.bIsArray) { int64 arraySize = pItem->propertyDetails.ArraySize; typeString += arraySize > 0 ? FString::Printf(TEXT(" with %d elements"), arraySize) : TEXT(" of variable size"); } TArray qualifierList; if (pItem->propertyDetails.bHasOffset) { qualifierList.Add("Offset"); } if (pItem->propertyDetails.bHasScale) { qualifierList.Add("Scale"); } if (pItem->propertyDetails.bHasNoDataValue) { qualifierList.Add("'No Data' Value"); } if (pItem->propertyDetails.bHasDefaultValue) { qualifierList.Add("Default Value"); } FString qualifierString = qualifierList.IsEmpty() ? FString() : "Contains " + FString::Join(qualifierList, TEXT(", ")); TSharedRef content = SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.45f).Padding(5.0f).VAlign( EVerticalAlignment::VAlign_Center) [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(typeString)) .ToolTipText(FText::FromString(FString( "The type of the property as defined in the EXT_structural_metadata extension.")))] + SHorizontalBox::Slot() .AutoWidth() .MaxWidth(200.0f) .Padding(5.0f) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(qualifierString)) .ToolTipText(FText::FromString( "Notable qualities of the property that require additional nodes to be generated for the material."))]; if (pItem->encodingDetails) { FCesiumMetadataEncodingDetails bestFitEncodingDetails = FCesiumMetadataEncodingDetails::GetBestFitForProperty( pItem->propertyDetails); createEnumComboBox( pItem->encodingDetails->pConversionCombo, pItem->encodingDetails->conversionMethods, pItem->encodingDetails->pConversionSelection ? *pItem->encodingDetails->pConversionSelection : bestFitEncodingDetails.Conversion, FString()); createEnumComboBox( pItem->encodingDetails->pEncodedTypeCombo, this->_encodedTypeOptions, pItem->encodingDetails->pEncodedTypeSelection ? *pItem->encodingDetails->pEncodedTypeSelection : bestFitEncodingDetails.Type, TEXT( "The type to which to coerce the property's data. Affects the texture format that is used to encode the data.")); createEnumComboBox( pItem->encodingDetails->pEncodedComponentTypeCombo, this->_encodedComponentTypeOptions, pItem->encodingDetails->pEncodedComponentTypeSelection ? *pItem->encodingDetails->pEncodedComponentTypeSelection : bestFitEncodingDetails.ComponentType, TEXT( "The component type to which to coerce the property's data. Affects the texture format that is used to encode the data.")); if (pItem->encodingDetails->pConversionCombo.IsValid()) { content->AddSlot().FillWidth(0.6).Padding(5.0f).VAlign( EVerticalAlignment::VAlign_Center) [pItem->encodingDetails->pConversionCombo->AsShared()]; } auto visibilityLambda = TAttribute::Create([pItem]() { if (!pItem->encodingDetails) { return EVisibility::Hidden; } bool show = false; if (pItem->encodingDetails->pConversionCombo.IsValid()) { show = pItem->encodingDetails->pConversionCombo->GetSelectedItem() .IsValid(); } return show ? EVisibility::Visible : EVisibility::Hidden; }); if (pItem->encodingDetails->pEncodedTypeCombo.IsValid()) { pItem->encodingDetails->pEncodedTypeCombo->SetVisibility( visibilityLambda); content->AddSlot().AutoWidth().Padding(5.0f).VAlign( EVerticalAlignment::VAlign_Center) [pItem->encodingDetails->pEncodedTypeCombo->AsShared()]; } if (pItem->encodingDetails->pEncodedComponentTypeCombo.IsValid()) { pItem->encodingDetails->pEncodedComponentTypeCombo->SetVisibility( visibilityLambda); content->AddSlot().AutoWidth().Padding(5.0f).VAlign( EVerticalAlignment::VAlign_Center) [pItem->encodingDetails->pEncodedComponentTypeCombo->AsShared()]; } } TSharedRef pAddButton = PropertyCustomizationHelpers::MakeAddButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->registerPropertyInstance(pItem); }), FText::FromString(TEXT( "Add this property to the tileset's CesiumFeaturesMetadataComponent.")), TAttribute::Create([this, pItem]() { FCesiumMetadataEncodingDetails selectedEncodingDetails = getSelectedEncodingDetails( pItem->encodingDetails->pConversionCombo, pItem->encodingDetails->pEncodedTypeCombo, pItem->encodingDetails->pEncodedComponentTypeCombo); return this->findOnComponent(pItem, false) == ComponentSearchResult::NoMatch && validateEncodingDetails(selectedEncodingDetails); })); pAddButton->SetVisibility(TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem, false) == ComponentSearchResult::NoMatch ? EVisibility::Visible : EVisibility::Collapsed; })); TSharedRef pOverwriteButton = PropertyCustomizationHelpers::MakeEditButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->registerPropertyInstance(pItem); }), FText::FromString(TEXT( "Overwrites the existing property on the tileset's CesiumFeaturesMetadataComponent " "with the same name.")), TAttribute::Create([this, pItem]() { FCesiumMetadataEncodingDetails selectedEncodingDetails = getSelectedEncodingDetails( pItem->encodingDetails->pConversionCombo, pItem->encodingDetails->pEncodedTypeCombo, pItem->encodingDetails->pEncodedComponentTypeCombo); return this->findOnComponent(pItem, true) == ComponentSearchResult::PartialMatch && validateEncodingDetails(selectedEncodingDetails); })); pOverwriteButton->SetVisibility(TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem, true) != ComponentSearchResult::NoMatch ? EVisibility::Visible : EVisibility::Collapsed; })); TSharedRef pRemoveButton = PropertyCustomizationHelpers::MakeRemoveButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->removePropertyInstance(pItem); }), FText::FromString(TEXT( "Remove this property from the tileset's CesiumFeaturesMetadataComponent.")), TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem, false) == ComponentSearchResult::ExactMatch; })); content->AddSlot() .AutoWidth() .HAlign(EHorizontalAlignment::HAlign_Right) .VAlign(EVerticalAlignment::VAlign_Center)[pAddButton]; content->AddSlot() .AutoWidth() .HAlign(EHorizontalAlignment::HAlign_Right) .VAlign(EVerticalAlignment::VAlign_Center)[pOverwriteButton]; content->AddSlot() .AutoWidth() .HAlign(EHorizontalAlignment::HAlign_Right) .VAlign(EVerticalAlignment::VAlign_Center)[pRemoveButton]; return SNew(STableRow>, list) .Content()[SNew(SBox) .HAlign(EHorizontalAlignment::HAlign_Fill) .Content()[content]]; } TSharedRef CesiumFeaturesMetadataViewer::createGltfPropertyDropdown( TSharedRef pItem, const TSharedRef& list) { return SNew(STableRow>, list) .Content()[SNew(SExpandableArea) .InitiallyCollapsed(true) .HeaderContent()[SNew(STextBlock) .Text(FText::FromString(*pItem->pId))] .BodyContent()[SNew( SListView>) .ListItemsSource(&pItem->instances) .SelectionMode(ESelectionMode::None) .OnGenerateRow( this, &CesiumFeaturesMetadataViewer:: createPropertyInstanceRow)]]; } void CesiumFeaturesMetadataViewer::createGltfPropertySourceDropdown( TSharedRef& pContent, const PropertySourceView& source) { FString sourceDisplayName = FString::Printf(TEXT("\"%s\""), **source.pName); switch (source.type) { case EPropertySource::PropertyTable: sourceDisplayName += " (Property Table)"; break; case EPropertySource::PropertyTexture: sourceDisplayName += " (Property Texture)"; break; } pContent->AddSlot() [SNew(SExpandableArea) .InitiallyCollapsed(false) .HeaderContent()[SNew(STextBlock) .Text(FText::FromString(sourceDisplayName))] .BodyContent() [SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.05f) + SHorizontalBox::Slot() [SNew(SListView>) .ListItemsSource(&source.properties) .SelectionMode(ESelectionMode::None) .OnGenerateRow( this, &CesiumFeaturesMetadataViewer:: createGltfPropertyDropdown)]]]; } template TSharedRef CesiumFeaturesMetadataViewer::createEnumDropdownOption( TSharedRef pOption) { return SNew(STextBlock).Text(getEnumDisplayNameText(*pOption)); } template void CesiumFeaturesMetadataViewer::createEnumComboBox( TSharedPtr>>& pComboBox, const TArray>& options, TEnum initialValue, const FString& tooltip) { CESIUM_ASSERT(options.Num() > 0); int32 initialIndex = 0; for (int32 i = 0; i < options.Num(); i++) { if (initialValue == *options[i]) { initialIndex = i; break; } } SAssignNew(pComboBox, SComboBox>) .OptionsSource(&options) .InitiallySelectedItem(options[initialIndex]) .OnGenerateWidget( this, &CesiumFeaturesMetadataViewer::createEnumDropdownOption) .Content()[SNew(STextBlock) .MinDesiredWidth(50.0f) .Text_Lambda([&pComboBox]() { return pComboBox->GetSelectedItem().IsValid() ? getEnumDisplayNameText( *pComboBox->GetSelectedItem()) : FText::FromString(FString()); }) .ToolTipText_Lambda([&pComboBox, tooltip]() { if constexpr (std::is_same_v< TEnum, ECesiumEncodedMetadataConversion>) { UEnum* pEnum = StaticEnum(); if (pEnum) { return pComboBox->GetSelectedItem().IsValid() ? pEnum->GetToolTipTextByIndex(int64( *pComboBox->GetSelectedItem())) : FText::FromString(FString()); } } return FText::FromString(tooltip); })]; } TSharedRef CesiumFeaturesMetadataViewer::createFeatureIdSetInstanceRow( TSharedRef pItem, const TSharedRef& list) { TSharedRef pBox = SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.5f).Padding(5.0f).VAlign( EVerticalAlignment::VAlign_Center) [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(enumToNameString(pItem->type)))]; if (!pItem->pPropertyTableName->IsEmpty()) { FString sourceString = FString::Printf( TEXT("Used with \"%s\" (Property Table)"), **pItem->pPropertyTableName); pBox->AddSlot() .FillWidth(1.0f) .Padding(5.0f) .HAlign(HAlign_Fill) .VAlign(EVerticalAlignment::VAlign_Center) [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(sourceString)) .ToolTipText(FText::FromString( "The property table with which this feature ID set should be used. " "Add properties from the corresponding property table under \"glTF Metadata\"."))]; } TSharedRef pAddButton = PropertyCustomizationHelpers::MakeAddButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->registerFeatureIdSetInstance(pItem); }), FText::FromString(TEXT( "Add this feature ID set to the tileset's CesiumFeaturesMetadataComponent.")), TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem) == ComponentSearchResult::NoMatch; })); pAddButton->SetVisibility(TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem) == ComponentSearchResult::NoMatch ? EVisibility::Visible : EVisibility::Collapsed; })); TSharedRef pOverwriteButton = PropertyCustomizationHelpers::MakeEditButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->registerFeatureIdSetInstance(pItem); }), FText::FromString( TEXT("Overwrites the existing feature ID set on the tileset's " "CesiumFeaturesMetadataComponent with the same name.")), TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem) == ComponentSearchResult::PartialMatch; })); pOverwriteButton->SetVisibility( TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem) != ComponentSearchResult::NoMatch ? EVisibility::Visible : EVisibility::Collapsed; })); TSharedRef pRemoveButton = PropertyCustomizationHelpers::MakeRemoveButton( FSimpleDelegate::CreateLambda( [this, pItem]() { this->removeFeatureIdSetInstance(pItem); }), FText::FromString(TEXT( "Remove this feature ID set from the tileset's CesiumFeaturesMetadataComponent.")), TAttribute::Create([this, pItem]() { return this->findOnComponent(pItem) == ComponentSearchResult::ExactMatch; })); pBox->AddSlot() .AutoWidth() .HAlign(EHorizontalAlignment::HAlign_Right) .VAlign(EVerticalAlignment::VAlign_Center) [SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[pAddButton] + SHorizontalBox::Slot().AutoWidth()[pRemoveButton]]; return SNew(STableRow>, list) .Content()[SNew(SBox) .HAlign(EHorizontalAlignment::HAlign_Fill) .Content()[std::move(pBox)]]; } void CesiumFeaturesMetadataViewer::createGltfFeatureIdSetDropdown( TSharedRef& pContent, const FeatureIdSetView& featureIdSet) { pContent->AddSlot() [SNew(SExpandableArea) .InitiallyCollapsed(false) .HeaderContent()[SNew(STextBlock) .Text(FText::FromString(*featureIdSet.pName))] .BodyContent()[SNew(SListView>) .ListItemsSource(&featureIdSet.instances) .SelectionMode(ESelectionMode::None) .OnGenerateRow( this, &CesiumFeaturesMetadataViewer:: createFeatureIdSetInstanceRow)]]; } namespace { template TProperty* findProperty( TArray& sources, const FString& sourceName, const FString& propertyName, bool createIfMissing) { TPropertySource* pPropertySource = sources.FindByPredicate( [&sourceName](const TPropertySource& existingSource) { return sourceName == existingSource.Name; }); if (!pPropertySource && !createIfMissing) { return nullptr; } if (!pPropertySource) { int32 index = sources.Emplace(TPropertySource{sourceName, TArray()}); pPropertySource = &sources[index]; } TProperty* pProperty = pPropertySource->Properties.FindByPredicate( [&propertyName](const TProperty& existingProperty) { return propertyName == existingProperty.Name; }); if (!pProperty && createIfMissing) { int32 index = pPropertySource->Properties.Emplace(); pProperty = &pPropertySource->Properties[index]; pProperty->Name = propertyName; } return pProperty; } FCesiumFeatureIdSetDescription* findFeatureIdSet( TArray& featureIdSets, const FString& name, bool createIfMissing) { FCesiumFeatureIdSetDescription* pFeatureIdSet = featureIdSets.FindByPredicate( [&name](const FCesiumFeatureIdSetDescription& existingSet) { return name == existingSet.Name; }); if (!pFeatureIdSet && createIfMissing) { int32 index = featureIdSets.Emplace(); pFeatureIdSet = &featureIdSets[index]; pFeatureIdSet->Name = name; } return pFeatureIdSet; } } // namespace CesiumFeaturesMetadataViewer::ComponentSearchResult CesiumFeaturesMetadataViewer::findOnComponent( TSharedRef pItem, bool compareEncodingDetails) { if (!this->_pFeaturesMetadataComponent.IsValid()) { return ComponentSearchResult::NoMatch; } UCesiumFeaturesMetadataComponent& featuresMetadata = *this->_pFeaturesMetadataComponent; if (pItem->encodingDetails) { // Check whether the property already exists. FCesiumPropertyTablePropertyDescription* pProperty = findProperty< FCesiumPropertyTableDescription, FCesiumPropertyTablePropertyDescription>( featuresMetadata.Description.ModelMetadata.PropertyTables, *pItem->pSourceName, *pItem->pPropertyId, false); if (!pProperty) { return ComponentSearchResult::NoMatch; } if (pProperty->PropertyDetails != pItem->propertyDetails) { return ComponentSearchResult::PartialMatch; } if (compareEncodingDetails) { FCesiumMetadataEncodingDetails selectedEncodingDetails = getSelectedEncodingDetails( pItem->encodingDetails->pConversionCombo, pItem->encodingDetails->pEncodedTypeCombo, pItem->encodingDetails->pEncodedComponentTypeCombo); return pProperty->EncodingDetails == selectedEncodingDetails ? ComponentSearchResult::ExactMatch : ComponentSearchResult::PartialMatch; } else { return ComponentSearchResult::ExactMatch; } } else { FCesiumPropertyTexturePropertyDescription* pProperty = findProperty< FCesiumPropertyTextureDescription, FCesiumPropertyTexturePropertyDescription>( featuresMetadata.Description.ModelMetadata.PropertyTextures, *pItem->pSourceName, *pItem->pPropertyId, false); if (!pProperty) { return ComponentSearchResult::NoMatch; } return pProperty->PropertyDetails == pItem->propertyDetails ? ComponentSearchResult::ExactMatch : ComponentSearchResult::PartialMatch; } } CesiumFeaturesMetadataViewer::ComponentSearchResult CesiumFeaturesMetadataViewer::findOnComponent( TSharedRef pItem) { if (!this->_pFeaturesMetadataComponent.IsValid()) { return ComponentSearchResult::NoMatch; } UCesiumFeaturesMetadataComponent& featuresMetadata = *this->_pFeaturesMetadataComponent; FCesiumFeatureIdSetDescription* pFeatureIdSet = findFeatureIdSet( featuresMetadata.Description.PrimitiveFeatures.FeatureIdSets, *pItem->pFeatureIdSetName, false); if (!pFeatureIdSet) { return ComponentSearchResult::NoMatch; } return pFeatureIdSet->PropertyTableName == *pItem->pPropertyTableName ? ComponentSearchResult::ExactMatch : ComponentSearchResult::PartialMatch; } void CesiumFeaturesMetadataViewer::registerPropertyInstance( TSharedRef pItem) { if (!this->_pFeaturesMetadataComponent.IsValid()) { UE_LOG( LogCesiumEditor, Error, TEXT( "This window was opened for a now invalid CesiumFeaturesMetadataComponent.")) return; } UKismetSystemLibrary::BeginTransaction( TEXT("Cesium Features / Metadata Viewer"), FText::FromString( FString("Register property instance with ACesium3DTileset")), this->_pFeaturesMetadataComponent.Get()); this->_pFeaturesMetadataComponent->PreEditChange(NULL); FCesiumFeaturesMetadataDescription& description = this->_pFeaturesMetadataComponent->Description; if (pItem->encodingDetails) { CESIUM_ASSERT( pItem->encodingDetails->pConversionCombo && pItem->encodingDetails->pEncodedTypeCombo && pItem->encodingDetails->pEncodedComponentTypeCombo); FCesiumPropertyTablePropertyDescription* pProperty = findProperty< FCesiumPropertyTableDescription, FCesiumPropertyTablePropertyDescription>( description.ModelMetadata.PropertyTables, *pItem->pSourceName, *pItem->pPropertyId, true); CESIUM_ASSERT(pProperty != nullptr); FCesiumPropertyTablePropertyDescription& property = *pProperty; property.PropertyDetails = pItem->propertyDetails; property.EncodingDetails = getSelectedEncodingDetails( pItem->encodingDetails->pConversionCombo, pItem->encodingDetails->pEncodedTypeCombo, pItem->encodingDetails->pEncodedComponentTypeCombo); } else { FCesiumPropertyTexturePropertyDescription* pProperty = findProperty< FCesiumPropertyTextureDescription, FCesiumPropertyTexturePropertyDescription>( description.ModelMetadata.PropertyTextures, *pItem->pSourceName, *pItem->pPropertyId, true); CESIUM_ASSERT(pProperty != nullptr); FCesiumPropertyTexturePropertyDescription& property = *pProperty; property.PropertyDetails = pItem->propertyDetails; if (!this->_propertyTextureNames.Contains(*pItem->pSourceName)) { description.PrimitiveMetadata.PropertyTextureNames.Add( *pItem->pSourceName); } } this->_pFeaturesMetadataComponent->PostEditChange(); UKismetSystemLibrary::EndTransaction(); } void CesiumFeaturesMetadataViewer::registerFeatureIdSetInstance( TSharedRef pItem) { if (!this->_pFeaturesMetadataComponent.IsValid()) { UE_LOG( LogCesiumEditor, Error, TEXT( "This window was opened for a now invalid CesiumFeaturesMetadataComponent.")) return; } UKismetSystemLibrary::BeginTransaction( TEXT("Cesium Features / Metadata Viewer"), FText::FromString( FString("Register feature ID set instance with ACesium3DTileset")), this->_pFeaturesMetadataComponent.Get()); this->_pFeaturesMetadataComponent->PreEditChange(NULL); FCesiumFeaturesMetadataDescription& description = this->_pFeaturesMetadataComponent->Description; FCesiumFeatureIdSetDescription* pFeatureIdSet = findFeatureIdSet( description.PrimitiveFeatures.FeatureIdSets, *pItem->pFeatureIdSetName, true); CESIUM_ASSERT(pFeatureIdSet != nullptr); pFeatureIdSet->Type = pItem->type; pFeatureIdSet->PropertyTableName = *pItem->pPropertyTableName; this->_pFeaturesMetadataComponent->PostEditChange(); UKismetSystemLibrary::EndTransaction(); } void CesiumFeaturesMetadataViewer::removePropertyInstance( TSharedRef pItem) { if (!this->_pFeaturesMetadataComponent.IsValid()) { UE_LOG( LogCesiumEditor, Error, TEXT( "This window was opened for a now invalid CesiumFeaturesMetadataComponent.")) return; } FCesiumFeaturesMetadataDescription& description = this->_pFeaturesMetadataComponent->Description; if (pItem->encodingDetails) { TArray& propertyTables = description.ModelMetadata.PropertyTables; int32 tableIndex = INDEX_NONE; for (int32 i = 0; i < propertyTables.Num(); i++) { if (propertyTables[i].Name == *pItem->pSourceName) { tableIndex = i; break; } } if (tableIndex == INDEX_NONE) { return; } FCesiumPropertyTableDescription& propertyTable = propertyTables[tableIndex]; int32 propertyIndex = INDEX_NONE; for (int32 i = 0; i < propertyTable.Properties.Num(); i++) { if (propertyTable.Properties[i].Name == *pItem->pPropertyId) { propertyIndex = i; break; } } if (propertyIndex != INDEX_NONE) { UKismetSystemLibrary::BeginTransaction( TEXT("Cesium Features / Metadata Viewer"), FText::FromString( FString("Remove property instance from ACesium3DTileset")), this->_pFeaturesMetadataComponent.Get()); this->_pFeaturesMetadataComponent->PreEditChange(NULL); propertyTable.Properties.RemoveAt(propertyIndex); if (propertyTable.Properties.IsEmpty()) { propertyTables.RemoveAt(tableIndex); } this->_pFeaturesMetadataComponent->PostEditChange(); UKismetSystemLibrary::EndTransaction(); } } else { TArray& propertyTextures = description.ModelMetadata.PropertyTextures; int32 textureIndex = INDEX_NONE; for (int32 i = 0; i < propertyTextures.Num(); i++) { if (propertyTextures[i].Name == *pItem->pSourceName) { textureIndex = i; break; } } if (textureIndex == INDEX_NONE) { return; } FCesiumPropertyTextureDescription& propertyTexture = propertyTextures[textureIndex]; int32 propertyIndex = INDEX_NONE; for (int32 i = 0; i < propertyTexture.Properties.Num(); i++) { if (propertyTexture.Properties[i].Name == *pItem->pPropertyId) { propertyIndex = i; break; } } if (propertyIndex != INDEX_NONE) { UKismetSystemLibrary::BeginTransaction( TEXT("Cesium Features / Metadata Viewer"), FText::FromString( FString("Remove property instance from ACesium3DTileset")), this->_pFeaturesMetadataComponent.Get()); this->_pFeaturesMetadataComponent->PreEditChange(NULL); propertyTexture.Properties.RemoveAt(propertyIndex); if (propertyTexture.Properties.IsEmpty()) { propertyTextures.RemoveAt(textureIndex); if (this->_propertyTextureNames.Contains(*pItem->pSourceName)) { description.PrimitiveMetadata.PropertyTextureNames.Remove( *pItem->pSourceName); } } this->_pFeaturesMetadataComponent->PostEditChange(); UKismetSystemLibrary::EndTransaction(); } } } void CesiumFeaturesMetadataViewer::removeFeatureIdSetInstance( TSharedRef pItem) { if (!this->_pFeaturesMetadataComponent.IsValid()) { UE_LOG( LogCesiumEditor, Error, TEXT( "This window was opened for a now invalid CesiumFeaturesMetadataComponent.")) return; } TArray& featureIdSets = this->_pFeaturesMetadataComponent->Description.PrimitiveFeatures .FeatureIdSets; int32 featureIdSetIndex = INDEX_NONE; for (int32 i = 0; i < featureIdSets.Num(); i++) { if (featureIdSets[i].Name == *pItem->pFeatureIdSetName) { featureIdSetIndex = i; break; } } if (featureIdSetIndex != INDEX_NONE) { UKismetSystemLibrary::BeginTransaction( TEXT("Cesium Features / Metadata Viewer"), FText::FromString( FString("Remove feature ID set instance from ACesium3DTileset")), this->_pFeaturesMetadataComponent.Get()); this->_pFeaturesMetadataComponent->PreEditChange(NULL); featureIdSets.RemoveAt(featureIdSetIndex); this->_pFeaturesMetadataComponent->PostEditChange(); UKismetSystemLibrary::EndTransaction(); } } TSharedRef CesiumFeaturesMetadataViewer::getSharedRef(const FString& string) { return _stringMap.Contains(string) ? _stringMap[string] : _stringMap.Emplace(string, MakeShared(string)); } void CesiumFeaturesMetadataViewer::initializeStaticVariables() { if (_conversionOptions.IsEmpty()) { populateEnumOptions(_conversionOptions); } if (_encodedTypeOptions.IsEmpty()) { populateEnumOptions(_encodedTypeOptions); } if (_encodedComponentTypeOptions.IsEmpty()) { populateEnumOptions( _encodedComponentTypeOptions); } }