chore: checkpoint current workspace changes
This commit is contained in:
@@ -12,6 +12,12 @@
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
enum class ArtifactStorageKind : Core::uint8 {
|
||||
Unknown = 0,
|
||||
LegacyDirectory = 1,
|
||||
SingleFileContainer = 2
|
||||
};
|
||||
|
||||
class Mesh;
|
||||
class Material;
|
||||
|
||||
@@ -49,8 +55,10 @@ public:
|
||||
Containers::String importerName;
|
||||
Core::uint32 importerVersion = 0;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
ArtifactStorageKind storageKind = ArtifactStorageKind::Unknown;
|
||||
Containers::String artifactDirectory;
|
||||
Containers::String mainArtifactPath;
|
||||
Containers::String mainEntryName;
|
||||
Containers::String sourceHash;
|
||||
Containers::String metaHash;
|
||||
Core::uint64 sourceFileSize = 0;
|
||||
@@ -68,7 +76,10 @@ public:
|
||||
AssetGUID assetGuid;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
Containers::String artifactMainPath;
|
||||
Containers::String artifactMainEntryPath;
|
||||
Containers::String artifactDirectory;
|
||||
ArtifactStorageKind artifactStorageKind = ArtifactStorageKind::Unknown;
|
||||
Containers::String mainEntryName;
|
||||
LocalID mainLocalID = kMainAssetLocalID;
|
||||
};
|
||||
|
||||
@@ -100,7 +111,7 @@ public:
|
||||
const Containers::String& GetLastErrorMessage() const { return m_lastErrorMessage; }
|
||||
|
||||
private:
|
||||
static constexpr Core::uint32 kCurrentImporterVersion = 7;
|
||||
static constexpr Core::uint32 kBaseImporterVersion = 7;
|
||||
|
||||
void EnsureProjectLayout();
|
||||
void LoadSourceAssetDB();
|
||||
@@ -126,6 +137,7 @@ private:
|
||||
static Containers::String NormalizePathString(const Containers::String& path);
|
||||
static Containers::String MakeKey(const Containers::String& path);
|
||||
static Containers::String GetImporterNameForPath(const Containers::String& relativePath, bool isFolder);
|
||||
static Core::uint32 GetCurrentImporterVersion(const Containers::String& importerName);
|
||||
static ResourceType GetPrimaryResourceTypeForImporter(const Containers::String& importerName);
|
||||
|
||||
bool ShouldReimport(const SourceAssetRecord& sourceRecord,
|
||||
@@ -154,6 +166,8 @@ private:
|
||||
const SourceAssetRecord& sourceRecord,
|
||||
const std::vector<ArtifactDependencyRecord>& dependencies = {}) const;
|
||||
Containers::String BuildArtifactDirectory(const Containers::String& artifactKey) const;
|
||||
Containers::String BuildArtifactFilePath(const Containers::String& artifactKey,
|
||||
const char* extension) const;
|
||||
static Containers::String ReadWholeFileText(const std::filesystem::path& path);
|
||||
static Containers::String ComputeFileHash(const std::filesystem::path& path);
|
||||
static Core::uint64 GetFileSizeValue(const std::filesystem::path& path);
|
||||
|
||||
@@ -47,7 +47,11 @@ public:
|
||||
AssetGUID assetGuid;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
Containers::String runtimeLoadPath;
|
||||
Containers::String artifactMainPath;
|
||||
Containers::String artifactMainEntryPath;
|
||||
Containers::String artifactDirectory;
|
||||
ArtifactStorageKind artifactStorageKind = ArtifactStorageKind::Unknown;
|
||||
Containers::String mainEntryName;
|
||||
LocalID mainLocalID = kMainAssetLocalID;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
virtual void SetTopology(uint32_t topologyType) = 0; // PrimitiveTopologyType
|
||||
virtual void SetRenderTargetFormats(uint32_t count, const uint32_t* formats, uint32_t depthFormat) = 0;
|
||||
virtual void SetSampleCount(uint32_t count) = 0;
|
||||
virtual void SetSampleQuality(uint32_t quality) = 0;
|
||||
virtual void SetComputeShader(RHIShader* shader) = 0;
|
||||
|
||||
// State query
|
||||
|
||||
@@ -54,6 +54,7 @@ struct RenderDirectionalShadowData {
|
||||
bool enabled = false;
|
||||
Math::Matrix4x4 viewProjection = Math::Matrix4x4::Identity();
|
||||
Math::Vector4 shadowParams = Math::Vector4::Zero();
|
||||
Math::Vector4 shadowOptions = Math::Vector4::Zero();
|
||||
RHI::RHIResourceView* shadowMap = nullptr;
|
||||
|
||||
bool IsValid() const {
|
||||
|
||||
@@ -29,6 +29,11 @@ struct BuiltinSkyboxMaterialData {
|
||||
BuiltinSkyboxTextureMode textureMode = BuiltinSkyboxTextureMode::None;
|
||||
};
|
||||
|
||||
struct BuiltinDepthStyleMaterialConstants {
|
||||
Math::Vector4 baseColorFactor = Math::Vector4::One();
|
||||
Math::Vector4 alphaCutoffParams = Math::Vector4(0.5f, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
|
||||
struct MaterialConstantLayoutView {
|
||||
const Resources::MaterialConstantFieldDesc* fields = nullptr;
|
||||
size_t count = 0;
|
||||
@@ -277,6 +282,40 @@ inline MaterialConstantPayloadView ResolveSchemaMaterialConstantPayload(const Re
|
||||
return { constantBufferData.Data(), constantBufferData.Size(), layoutView };
|
||||
}
|
||||
|
||||
inline BuiltinDepthStyleMaterialConstants BuildBuiltinDepthStyleMaterialConstants(
|
||||
const Resources::Material* material) {
|
||||
BuiltinDepthStyleMaterialConstants constants = {};
|
||||
constants.baseColorFactor = ResolveBuiltinBaseColorFactor(material);
|
||||
constants.alphaCutoffParams = Math::Vector4(ResolveBuiltinAlphaCutoff(material), 0.0f, 0.0f, 0.0f);
|
||||
return constants;
|
||||
}
|
||||
|
||||
inline MaterialConstantPayloadView ResolveBuiltinDepthStyleMaterialConstantPayload(
|
||||
const Resources::Material* material,
|
||||
BuiltinDepthStyleMaterialConstants& outConstants,
|
||||
Resources::MaterialConstantFieldDesc (&outLayout)[2]) {
|
||||
outConstants = BuildBuiltinDepthStyleMaterialConstants(material);
|
||||
|
||||
outLayout[0].name = "gBaseColorFactor";
|
||||
outLayout[0].type = Resources::MaterialPropertyType::Float4;
|
||||
outLayout[0].offset = 0u;
|
||||
outLayout[0].size = static_cast<Core::uint32>(sizeof(Math::Vector4));
|
||||
outLayout[0].alignedSize = static_cast<Core::uint32>(sizeof(Math::Vector4));
|
||||
|
||||
outLayout[1].name = "gAlphaCutoffParams";
|
||||
outLayout[1].type = Resources::MaterialPropertyType::Float4;
|
||||
outLayout[1].offset = static_cast<Core::uint32>(sizeof(Math::Vector4));
|
||||
outLayout[1].size = static_cast<Core::uint32>(sizeof(Math::Vector4));
|
||||
outLayout[1].alignedSize = static_cast<Core::uint32>(sizeof(Math::Vector4));
|
||||
|
||||
MaterialConstantLayoutView layoutView = {};
|
||||
layoutView.fields = outLayout;
|
||||
layoutView.count = 2u;
|
||||
layoutView.size = sizeof(BuiltinDepthStyleMaterialConstants);
|
||||
|
||||
return { &outConstants, sizeof(BuiltinDepthStyleMaterialConstants), layoutView };
|
||||
}
|
||||
|
||||
inline bool TryResolveMaterialBufferResourceView(
|
||||
const Resources::Material* material,
|
||||
const BuiltinPassResourceBindingDesc& binding,
|
||||
|
||||
@@ -43,8 +43,8 @@ public:
|
||||
const Containers::String& GetShaderPath() const;
|
||||
|
||||
private:
|
||||
bool EnsureInitialized(const RenderContext& renderContext, RHI::Format renderTargetFormat);
|
||||
bool CreateResources(const RenderContext& renderContext, RHI::Format renderTargetFormat);
|
||||
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
void DestroyResources();
|
||||
void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet);
|
||||
|
||||
@@ -53,6 +53,8 @@ private:
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
|
||||
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
|
||||
uint32_t m_renderTargetSampleCount = 1u;
|
||||
uint32_t m_renderTargetSampleQuality = 0u;
|
||||
Resources::ResourceHandle<Resources::Shader> m_shader;
|
||||
RHI::RHISampler* m_sampler = nullptr;
|
||||
RHI::RHIPipelineLayout* m_pipelineLayout = nullptr;
|
||||
|
||||
@@ -133,6 +133,8 @@ private:
|
||||
uint32_t renderTargetCount = 0;
|
||||
uint32_t renderTargetFormat = 0;
|
||||
uint32_t depthStencilFormat = 0;
|
||||
uint32_t sampleCount = 1;
|
||||
uint32_t sampleQuality = 0;
|
||||
|
||||
bool operator==(const PipelineStateKey& other) const {
|
||||
return renderState == other.renderState &&
|
||||
@@ -141,7 +143,9 @@ private:
|
||||
keywordSignature == other.keywordSignature &&
|
||||
renderTargetCount == other.renderTargetCount &&
|
||||
renderTargetFormat == other.renderTargetFormat &&
|
||||
depthStencilFormat == other.depthStencilFormat;
|
||||
depthStencilFormat == other.depthStencilFormat &&
|
||||
sampleCount == other.sampleCount &&
|
||||
sampleQuality == other.sampleQuality;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,6 +158,8 @@ private:
|
||||
hash ^= std::hash<uint32_t>{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.sampleCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.sampleQuality) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ public:
|
||||
const Containers::String& GetShaderPath() const;
|
||||
|
||||
private:
|
||||
bool EnsureInitialized(const RenderContext& renderContext, RHI::Format renderTargetFormat);
|
||||
bool CreateResources(const RenderContext& renderContext, RHI::Format renderTargetFormat);
|
||||
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
void DestroyResources();
|
||||
void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet);
|
||||
|
||||
@@ -53,6 +53,8 @@ private:
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
|
||||
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
|
||||
uint32_t m_renderTargetSampleCount = 1u;
|
||||
uint32_t m_renderTargetSampleQuality = 0u;
|
||||
Resources::ResourceHandle<Resources::Shader> m_shader;
|
||||
RHI::RHISampler* m_sampler = nullptr;
|
||||
RHI::RHIPipelineLayout* m_pipelineLayout = nullptr;
|
||||
|
||||
@@ -55,8 +55,8 @@ public:
|
||||
const InfiniteGridPassData& data);
|
||||
|
||||
private:
|
||||
bool EnsureInitialized(const RenderContext& renderContext);
|
||||
bool CreateResources(const RenderContext& renderContext);
|
||||
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
void DestroyResources();
|
||||
|
||||
RHI::RHIDevice* m_device = nullptr;
|
||||
@@ -67,6 +67,9 @@ private:
|
||||
RHI::RHIDescriptorSet* m_constantSet = nullptr;
|
||||
Containers::String m_shaderPath;
|
||||
Resources::ResourceHandle<Resources::Shader> m_builtinInfiniteGridShader;
|
||||
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
|
||||
RHI::Format m_depthStencilFormat = RHI::Format::Unknown;
|
||||
uint32_t m_renderTargetSampleCount = 1u;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
|
||||
@@ -31,6 +31,11 @@ struct ObjectIdOutlineStyle {
|
||||
bool debugSelectionMask = false;
|
||||
};
|
||||
|
||||
struct ObjectIdOutlinePassInputs {
|
||||
RHI::RHIResourceView* objectIdTextureView = nullptr;
|
||||
RHI::ResourceStates objectIdTextureState = RHI::ResourceStates::PixelShaderResource;
|
||||
};
|
||||
|
||||
class BuiltinObjectIdOutlinePass {
|
||||
public:
|
||||
explicit BuiltinObjectIdOutlinePass(Containers::String shaderPath = Containers::String());
|
||||
@@ -50,7 +55,7 @@ public:
|
||||
bool Render(
|
||||
const RenderContext& renderContext,
|
||||
const RenderSurface& surface,
|
||||
RHI::RHIResourceView* objectIdTextureView,
|
||||
const ObjectIdOutlinePassInputs& inputs,
|
||||
const std::vector<uint64_t>& selectedObjectIds,
|
||||
const ObjectIdOutlineStyle& style = {});
|
||||
|
||||
@@ -62,8 +67,8 @@ private:
|
||||
std::array<Math::Vector4, kMaxSelectedObjectCount> selectedObjectColors = {};
|
||||
};
|
||||
|
||||
bool EnsureInitialized(const RenderContext& renderContext);
|
||||
bool CreateResources(const RenderContext& renderContext);
|
||||
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
void DestroyResources();
|
||||
bool HasCreatedResources() const;
|
||||
void ResetState();
|
||||
@@ -78,6 +83,8 @@ private:
|
||||
RHI::RHIDescriptorSet* m_textureSet = nullptr;
|
||||
Containers::String m_shaderPath;
|
||||
std::optional<Resources::ResourceHandle<Resources::Shader>> m_builtinObjectIdOutlineShader;
|
||||
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
|
||||
uint32_t m_renderTargetSampleCount = 1u;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
|
||||
@@ -27,6 +27,13 @@ struct SelectionOutlineStyle {
|
||||
bool debugSelectionMask = false;
|
||||
};
|
||||
|
||||
struct SelectionOutlinePassInputs {
|
||||
RHI::RHIResourceView* selectionMaskTextureView = nullptr;
|
||||
RHI::ResourceStates selectionMaskState = RHI::ResourceStates::PixelShaderResource;
|
||||
RHI::RHIResourceView* depthTextureView = nullptr;
|
||||
RHI::ResourceStates depthTextureState = RHI::ResourceStates::DepthWrite;
|
||||
};
|
||||
|
||||
class BuiltinSelectionOutlinePass {
|
||||
public:
|
||||
explicit BuiltinSelectionOutlinePass(Containers::String shaderPath = Containers::String());
|
||||
@@ -44,8 +51,7 @@ public:
|
||||
bool Render(
|
||||
const RenderContext& renderContext,
|
||||
const RenderSurface& surface,
|
||||
RHI::RHIResourceView* selectionMaskTextureView,
|
||||
RHI::RHIResourceView* depthTextureView,
|
||||
const SelectionOutlinePassInputs& inputs,
|
||||
const SelectionOutlineStyle& style = {});
|
||||
|
||||
private:
|
||||
@@ -56,8 +62,8 @@ private:
|
||||
Math::Vector4 depthParams = Math::Vector4::Zero();
|
||||
};
|
||||
|
||||
bool EnsureInitialized(const RenderContext& renderContext);
|
||||
bool CreateResources(const RenderContext& renderContext);
|
||||
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
|
||||
void DestroyResources();
|
||||
bool HasCreatedResources() const;
|
||||
void ResetState();
|
||||
@@ -72,6 +78,8 @@ private:
|
||||
RHI::RHIDescriptorSet* m_textureSet = nullptr;
|
||||
Containers::String m_shaderPath;
|
||||
std::optional<Resources::ResourceHandle<Resources::Shader>> m_builtinSelectionOutlineShader;
|
||||
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
|
||||
uint32_t m_renderTargetSampleCount = 1u;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
|
||||
const char* GetName() const override;
|
||||
bool Initialize(const RenderContext& context) override;
|
||||
bool PrepareVolumeResources(
|
||||
const RenderContext& context,
|
||||
const RenderSceneData& sceneData);
|
||||
bool Execute(const RenderPassContext& context) override;
|
||||
void Shutdown() override;
|
||||
|
||||
@@ -140,6 +143,8 @@ private:
|
||||
uint32_t renderTargetCount = 0;
|
||||
uint32_t renderTargetFormat = 0;
|
||||
uint32_t depthStencilFormat = 0;
|
||||
uint32_t sampleCount = 1;
|
||||
uint32_t sampleQuality = 0;
|
||||
|
||||
bool operator==(const PipelineStateKey& other) const {
|
||||
return renderState == other.renderState &&
|
||||
@@ -148,7 +153,9 @@ private:
|
||||
keywordSignature == other.keywordSignature &&
|
||||
renderTargetCount == other.renderTargetCount &&
|
||||
renderTargetFormat == other.renderTargetFormat &&
|
||||
depthStencilFormat == other.depthStencilFormat;
|
||||
depthStencilFormat == other.depthStencilFormat &&
|
||||
sampleCount == other.sampleCount &&
|
||||
sampleQuality == other.sampleQuality;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,6 +168,8 @@ private:
|
||||
hash ^= std::hash<uint32_t>{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.sampleCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
hash ^= std::hash<uint32_t>{}(key.sampleQuality) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,6 +128,7 @@ struct DirectionalShadowRenderPlan {
|
||||
Math::Vector3 lightDirection = Math::Vector3::Back();
|
||||
Math::Vector3 focusPoint = Math::Vector3::Zero();
|
||||
float orthographicHalfExtent = 0.0f;
|
||||
float texelWorldSize = 0.0f;
|
||||
float nearClipPlane = 0.1f;
|
||||
float farClipPlane = 0.0f;
|
||||
uint32_t mapWidth = 0;
|
||||
|
||||
@@ -22,6 +22,7 @@ struct RenderPassContext {
|
||||
const RenderSceneData& sceneData;
|
||||
const RenderSurface* sourceSurface = nullptr;
|
||||
RHI::RHIResourceView* sourceColorView = nullptr;
|
||||
RHI::ResourceStates sourceColorState = RHI::ResourceStates::Common;
|
||||
};
|
||||
|
||||
class RenderPass {
|
||||
|
||||
@@ -51,6 +51,16 @@ public:
|
||||
void SetColorStateAfter(RHI::ResourceStates state) { m_colorStateAfter = state; }
|
||||
RHI::ResourceStates GetColorStateAfter() const { return m_colorStateAfter; }
|
||||
|
||||
void SetDepthStateBefore(RHI::ResourceStates state) { m_depthStateBefore = state; }
|
||||
RHI::ResourceStates GetDepthStateBefore() const { return m_depthStateBefore; }
|
||||
|
||||
void SetDepthStateAfter(RHI::ResourceStates state) { m_depthStateAfter = state; }
|
||||
RHI::ResourceStates GetDepthStateAfter() const { return m_depthStateAfter; }
|
||||
|
||||
void SetSampleDesc(uint32_t sampleCount, uint32_t sampleQuality = 0);
|
||||
uint32_t GetSampleCount() const { return m_sampleCount; }
|
||||
uint32_t GetSampleQuality() const { return m_sampleQuality; }
|
||||
|
||||
private:
|
||||
uint32_t m_width = 0;
|
||||
uint32_t m_height = 0;
|
||||
@@ -63,6 +73,10 @@ private:
|
||||
bool m_autoTransition = true;
|
||||
RHI::ResourceStates m_colorStateBefore = RHI::ResourceStates::Present;
|
||||
RHI::ResourceStates m_colorStateAfter = RHI::ResourceStates::Present;
|
||||
RHI::ResourceStates m_depthStateBefore = RHI::ResourceStates::DepthWrite;
|
||||
RHI::ResourceStates m_depthStateAfter = RHI::ResourceStates::DepthWrite;
|
||||
uint32_t m_sampleCount = 1;
|
||||
uint32_t m_sampleQuality = 0;
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class GaussianSplat;
|
||||
|
||||
bool SerializeGaussianSplatArtifactPayload(const GaussianSplat& gaussianSplat,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
|
||||
const GaussianSplat& gaussianSplat,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Model;
|
||||
|
||||
bool SerializeModelArtifactPayload(const Model& model,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
bool WriteModelArtifactFile(const Containers::String& artifactPath,
|
||||
const Model& model,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
#include <XCEngine/Resources/Shader/Shader.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
constexpr Core::uint32 kShaderCompilationCacheSchemaVersion = 1;
|
||||
|
||||
enum class ShaderBytecodeFormat : Core::uint32 {
|
||||
Unknown = 0,
|
||||
DXIL,
|
||||
DXBC,
|
||||
SPIRV,
|
||||
GLSLSource,
|
||||
OpenGLProgramBinary
|
||||
};
|
||||
|
||||
struct ShaderCompileKey {
|
||||
Containers::String shaderPath;
|
||||
Containers::String sourceHash;
|
||||
Containers::String dependencyHash;
|
||||
Containers::String passName;
|
||||
Containers::String entryPoint;
|
||||
Containers::String profile;
|
||||
Containers::String compilerName;
|
||||
Containers::String compilerVersion;
|
||||
Containers::String optionsSignature;
|
||||
ShaderType stage = ShaderType::Fragment;
|
||||
ShaderLanguage sourceLanguage = ShaderLanguage::HLSL;
|
||||
ShaderBackend backend = ShaderBackend::Generic;
|
||||
Containers::Array<Containers::String> keywords;
|
||||
|
||||
void Normalize();
|
||||
Containers::String BuildSignature() const;
|
||||
Containers::String BuildCacheKey() const;
|
||||
};
|
||||
|
||||
struct ShaderCacheEntry {
|
||||
ShaderCompileKey key;
|
||||
ShaderBytecodeFormat format = ShaderBytecodeFormat::Unknown;
|
||||
Containers::Array<Core::uint8> payload;
|
||||
};
|
||||
|
||||
class ShaderCompilationCache {
|
||||
public:
|
||||
void Initialize(const Containers::String& libraryRoot);
|
||||
void Shutdown();
|
||||
|
||||
bool IsInitialized() const { return !m_libraryRoot.Empty(); }
|
||||
const Containers::String& GetLibraryRoot() const { return m_libraryRoot; }
|
||||
const Containers::String& GetDatabasePath() const { return m_databasePath; }
|
||||
Core::uint32 GetRecordCount() const { return static_cast<Core::uint32>(m_records.size()); }
|
||||
|
||||
Containers::String BuildCacheKey(const ShaderCompileKey& key) const;
|
||||
Containers::String BuildCacheRelativePath(const ShaderCompileKey& key) const;
|
||||
Containers::String BuildCacheAbsolutePath(const ShaderCompileKey& key) const;
|
||||
|
||||
bool Store(const ShaderCacheEntry& entry,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
bool TryLoad(const ShaderCompileKey& key,
|
||||
ShaderCacheEntry& outEntry,
|
||||
Containers::String* outErrorMessage = nullptr) const;
|
||||
|
||||
private:
|
||||
struct ShaderCacheRecord {
|
||||
ShaderBackend backend = ShaderBackend::Generic;
|
||||
ShaderBytecodeFormat format = ShaderBytecodeFormat::Unknown;
|
||||
Containers::String relativePath;
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
void LoadDatabase();
|
||||
void SaveDatabase() const;
|
||||
|
||||
static Containers::String BuildBackendDirectoryName(ShaderBackend backend);
|
||||
static AssetGUID ComputeKeyGuid(const Containers::String& cacheKey);
|
||||
|
||||
Containers::String m_libraryRoot;
|
||||
Containers::String m_databasePath;
|
||||
std::unordered_map<std::string, ShaderCacheRecord> m_records;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace Detail {
|
||||
|
||||
enum class TextureSemanticUsage {
|
||||
Unknown,
|
||||
Color,
|
||||
Data
|
||||
};
|
||||
|
||||
inline std::string ToLowerCopy(const Containers::String& value) {
|
||||
std::string lower = value.CStr() != nullptr ? std::string(value.CStr()) : std::string();
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return lower;
|
||||
}
|
||||
|
||||
inline bool ContainsAnyToken(const std::string& value, std::initializer_list<const char*> tokens) {
|
||||
for (const char* token : tokens) {
|
||||
if (token != nullptr && value.find(token) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline TextureSemanticUsage ClassifyTextureSemanticUsage(const std::string& identifierLower) {
|
||||
if (identifierLower.empty()) {
|
||||
return TextureSemanticUsage::Unknown;
|
||||
}
|
||||
|
||||
// Color ramps are authored visually even when they drive toon bands.
|
||||
if (ContainsAnyToken(identifierLower, { "shadow_ramp", "specular_ramp", "toon_ramp", "ramp" })) {
|
||||
return TextureSemanticUsage::Color;
|
||||
}
|
||||
|
||||
if (ContainsAnyToken(
|
||||
identifierLower,
|
||||
{ "normal",
|
||||
"nrm",
|
||||
"lightmap",
|
||||
"light_map",
|
||||
"metal",
|
||||
"metallic",
|
||||
"rough",
|
||||
"roughness",
|
||||
"smoothness",
|
||||
"gloss",
|
||||
"mask",
|
||||
"occlusion",
|
||||
"ao",
|
||||
"orm",
|
||||
"height",
|
||||
"displace",
|
||||
"face_shadow",
|
||||
"faceshadow",
|
||||
"lookup" })) {
|
||||
return TextureSemanticUsage::Data;
|
||||
}
|
||||
|
||||
if (ContainsAnyToken(
|
||||
identifierLower,
|
||||
{ "diffuse",
|
||||
"albedo",
|
||||
"basecolor",
|
||||
"base_color",
|
||||
"basemap",
|
||||
"base_map",
|
||||
"maintex",
|
||||
"main_tex",
|
||||
"colormap",
|
||||
"color_map",
|
||||
"emissive",
|
||||
"emission" })) {
|
||||
return TextureSemanticUsage::Color;
|
||||
}
|
||||
|
||||
return TextureSemanticUsage::Unknown;
|
||||
}
|
||||
|
||||
inline bool ResolveTextureSRGBUsage(const Containers::String& identifier, const char* propertyName) {
|
||||
const TextureSemanticUsage identifierUsage = ClassifyTextureSemanticUsage(ToLowerCopy(identifier));
|
||||
if (identifierUsage == TextureSemanticUsage::Color) {
|
||||
return true;
|
||||
}
|
||||
if (identifierUsage == TextureSemanticUsage::Data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (propertyName != nullptr && propertyName[0] != '\0') {
|
||||
const Containers::String propertyIdentifier(propertyName);
|
||||
const TextureSemanticUsage propertyUsage =
|
||||
ClassifyTextureSemanticUsage(ToLowerCopy(propertyIdentifier));
|
||||
if (propertyUsage == TextureSemanticUsage::Color) {
|
||||
return true;
|
||||
}
|
||||
if (propertyUsage == TextureSemanticUsage::Data) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Match Unity-style defaults: unknown image assets are assumed to be color textures.
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
inline TextureImportSettings BuildTextureImportSettingsFromIdentifier(
|
||||
const Containers::String& identifier,
|
||||
const char* propertyName = nullptr) {
|
||||
TextureImportSettings settings;
|
||||
const bool importAsSRGB = Detail::ResolveTextureSRGBUsage(identifier, propertyName);
|
||||
settings.SetSRGB(importAsSRGB);
|
||||
settings.SetTargetFormat(importAsSRGB ? TextureFormat::RGBA8_SRGB : TextureFormat::RGBA8_UNORM);
|
||||
return settings;
|
||||
}
|
||||
|
||||
inline TextureImportSettings BuildTextureImportSettingsForMaterialProperty(
|
||||
const char* propertyName,
|
||||
const Containers::String& texturePath = Containers::String()) {
|
||||
if (!texturePath.Empty()) {
|
||||
return BuildTextureImportSettingsFromIdentifier(texturePath, propertyName);
|
||||
}
|
||||
|
||||
return BuildTextureImportSettingsFromIdentifier(
|
||||
propertyName != nullptr ? Containers::String(propertyName) : Containers::String(),
|
||||
propertyName);
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
341
engine/include/XCEngine/UI/Widgets/UIDragDropInteraction.h
Normal file
341
engine/include/XCEngine/UI/Widgets/UIDragDropInteraction.h
Normal file
@@ -0,0 +1,341 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/Core/UIInvalidation.h>
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::UI::Widgets {
|
||||
|
||||
enum class UIDragDropOperation : std::uint8_t {
|
||||
None = 0,
|
||||
Copy = 1 << 0,
|
||||
Move = 1 << 1,
|
||||
Link = 1 << 2
|
||||
};
|
||||
|
||||
inline constexpr UIDragDropOperation operator|(
|
||||
UIDragDropOperation left,
|
||||
UIDragDropOperation right) {
|
||||
return static_cast<UIDragDropOperation>(
|
||||
static_cast<std::uint8_t>(left) |
|
||||
static_cast<std::uint8_t>(right));
|
||||
}
|
||||
|
||||
inline constexpr UIDragDropOperation operator&(
|
||||
UIDragDropOperation left,
|
||||
UIDragDropOperation right) {
|
||||
return static_cast<UIDragDropOperation>(
|
||||
static_cast<std::uint8_t>(left) &
|
||||
static_cast<std::uint8_t>(right));
|
||||
}
|
||||
|
||||
inline constexpr UIDragDropOperation& operator|=(
|
||||
UIDragDropOperation& left,
|
||||
UIDragDropOperation right) {
|
||||
left = left | right;
|
||||
return left;
|
||||
}
|
||||
|
||||
inline constexpr bool HasAnyUIDragDropOperation(
|
||||
UIDragDropOperation value,
|
||||
UIDragDropOperation flags) {
|
||||
return (value & flags) != UIDragDropOperation::None;
|
||||
}
|
||||
|
||||
struct UIDragDropPayload {
|
||||
std::string typeId = {};
|
||||
std::string itemId = {};
|
||||
std::string label = {};
|
||||
};
|
||||
|
||||
struct UIDragDropSourceDescriptor {
|
||||
UIElementId ownerId = 0u;
|
||||
std::string sourceId = {};
|
||||
UIPoint pointerDownPosition = {};
|
||||
UIDragDropPayload payload = {};
|
||||
UIDragDropOperation allowedOperations = UIDragDropOperation::Move;
|
||||
float activationDistance = 4.0f;
|
||||
};
|
||||
|
||||
struct UIDragDropTargetDescriptor {
|
||||
UIElementId ownerId = 0u;
|
||||
std::string targetId = {};
|
||||
std::span<const std::string_view> acceptedPayloadTypes = {};
|
||||
UIDragDropOperation acceptedOperations =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Move |
|
||||
UIDragDropOperation::Link;
|
||||
UIDragDropOperation preferredOperation = UIDragDropOperation::Move;
|
||||
};
|
||||
|
||||
struct UIDragDropState {
|
||||
bool armed = false;
|
||||
bool active = false;
|
||||
UIElementId sourceOwnerId = 0u;
|
||||
std::string sourceId = {};
|
||||
UIPoint pointerDownPosition = {};
|
||||
UIPoint pointerPosition = {};
|
||||
UIDragDropPayload payload = {};
|
||||
UIDragDropOperation allowedOperations = UIDragDropOperation::None;
|
||||
float activationDistance = 4.0f;
|
||||
UIElementId targetOwnerId = 0u;
|
||||
std::string targetId = {};
|
||||
UIDragDropOperation previewOperation = UIDragDropOperation::None;
|
||||
};
|
||||
|
||||
struct UIDragDropResult {
|
||||
bool armed = false;
|
||||
bool activated = false;
|
||||
bool targetChanged = false;
|
||||
bool completed = false;
|
||||
bool cancelled = false;
|
||||
UIElementId sourceOwnerId = 0u;
|
||||
UIElementId targetOwnerId = 0u;
|
||||
std::string sourceId = {};
|
||||
std::string targetId = {};
|
||||
std::string payloadTypeId = {};
|
||||
std::string payloadItemId = {};
|
||||
UIDragDropOperation operation = UIDragDropOperation::None;
|
||||
};
|
||||
|
||||
inline constexpr bool IsUIDragDropInProgress(const UIDragDropState& state) {
|
||||
return state.armed || state.active;
|
||||
}
|
||||
|
||||
inline constexpr bool HasResolvedUIDragDropTarget(const UIDragDropState& state) {
|
||||
return state.targetOwnerId != 0u &&
|
||||
!state.targetId.empty() &&
|
||||
state.previewOperation != UIDragDropOperation::None;
|
||||
}
|
||||
|
||||
inline bool DoesUIDragDropPayloadTypeMatch(
|
||||
std::string_view payloadType,
|
||||
std::span<const std::string_view> acceptedPayloadTypes) {
|
||||
if (acceptedPayloadTypes.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return std::find(
|
||||
acceptedPayloadTypes.begin(),
|
||||
acceptedPayloadTypes.end(),
|
||||
payloadType) != acceptedPayloadTypes.end();
|
||||
}
|
||||
|
||||
inline constexpr UIDragDropOperation PickUIDragDropOperation(
|
||||
UIDragDropOperation availableOperations,
|
||||
UIDragDropOperation preferredOperations = UIDragDropOperation::None) {
|
||||
const UIDragDropOperation preferredAvailable =
|
||||
availableOperations & preferredOperations;
|
||||
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Move)) {
|
||||
return UIDragDropOperation::Move;
|
||||
}
|
||||
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Copy)) {
|
||||
return UIDragDropOperation::Copy;
|
||||
}
|
||||
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Link)) {
|
||||
return UIDragDropOperation::Link;
|
||||
}
|
||||
|
||||
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Move)) {
|
||||
return UIDragDropOperation::Move;
|
||||
}
|
||||
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Copy)) {
|
||||
return UIDragDropOperation::Copy;
|
||||
}
|
||||
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Link)) {
|
||||
return UIDragDropOperation::Link;
|
||||
}
|
||||
return UIDragDropOperation::None;
|
||||
}
|
||||
|
||||
inline UIDragDropOperation ResolveUIDragDropOperation(
|
||||
UIDragDropOperation allowedOperations,
|
||||
const UIDragDropTargetDescriptor& target) {
|
||||
const UIDragDropOperation supportedOperations =
|
||||
allowedOperations & target.acceptedOperations;
|
||||
if (supportedOperations == UIDragDropOperation::None) {
|
||||
return UIDragDropOperation::None;
|
||||
}
|
||||
|
||||
return PickUIDragDropOperation(
|
||||
supportedOperations,
|
||||
target.preferredOperation);
|
||||
}
|
||||
|
||||
inline UIDragDropOperation ResolveUIDragDropOperation(
|
||||
const UIDragDropState& state,
|
||||
const UIDragDropTargetDescriptor& target) {
|
||||
if (!state.active ||
|
||||
!DoesUIDragDropPayloadTypeMatch(state.payload.typeId, target.acceptedPayloadTypes)) {
|
||||
return UIDragDropOperation::None;
|
||||
}
|
||||
|
||||
return ResolveUIDragDropOperation(state.allowedOperations, target);
|
||||
}
|
||||
|
||||
inline void FillUIDragDropResult(
|
||||
const UIDragDropState& state,
|
||||
UIDragDropResult& result) {
|
||||
result.sourceOwnerId = state.sourceOwnerId;
|
||||
result.targetOwnerId = state.targetOwnerId;
|
||||
result.sourceId = state.sourceId;
|
||||
result.targetId = state.targetId;
|
||||
result.payloadTypeId = state.payload.typeId;
|
||||
result.payloadItemId = state.payload.itemId;
|
||||
result.operation = state.previewOperation;
|
||||
}
|
||||
|
||||
inline bool BeginUIDragDrop(
|
||||
const UIDragDropSourceDescriptor& descriptor,
|
||||
UIDragDropState& outState,
|
||||
UIDragDropResult* outResult = nullptr) {
|
||||
if (descriptor.ownerId == 0u ||
|
||||
descriptor.allowedOperations == UIDragDropOperation::None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outState = {};
|
||||
outState.armed = true;
|
||||
outState.sourceOwnerId = descriptor.ownerId;
|
||||
outState.sourceId = descriptor.sourceId;
|
||||
outState.pointerDownPosition = descriptor.pointerDownPosition;
|
||||
outState.pointerPosition = descriptor.pointerDownPosition;
|
||||
outState.payload = descriptor.payload;
|
||||
outState.allowedOperations = descriptor.allowedOperations;
|
||||
outState.activationDistance = (std::max)(descriptor.activationDistance, 0.0f);
|
||||
|
||||
if (outResult != nullptr) {
|
||||
*outResult = {};
|
||||
outResult->armed = true;
|
||||
FillUIDragDropResult(outState, *outResult);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool UpdateUIDragDropPointer(
|
||||
UIDragDropState& state,
|
||||
const UIPoint& pointerPosition,
|
||||
UIDragDropResult* outResult = nullptr) {
|
||||
if (!IsUIDragDropInProgress(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.pointerPosition = pointerPosition;
|
||||
|
||||
UIDragDropResult localResult = {};
|
||||
FillUIDragDropResult(state, localResult);
|
||||
if (!state.active) {
|
||||
const float deltaX = pointerPosition.x - state.pointerDownPosition.x;
|
||||
const float deltaY = pointerPosition.y - state.pointerDownPosition.y;
|
||||
const float distance = std::sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
if (distance >= state.activationDistance) {
|
||||
state.active = true;
|
||||
localResult.activated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (outResult != nullptr) {
|
||||
*outResult = std::move(localResult);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void ClearUIDragDropTarget(
|
||||
UIDragDropState& state,
|
||||
UIDragDropResult* outResult = nullptr) {
|
||||
const bool changed =
|
||||
state.targetOwnerId != 0u ||
|
||||
!state.targetId.empty() ||
|
||||
state.previewOperation != UIDragDropOperation::None;
|
||||
state.targetOwnerId = 0u;
|
||||
state.targetId.clear();
|
||||
state.previewOperation = UIDragDropOperation::None;
|
||||
|
||||
if (outResult != nullptr) {
|
||||
*outResult = {};
|
||||
FillUIDragDropResult(state, *outResult);
|
||||
outResult->targetChanged = changed;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool UpdateUIDragDropTarget(
|
||||
UIDragDropState& state,
|
||||
const UIDragDropTargetDescriptor* target,
|
||||
UIDragDropResult* outResult = nullptr) {
|
||||
if (!state.active) {
|
||||
ClearUIDragDropTarget(state, outResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target == nullptr) {
|
||||
ClearUIDragDropTarget(state, outResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIDragDropOperation resolvedOperation =
|
||||
ResolveUIDragDropOperation(state, *target);
|
||||
if (resolvedOperation == UIDragDropOperation::None) {
|
||||
ClearUIDragDropTarget(state, outResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool changed =
|
||||
state.targetOwnerId != target->ownerId ||
|
||||
state.targetId != target->targetId ||
|
||||
state.previewOperation != resolvedOperation;
|
||||
|
||||
state.targetOwnerId = target->ownerId;
|
||||
state.targetId = target->targetId;
|
||||
state.previewOperation = resolvedOperation;
|
||||
|
||||
if (outResult != nullptr) {
|
||||
*outResult = {};
|
||||
FillUIDragDropResult(state, *outResult);
|
||||
outResult->targetChanged = changed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool EndUIDragDrop(
|
||||
UIDragDropState& state,
|
||||
UIDragDropResult& outResult) {
|
||||
if (!IsUIDragDropInProgress(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult = {};
|
||||
FillUIDragDropResult(state, outResult);
|
||||
if (state.active && HasResolvedUIDragDropTarget(state)) {
|
||||
outResult.completed = true;
|
||||
} else {
|
||||
outResult.cancelled = true;
|
||||
}
|
||||
|
||||
state = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool CancelUIDragDrop(
|
||||
UIDragDropState& state,
|
||||
UIDragDropResult* outResult = nullptr) {
|
||||
if (!IsUIDragDropInProgress(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outResult != nullptr) {
|
||||
*outResult = {};
|
||||
FillUIDragDropResult(state, *outResult);
|
||||
outResult->cancelled = true;
|
||||
}
|
||||
|
||||
state = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Widgets
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
bool RemoveSelection(std::string_view selectionId);
|
||||
bool ClearSelection();
|
||||
bool ToggleSelection(std::string selectionId);
|
||||
bool ToggleSelectionMembership(std::string selectionId, bool makePrimary = true);
|
||||
|
||||
private:
|
||||
static void NormalizeSelectionIds(std::vector<std::string>& selectionIds);
|
||||
|
||||
Reference in New Issue
Block a user