From ed8c27fde2aafba0e1d5e00e1120bf4753807c9f Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 13:18:32 +0800 Subject: [PATCH] fix: clear empty inspector state --- editor/src/panels/InspectorPanel.cpp | 984 ++++++++++++++++++++++++++- 1 file changed, 969 insertions(+), 15 deletions(-) diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 67d2b16b..2fcf9a79 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -1,16 +1,30 @@ #include "Actions/InspectorActionRouter.h" #include "Actions/ActionRouting.h" #include "Actions/EditorActions.h" +#include "Commands/ProjectCommands.h" #include "InspectorPanel.h" +#include "Core/AssetItem.h" #include "Core/IEditorContext.h" +#include "Core/IProjectManager.h" #include "Core/ISceneManager.h" #include "Core/ISelectionManager.h" #include "Core/EventBus.h" #include "Core/EditorEvents.h" #include "ComponentEditors/ComponentEditorRegistry.h" #include "ComponentEditors/IComponentEditor.h" +#include "Platform/Win32Utf8.h" +#include "Utils/ProjectFileUtils.h" #include "UI/UI.h" + +#include +#include + +#include +#include +#include +#include #include +#include #include namespace XCEngine { @@ -40,6 +54,496 @@ void DrawInspectorComponentContextMenu( }); } +template +void CopyToCharBuffer(const std::string& value, std::array& buffer) { + buffer.fill('\0'); + if constexpr (N > 0) { + const size_t copyLength = (std::min)(value.size(), N - 1); + if (copyLength > 0) { + std::memcpy(buffer.data(), value.data(), copyLength); + } + buffer[copyLength] = '\0'; + } +} + +template +std::string BufferToString(const std::array& buffer) { + return std::string(buffer.data()); +} + +std::string TrimCopy(const std::string& value) { + return ProjectFileUtils::Trim(value); +} + +std::string EscapeJsonString(const std::string& value) { + std::string escaped; + escaped.reserve(value.size()); + for (const char ch : value) { + switch (ch) { + case '\\': + escaped += "\\\\"; + break; + case '"': + escaped += "\\\""; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + escaped.push_back(ch); + break; + } + } + return escaped; +} + +struct InspectorAssetReferenceInteraction { + std::string assignedPath; + bool clearRequested = false; +}; + +InspectorAssetReferenceInteraction DrawInspectorAssetReferenceProperty( + const char* label, + const std::string& currentPath, + const char* emptyHint, + std::initializer_list supportedExtensions) { + InspectorAssetReferenceInteraction interaction; + const std::string popupTitle = std::string("Select ") + (label ? label : "Asset"); + UI::ReferencePickerOptions pickerOptions; + pickerOptions.popupTitle = popupTitle.c_str(); + pickerOptions.emptyHint = emptyHint; + pickerOptions.searchHint = "Search"; + pickerOptions.noAssetsText = "No compatible assets."; + pickerOptions.assetsTabLabel = "Assets"; + pickerOptions.showAssetsTab = true; + pickerOptions.showSceneTab = false; + pickerOptions.supportedAssetExtensions = supportedExtensions; + + UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { + UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight()); + const UI::ReferencePickerInteraction pickerInteraction = + UI::DrawReferencePickerControl( + currentPath, + ::XCEngine::Components::GameObject::INVALID_ID, + pickerOptions, + layout.controlWidth); + interaction.assignedPath = pickerInteraction.assignedAssetPath; + interaction.clearRequested = pickerInteraction.clearRequested; + return false; + }); + + return interaction; +} + +constexpr int kMaterialQueuePresetCount = 6; +constexpr int kCustomRenderQueuePresetIndex = 5; +constexpr const char* kMaterialQueuePresetLabels[kMaterialQueuePresetCount] = { + "Background", + "Geometry", + "Alpha Test", + "Transparent", + "Overlay", + "Custom" +}; +constexpr int kMaterialQueuePresetValues[kMaterialQueuePresetCount - 1] = { + static_cast(::XCEngine::Resources::MaterialRenderQueue::Background), + static_cast(::XCEngine::Resources::MaterialRenderQueue::Geometry), + static_cast(::XCEngine::Resources::MaterialRenderQueue::AlphaTest), + static_cast(::XCEngine::Resources::MaterialRenderQueue::Transparent), + static_cast(::XCEngine::Resources::MaterialRenderQueue::Overlay) +}; + +int ResolveRenderQueuePresetIndex(int renderQueue) { + for (int presetIndex = 0; presetIndex < kMaterialQueuePresetCount - 1; ++presetIndex) { + if (kMaterialQueuePresetValues[presetIndex] == renderQueue) { + return presetIndex; + } + } + return kCustomRenderQueuePresetIndex; +} + +const char* SerializeRenderQueue(int renderQueue) { + switch (renderQueue) { + case static_cast(::XCEngine::Resources::MaterialRenderQueue::Background): + return "background"; + case static_cast(::XCEngine::Resources::MaterialRenderQueue::Geometry): + return "geometry"; + case static_cast(::XCEngine::Resources::MaterialRenderQueue::AlphaTest): + return "alpha_test"; + case static_cast(::XCEngine::Resources::MaterialRenderQueue::Transparent): + return "transparent"; + case static_cast(::XCEngine::Resources::MaterialRenderQueue::Overlay): + return "overlay"; + default: + return nullptr; + } +} + +constexpr const char* kCullModeLabels[] = { "None", "Front", "Back" }; + +int ResolveCullModeIndex(::XCEngine::Resources::MaterialCullMode mode) { + switch (mode) { + case ::XCEngine::Resources::MaterialCullMode::Front: + return 1; + case ::XCEngine::Resources::MaterialCullMode::Back: + return 2; + case ::XCEngine::Resources::MaterialCullMode::None: + default: + return 0; + } +} + +::XCEngine::Resources::MaterialCullMode CullModeFromIndex(int index) { + switch (index) { + case 1: + return ::XCEngine::Resources::MaterialCullMode::Front; + case 2: + return ::XCEngine::Resources::MaterialCullMode::Back; + case 0: + default: + return ::XCEngine::Resources::MaterialCullMode::None; + } +} + +const char* SerializeCullMode(::XCEngine::Resources::MaterialCullMode mode) { + switch (mode) { + case ::XCEngine::Resources::MaterialCullMode::Front: + return "front"; + case ::XCEngine::Resources::MaterialCullMode::Back: + return "back"; + case ::XCEngine::Resources::MaterialCullMode::None: + default: + return "none"; + } +} + +constexpr const char* kComparisonFuncLabels[] = { + "Never", + "Less", + "Equal", + "Less Equal", + "Greater", + "Not Equal", + "Greater Equal", + "Always" +}; + +int ResolveComparisonFuncIndex(::XCEngine::Resources::MaterialComparisonFunc func) { + return static_cast(func); +} + +::XCEngine::Resources::MaterialComparisonFunc ComparisonFuncFromIndex(int index) { + switch (index) { + case 0: + return ::XCEngine::Resources::MaterialComparisonFunc::Never; + case 1: + return ::XCEngine::Resources::MaterialComparisonFunc::Less; + case 2: + return ::XCEngine::Resources::MaterialComparisonFunc::Equal; + case 3: + return ::XCEngine::Resources::MaterialComparisonFunc::LessEqual; + case 4: + return ::XCEngine::Resources::MaterialComparisonFunc::Greater; + case 5: + return ::XCEngine::Resources::MaterialComparisonFunc::NotEqual; + case 6: + return ::XCEngine::Resources::MaterialComparisonFunc::GreaterEqual; + case 7: + default: + return ::XCEngine::Resources::MaterialComparisonFunc::Always; + } +} + +const char* SerializeComparisonFunc(::XCEngine::Resources::MaterialComparisonFunc func) { + switch (func) { + case ::XCEngine::Resources::MaterialComparisonFunc::Never: + return "never"; + case ::XCEngine::Resources::MaterialComparisonFunc::Less: + return "less"; + case ::XCEngine::Resources::MaterialComparisonFunc::Equal: + return "equal"; + case ::XCEngine::Resources::MaterialComparisonFunc::LessEqual: + return "less_equal"; + case ::XCEngine::Resources::MaterialComparisonFunc::Greater: + return "greater"; + case ::XCEngine::Resources::MaterialComparisonFunc::NotEqual: + return "not_equal"; + case ::XCEngine::Resources::MaterialComparisonFunc::GreaterEqual: + return "greater_equal"; + case ::XCEngine::Resources::MaterialComparisonFunc::Always: + default: + return "always"; + } +} + +constexpr const char* kBlendOpLabels[] = { + "Add", + "Subtract", + "Reverse Subtract", + "Min", + "Max" +}; + +int ResolveBlendOpIndex(::XCEngine::Resources::MaterialBlendOp op) { + return static_cast(op); +} + +::XCEngine::Resources::MaterialBlendOp BlendOpFromIndex(int index) { + switch (index) { + case 1: + return ::XCEngine::Resources::MaterialBlendOp::Subtract; + case 2: + return ::XCEngine::Resources::MaterialBlendOp::ReverseSubtract; + case 3: + return ::XCEngine::Resources::MaterialBlendOp::Min; + case 4: + return ::XCEngine::Resources::MaterialBlendOp::Max; + case 0: + default: + return ::XCEngine::Resources::MaterialBlendOp::Add; + } +} + +const char* SerializeBlendOp(::XCEngine::Resources::MaterialBlendOp op) { + switch (op) { + case ::XCEngine::Resources::MaterialBlendOp::Subtract: + return "subtract"; + case ::XCEngine::Resources::MaterialBlendOp::ReverseSubtract: + return "reverse_subtract"; + case ::XCEngine::Resources::MaterialBlendOp::Min: + return "min"; + case ::XCEngine::Resources::MaterialBlendOp::Max: + return "max"; + case ::XCEngine::Resources::MaterialBlendOp::Add: + default: + return "add"; + } +} + +constexpr const char* kBlendFactorLabels[] = { + "Zero", + "One", + "Src Color", + "One Minus Src Color", + "Src Alpha", + "One Minus Src Alpha", + "Dst Alpha", + "One Minus Dst Alpha", + "Dst Color", + "One Minus Dst Color", + "Src Alpha Saturate", + "Blend Factor", + "One Minus Blend Factor", + "Src1 Color", + "One Minus Src1 Color", + "Src1 Alpha", + "One Minus Src1 Alpha" +}; + +int ResolveBlendFactorIndex(::XCEngine::Resources::MaterialBlendFactor factor) { + return static_cast(factor); +} + +::XCEngine::Resources::MaterialBlendFactor BlendFactorFromIndex(int index) { + switch (index) { + case 0: + return ::XCEngine::Resources::MaterialBlendFactor::Zero; + case 1: + return ::XCEngine::Resources::MaterialBlendFactor::One; + case 2: + return ::XCEngine::Resources::MaterialBlendFactor::SrcColor; + case 3: + return ::XCEngine::Resources::MaterialBlendFactor::InvSrcColor; + case 4: + return ::XCEngine::Resources::MaterialBlendFactor::SrcAlpha; + case 5: + return ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha; + case 6: + return ::XCEngine::Resources::MaterialBlendFactor::DstAlpha; + case 7: + return ::XCEngine::Resources::MaterialBlendFactor::InvDstAlpha; + case 8: + return ::XCEngine::Resources::MaterialBlendFactor::DstColor; + case 9: + return ::XCEngine::Resources::MaterialBlendFactor::InvDstColor; + case 10: + return ::XCEngine::Resources::MaterialBlendFactor::SrcAlphaSat; + case 11: + return ::XCEngine::Resources::MaterialBlendFactor::BlendFactor; + case 12: + return ::XCEngine::Resources::MaterialBlendFactor::InvBlendFactor; + case 13: + return ::XCEngine::Resources::MaterialBlendFactor::Src1Color; + case 14: + return ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Color; + case 15: + return ::XCEngine::Resources::MaterialBlendFactor::Src1Alpha; + case 16: + default: + return ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Alpha; + } +} + +const char* SerializeBlendFactor(::XCEngine::Resources::MaterialBlendFactor factor) { + switch (factor) { + case ::XCEngine::Resources::MaterialBlendFactor::Zero: + return "zero"; + case ::XCEngine::Resources::MaterialBlendFactor::One: + return "one"; + case ::XCEngine::Resources::MaterialBlendFactor::SrcColor: + return "src_color"; + case ::XCEngine::Resources::MaterialBlendFactor::InvSrcColor: + return "one_minus_src_color"; + case ::XCEngine::Resources::MaterialBlendFactor::SrcAlpha: + return "src_alpha"; + case ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha: + return "one_minus_src_alpha"; + case ::XCEngine::Resources::MaterialBlendFactor::DstAlpha: + return "dst_alpha"; + case ::XCEngine::Resources::MaterialBlendFactor::InvDstAlpha: + return "one_minus_dst_alpha"; + case ::XCEngine::Resources::MaterialBlendFactor::DstColor: + return "dst_color"; + case ::XCEngine::Resources::MaterialBlendFactor::InvDstColor: + return "one_minus_dst_color"; + case ::XCEngine::Resources::MaterialBlendFactor::SrcAlphaSat: + return "src_alpha_sat"; + case ::XCEngine::Resources::MaterialBlendFactor::BlendFactor: + return "blend_factor"; + case ::XCEngine::Resources::MaterialBlendFactor::InvBlendFactor: + return "one_minus_blend_factor"; + case ::XCEngine::Resources::MaterialBlendFactor::Src1Color: + return "src1_color"; + case ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Color: + return "one_minus_src1_color"; + case ::XCEngine::Resources::MaterialBlendFactor::Src1Alpha: + return "src1_alpha"; + case ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Alpha: + default: + return "one_minus_src1_alpha"; + } +} + +void ApplyMaterialStateToResource( + const InspectorPanel::MaterialAssetState& state, + ::XCEngine::Resources::Material& material) { + const std::string shaderPath = TrimCopy(BufferToString(state.shaderPath)); + if (shaderPath.empty()) { + material.SetShader(::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>()); + } else { + material.SetShader(::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Shader>(shaderPath.c_str())); + } + + material.SetShaderPass(TrimCopy(BufferToString(state.shaderPass)).c_str()); + material.SetRenderQueue(state.renderQueue); + material.SetRenderState(state.renderState); + material.ClearTags(); + for (const InspectorPanel::MaterialTagEditRow& row : state.tags) { + const std::string tagName = TrimCopy(BufferToString(row.name)); + if (tagName.empty()) { + continue; + } + material.SetTag(tagName.c_str(), TrimCopy(BufferToString(row.value)).c_str()); + } + material.RecalculateMemorySize(); +} + +std::string BuildMaterialAssetFileText(const InspectorPanel::MaterialAssetState& state) { + std::vector rootEntries; + + const std::string shaderPath = TrimCopy(BufferToString(state.shaderPath)); + if (!shaderPath.empty()) { + rootEntries.push_back(" \"shader\": \"" + EscapeJsonString(shaderPath) + "\""); + } + + const std::string shaderPass = TrimCopy(BufferToString(state.shaderPass)); + if (!shaderPass.empty()) { + rootEntries.push_back(" \"shaderPass\": \"" + EscapeJsonString(shaderPass) + "\""); + } + + const int renderQueuePreset = ResolveRenderQueuePresetIndex(state.renderQueue); + if (renderQueuePreset != kCustomRenderQueuePresetIndex) { + rootEntries.push_back( + " \"renderQueue\": \"" + std::string(SerializeRenderQueue(state.renderQueue)) + "\""); + } else { + rootEntries.push_back(" \"renderQueue\": " + std::to_string(state.renderQueue)); + } + + std::vector tagEntries; + for (const InspectorPanel::MaterialTagEditRow& row : state.tags) { + const std::string tagName = TrimCopy(BufferToString(row.name)); + if (tagName.empty()) { + continue; + } + tagEntries.push_back( + " \"" + EscapeJsonString(tagName) + "\": \"" + + EscapeJsonString(TrimCopy(BufferToString(row.value))) + "\""); + } + if (!tagEntries.empty()) { + std::string tagsObject = " \"tags\": {\n"; + for (size_t tagIndex = 0; tagIndex < tagEntries.size(); ++tagIndex) { + tagsObject += tagEntries[tagIndex]; + if (tagIndex + 1 < tagEntries.size()) { + tagsObject += ","; + } + tagsObject += "\n"; + } + tagsObject += " }"; + rootEntries.push_back(tagsObject); + } + + const ::XCEngine::Resources::MaterialRenderState& renderState = state.renderState; + std::vector renderStateEntries; + renderStateEntries.push_back( + " \"cull\": \"" + std::string(SerializeCullMode(renderState.cullMode)) + "\""); + renderStateEntries.push_back(std::string(" \"depthTest\": ") + (renderState.depthTestEnable ? "true" : "false")); + renderStateEntries.push_back(std::string(" \"depthWrite\": ") + (renderState.depthWriteEnable ? "true" : "false")); + renderStateEntries.push_back( + " \"depthFunc\": \"" + std::string(SerializeComparisonFunc(renderState.depthFunc)) + "\""); + renderStateEntries.push_back(std::string(" \"blendEnable\": ") + (renderState.blendEnable ? "true" : "false")); + renderStateEntries.push_back( + " \"srcBlend\": \"" + std::string(SerializeBlendFactor(renderState.srcBlend)) + "\""); + renderStateEntries.push_back( + " \"dstBlend\": \"" + std::string(SerializeBlendFactor(renderState.dstBlend)) + "\""); + renderStateEntries.push_back( + " \"srcBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.srcBlendAlpha)) + "\""); + renderStateEntries.push_back( + " \"dstBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.dstBlendAlpha)) + "\""); + renderStateEntries.push_back( + " \"blendOp\": \"" + std::string(SerializeBlendOp(renderState.blendOp)) + "\""); + renderStateEntries.push_back( + " \"blendOpAlpha\": \"" + std::string(SerializeBlendOp(renderState.blendOpAlpha)) + "\""); + renderStateEntries.push_back(" \"colorWriteMask\": " + std::to_string(renderState.colorWriteMask)); + + std::string renderStateObject = " \"renderState\": {\n"; + for (size_t renderStateIndex = 0; renderStateIndex < renderStateEntries.size(); ++renderStateIndex) { + renderStateObject += renderStateEntries[renderStateIndex]; + if (renderStateIndex + 1 < renderStateEntries.size()) { + renderStateObject += ","; + } + renderStateObject += "\n"; + } + renderStateObject += " }"; + rootEntries.push_back(renderStateObject); + + std::string json = "{\n"; + for (size_t entryIndex = 0; entryIndex < rootEntries.size(); ++entryIndex) { + json += rootEntries[entryIndex]; + if (entryIndex + 1 < rootEntries.size()) { + json += ","; + } + json += "\n"; + } + json += "}\n"; + return json; +} + } // namespace InspectorPanel::InspectorPanel() : Panel("Inspector") {} @@ -76,6 +580,145 @@ void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) { } } +void InspectorPanel::SetSubjectMode(SubjectMode mode) { + if (m_subjectMode == mode) { + return; + } + + if (m_subjectMode == SubjectMode::MaterialAsset && m_materialAssetState.dirty) { + SaveMaterialAsset(); + } + + m_subjectMode = mode; + if (mode != SubjectMode::MaterialAsset) { + ClearMaterialAsset(); + } +} + +void InspectorPanel::ClearMaterialAsset() { + m_selectedMaterial.Reset(); + m_materialAssetState.Reset(); +} + +void InspectorPanel::ReloadMaterialAsset() { + if (!m_selectedAssetItem || !m_context) { + ClearMaterialAsset(); + return; + } + + m_selectedMaterial.Reset(); + m_materialAssetState.Reset(); + m_materialAssetState.assetFullPath = m_selectedAssetItem->fullPath; + m_materialAssetState.assetName = m_selectedAssetItem->name; + m_materialAssetState.assetPath = ProjectFileUtils::MakeProjectRelativePath( + m_context->GetProjectPath(), + m_selectedAssetItem->fullPath); + + if (m_materialAssetState.assetPath.empty()) { + m_materialAssetState.errorMessage = "Failed to resolve material asset path."; + return; + } + + m_selectedMaterial = ::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Material>( + m_materialAssetState.assetPath.c_str()); + if (!m_selectedMaterial.IsValid()) { + m_materialAssetState.errorMessage = "Material file is empty or invalid. Showing default values until you save it."; + m_materialAssetState.loaded = true; + m_materialAssetState.dirty = false; + return; + } + + ::XCEngine::Resources::Material* material = m_selectedMaterial.Get(); + if (material == nullptr) { + m_materialAssetState.errorMessage = "Material resource is unavailable. Showing default values until you save it."; + m_materialAssetState.loaded = true; + m_materialAssetState.dirty = false; + return; + } + + if (::XCEngine::Resources::Shader* shader = material->GetShader()) { + CopyToCharBuffer(std::string(shader->GetPath().CStr()), m_materialAssetState.shaderPath); + } + CopyToCharBuffer(std::string(material->GetShaderPass().CStr()), m_materialAssetState.shaderPass); + m_materialAssetState.renderQueue = material->GetRenderQueue(); + m_materialAssetState.renderState = material->GetRenderState(); + m_materialAssetState.tags.reserve(material->GetTagCount()); + for (Core::uint32 tagIndex = 0; tagIndex < material->GetTagCount(); ++tagIndex) { + MaterialTagEditRow row; + CopyToCharBuffer(std::string(material->GetTagName(tagIndex).CStr()), row.name); + CopyToCharBuffer(std::string(material->GetTagValue(tagIndex).CStr()), row.value); + m_materialAssetState.tags.push_back(row); + } + m_materialAssetState.loaded = true; + m_materialAssetState.dirty = false; +} + +void InspectorPanel::InspectMaterialAsset(const AssetItemPtr& item) { + if (!item) { + SetSubjectMode(SubjectMode::None); + return; + } + + const std::string assetPath = m_context + ? ProjectFileUtils::MakeProjectRelativePath(m_context->GetProjectPath(), item->fullPath) + : std::string(); + + if (m_subjectMode == SubjectMode::MaterialAsset && + m_materialAssetState.assetPath == assetPath && + m_selectedAssetItem && + m_selectedAssetItem->fullPath == item->fullPath) { + m_selectedAssetItem = item; + return; + } + + if (m_subjectMode == SubjectMode::MaterialAsset && m_materialAssetState.dirty) { + SaveMaterialAsset(); + } + + m_subjectMode = SubjectMode::MaterialAsset; + m_selectedAssetItem = item; + ReloadMaterialAsset(); +} + +void InspectorPanel::SyncSubject() { + if (!m_context) { + return; + } + + const EditorActionRoute activeRoute = m_context->GetActiveActionRoute(); + if (activeRoute != EditorActionRoute::None) { + m_lastExplicitRoute = activeRoute; + } + + if (m_lastExplicitRoute == EditorActionRoute::Project) { + AssetItemPtr selectedAsset = m_context->GetProjectManager().GetSelectedItem(); + if (selectedAsset && selectedAsset->type == "Material") { + InspectMaterialAsset(selectedAsset); + return; + } + + if (selectedAsset) { + SetSubjectMode(SubjectMode::UnsupportedAsset); + m_selectedAssetItem = selectedAsset; + return; + } + + SetSubjectMode(SubjectMode::None); + m_selectedAssetItem.reset(); + return; + } + + if (m_lastExplicitRoute == EditorActionRoute::Hierarchy || m_selectedEntityId != 0) { + auto* gameObject = m_context->GetSceneManager().GetEntity(m_selectedEntityId); + if (gameObject != nullptr) { + SetSubjectMode(SubjectMode::GameObject); + return; + } + } + + SetSubjectMode(SubjectMode::None); +} + void InspectorPanel::Render() { ImGui::PushStyleColor(ImGuiCol_WindowBg, UI::HierarchyInspectorPanelBackgroundColor()); ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::HierarchyInspectorPanelBackgroundColor()); @@ -86,21 +729,28 @@ void InspectorPanel::Render() { return; } + SyncSubject(); Actions::ObserveInactiveActionRoute(*m_context); - if (m_selectedEntityId) { - auto& sceneManager = m_context->GetSceneManager(); - auto* gameObject = sceneManager.GetEntity(m_selectedEntityId); - if (gameObject) { - RenderGameObject(gameObject); - } else { - RenderEmptyState("Object not found"); + switch (m_subjectMode) { + case SubjectMode::GameObject: { + auto* gameObject = m_context->GetSceneManager().GetEntity(m_selectedEntityId); + if (gameObject) { + RenderGameObject(gameObject); + } else { + RenderEmptyState("Object not found"); + } + break; } - } else { - UI::PanelContentScope content( - "InspectorEmpty", - UI::InspectorPanelContentPadding(), - ImGuiChildFlags_None); + case SubjectMode::MaterialAsset: + RenderMaterialAsset(); + break; + case SubjectMode::UnsupportedAsset: + RenderUnsupportedAsset(); + break; + case SubjectMode::None: + default: + break; } } ImGui::PopStyleColor(2); @@ -140,6 +790,310 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb } } +bool InspectorPanel::SaveMaterialAsset() { + if (!m_context || !m_selectedAssetItem || m_materialAssetState.assetFullPath.empty()) { + return false; + } + + try { + const std::filesystem::path materialPath(Platform::Utf8ToWide(m_materialAssetState.assetFullPath)); + std::ofstream output(materialPath, std::ios::out | std::ios::trunc); + if (!output.is_open()) { + m_materialAssetState.errorMessage = "Failed to open material file for writing."; + return false; + } + + const std::string fileText = BuildMaterialAssetFileText(m_materialAssetState); + output.write(fileText.data(), static_cast(fileText.size())); + output.close(); + if (!output.good()) { + m_materialAssetState.errorMessage = "Failed to write material file."; + return false; + } + + if (!m_selectedMaterial.IsValid()) { + m_selectedMaterial = ::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Material>( + m_materialAssetState.assetPath.c_str()); + } + if (m_selectedMaterial.IsValid()) { + ApplyMaterialStateToResource(m_materialAssetState, *m_selectedMaterial.Get()); + } + + m_materialAssetState.errorMessage.clear(); + m_materialAssetState.dirty = false; + return true; + } catch (...) { + m_materialAssetState.errorMessage = "Saving material asset failed."; + return false; + } +} + +void InspectorPanel::RenderMaterialAsset() { + ImGuiStyle& style = ImGui::GetStyle(); + UI::PanelContentScope content( + "InspectorMaterialAsset", + UI::InspectorPanelContentPadding(), + ImGuiChildFlags_None, + ImGuiWindowFlags_None, + true, + ImVec2(style.ItemSpacing.x, 0.0f)); + if (!content.IsOpen()) { + return; + } + + ImGui::TextUnformatted(m_materialAssetState.assetName.empty() ? "Material" : m_materialAssetState.assetName.c_str()); + if (!m_materialAssetState.loaded && !m_materialAssetState.errorMessage.empty()) { + ImGui::Spacing(); + UI::DrawHintText(m_materialAssetState.errorMessage.c_str()); + return; + } + + if (!m_materialAssetState.errorMessage.empty()) { + UI::DrawHintText(m_materialAssetState.errorMessage.c_str()); + } + + const UI::ComponentSectionResult materialSection = UI::BeginComponentSection( + "MaterialAssetMain", + "Material"); + if (materialSection.open) { + ImGui::Indent(materialSection.contentIndent); + + bool changed = false; + const InspectorAssetReferenceInteraction shaderInteraction = + DrawInspectorAssetReferenceProperty( + "Shader", + BufferToString(m_materialAssetState.shaderPath), + "Select Shader Asset", + { ".shader", ".hlsl", ".glsl", ".vert", ".frag", ".comp" }); + if (shaderInteraction.clearRequested) { + CopyToCharBuffer(std::string(), m_materialAssetState.shaderPath); + changed = true; + } else if (!shaderInteraction.assignedPath.empty() && + shaderInteraction.assignedPath != BufferToString(m_materialAssetState.shaderPath)) { + CopyToCharBuffer(shaderInteraction.assignedPath, m_materialAssetState.shaderPath); + changed = true; + } + + changed = UI::DrawPropertyRow("Pass", UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { + UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight()); + ImGui::SetNextItemWidth(layout.controlWidth); + return ImGui::InputText("##ShaderPass", m_materialAssetState.shaderPass.data(), m_materialAssetState.shaderPass.size()); + }) || changed; + + int renderQueuePreset = ResolveRenderQueuePresetIndex(m_materialAssetState.renderQueue); + const int newRenderQueuePreset = UI::DrawPropertyCombo( + "Render Queue", + renderQueuePreset, + kMaterialQueuePresetLabels, + kMaterialQueuePresetCount); + if (newRenderQueuePreset != renderQueuePreset) { + if (newRenderQueuePreset >= 0 && newRenderQueuePreset < kCustomRenderQueuePresetIndex) { + m_materialAssetState.renderQueue = kMaterialQueuePresetValues[newRenderQueuePreset]; + } + changed = true; + renderQueuePreset = newRenderQueuePreset; + } + if (renderQueuePreset == kCustomRenderQueuePresetIndex) { + changed = UI::DrawPropertyInt("Queue Value", m_materialAssetState.renderQueue, 1, -100000, 100000) || changed; + } + + if (changed) { + m_materialAssetState.dirty = true; + SaveMaterialAsset(); + } + + UI::EndComponentSection(materialSection); + } + + const UI::ComponentSectionResult renderStateSection = UI::BeginComponentSection( + "MaterialAssetRenderState", + "Render State"); + if (renderStateSection.open) { + ImGui::Indent(renderStateSection.contentIndent); + + bool changed = false; + int cullModeIndex = ResolveCullModeIndex(m_materialAssetState.renderState.cullMode); + const int newCullModeIndex = UI::DrawPropertyCombo( + "Cull", + cullModeIndex, + kCullModeLabels, + IM_ARRAYSIZE(kCullModeLabels)); + if (newCullModeIndex != cullModeIndex) { + m_materialAssetState.renderState.cullMode = CullModeFromIndex(newCullModeIndex); + changed = true; + } + + changed = UI::DrawPropertyBool("Depth Test", m_materialAssetState.renderState.depthTestEnable) || changed; + changed = UI::DrawPropertyBool("Depth Write", m_materialAssetState.renderState.depthWriteEnable) || changed; + + int depthFuncIndex = ResolveComparisonFuncIndex(m_materialAssetState.renderState.depthFunc); + const int newDepthFuncIndex = UI::DrawPropertyCombo( + "Depth Func", + depthFuncIndex, + kComparisonFuncLabels, + IM_ARRAYSIZE(kComparisonFuncLabels)); + if (newDepthFuncIndex != depthFuncIndex) { + m_materialAssetState.renderState.depthFunc = ComparisonFuncFromIndex(newDepthFuncIndex); + changed = true; + } + + changed = UI::DrawPropertyBool("Blend", m_materialAssetState.renderState.blendEnable) || changed; + + int srcBlendIndex = ResolveBlendFactorIndex(m_materialAssetState.renderState.srcBlend); + const int newSrcBlendIndex = UI::DrawPropertyCombo( + "Src Blend", + srcBlendIndex, + kBlendFactorLabels, + IM_ARRAYSIZE(kBlendFactorLabels)); + if (newSrcBlendIndex != srcBlendIndex) { + m_materialAssetState.renderState.srcBlend = BlendFactorFromIndex(newSrcBlendIndex); + changed = true; + } + + int dstBlendIndex = ResolveBlendFactorIndex(m_materialAssetState.renderState.dstBlend); + const int newDstBlendIndex = UI::DrawPropertyCombo( + "Dst Blend", + dstBlendIndex, + kBlendFactorLabels, + IM_ARRAYSIZE(kBlendFactorLabels)); + if (newDstBlendIndex != dstBlendIndex) { + m_materialAssetState.renderState.dstBlend = BlendFactorFromIndex(newDstBlendIndex); + changed = true; + } + + int srcBlendAlphaIndex = ResolveBlendFactorIndex(m_materialAssetState.renderState.srcBlendAlpha); + const int newSrcBlendAlphaIndex = UI::DrawPropertyCombo( + "Src Blend Alpha", + srcBlendAlphaIndex, + kBlendFactorLabels, + IM_ARRAYSIZE(kBlendFactorLabels)); + if (newSrcBlendAlphaIndex != srcBlendAlphaIndex) { + m_materialAssetState.renderState.srcBlendAlpha = BlendFactorFromIndex(newSrcBlendAlphaIndex); + changed = true; + } + + int dstBlendAlphaIndex = ResolveBlendFactorIndex(m_materialAssetState.renderState.dstBlendAlpha); + const int newDstBlendAlphaIndex = UI::DrawPropertyCombo( + "Dst Blend Alpha", + dstBlendAlphaIndex, + kBlendFactorLabels, + IM_ARRAYSIZE(kBlendFactorLabels)); + if (newDstBlendAlphaIndex != dstBlendAlphaIndex) { + m_materialAssetState.renderState.dstBlendAlpha = BlendFactorFromIndex(newDstBlendAlphaIndex); + changed = true; + } + + int blendOpIndex = ResolveBlendOpIndex(m_materialAssetState.renderState.blendOp); + const int newBlendOpIndex = UI::DrawPropertyCombo( + "Blend Op", + blendOpIndex, + kBlendOpLabels, + IM_ARRAYSIZE(kBlendOpLabels)); + if (newBlendOpIndex != blendOpIndex) { + m_materialAssetState.renderState.blendOp = BlendOpFromIndex(newBlendOpIndex); + changed = true; + } + + int blendOpAlphaIndex = ResolveBlendOpIndex(m_materialAssetState.renderState.blendOpAlpha); + const int newBlendOpAlphaIndex = UI::DrawPropertyCombo( + "Blend Op Alpha", + blendOpAlphaIndex, + kBlendOpLabels, + IM_ARRAYSIZE(kBlendOpLabels)); + if (newBlendOpAlphaIndex != blendOpAlphaIndex) { + m_materialAssetState.renderState.blendOpAlpha = BlendOpFromIndex(newBlendOpAlphaIndex); + changed = true; + } + + int colorWriteMask = static_cast(m_materialAssetState.renderState.colorWriteMask); + if (UI::DrawPropertyInt("Color Write Mask", colorWriteMask, 1, 0, 15)) { + m_materialAssetState.renderState.colorWriteMask = + static_cast(std::clamp(colorWriteMask, 0, 15)); + changed = true; + } + + if (changed) { + m_materialAssetState.dirty = true; + SaveMaterialAsset(); + } + + UI::EndComponentSection(renderStateSection); + } + + const UI::ComponentSectionResult tagSection = UI::BeginComponentSection( + "MaterialAssetTags", + "Tags"); + if (tagSection.open) { + ImGui::Indent(tagSection.contentIndent); + + bool changed = false; + size_t removeTagIndex = static_cast(-1); + for (size_t tagIndex = 0; tagIndex < m_materialAssetState.tags.size(); ++tagIndex) { + MaterialTagEditRow& row = m_materialAssetState.tags[tagIndex]; + ImGui::PushID(static_cast(tagIndex)); + const bool rowChanged = UI::DrawPropertyRow( + tagIndex == 0 ? "Entries" : "", + UI::InspectorPropertyLayout(), + [&](const UI::PropertyLayoutMetrics& layout) { + const float buttonWidth = 26.0f; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float fieldWidth = + (std::max)((layout.controlWidth - buttonWidth - spacing * 2.0f) * 0.5f, 40.0f); + UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight()); + ImGui::SetNextItemWidth(fieldWidth); + bool localChanged = ImGui::InputText("##TagName", row.name.data(), row.name.size()); + ImGui::SameLine(0.0f, spacing); + ImGui::SetNextItemWidth((std::max)(layout.controlWidth - fieldWidth - buttonWidth - spacing * 2.0f, 1.0f)); + localChanged = ImGui::InputText("##TagValue", row.value.data(), row.value.size()) || localChanged; + ImGui::SameLine(0.0f, spacing); + if (ImGui::Button("-", ImVec2(buttonWidth, 0.0f))) { + removeTagIndex = tagIndex; + } + return localChanged; + }); + changed = rowChanged || changed; + ImGui::PopID(); + } + + if (removeTagIndex != static_cast(-1) && removeTagIndex < m_materialAssetState.tags.size()) { + m_materialAssetState.tags.erase( + m_materialAssetState.tags.begin() + static_cast(removeTagIndex)); + changed = true; + } + + if (UI::InspectorActionButton("Add Tag", ImVec2(-1.0f, 0.0f))) { + m_materialAssetState.tags.push_back(MaterialTagEditRow{}); + changed = true; + } + + if (changed) { + m_materialAssetState.dirty = true; + SaveMaterialAsset(); + } + + UI::EndComponentSection(tagSection); + } +} + +void InspectorPanel::RenderUnsupportedAsset() { + UI::PanelContentScope content( + "InspectorUnsupportedAsset", + UI::InspectorPanelContentPadding(), + ImGuiChildFlags_None); + if (!content.IsOpen()) { + return; + } + + if (m_selectedAssetItem) { + ImGui::TextUnformatted(m_selectedAssetItem->name.c_str()); + UI::DrawHintText(m_selectedAssetItem->type.c_str()); + UI::DrawHintText("This asset type does not have a dedicated inspector yet."); + return; + } + + UI::DrawEmptyState("No asset selected"); +} + void InspectorPanel::RenderEmptyState(const char* title, const char* subtitle) { UI::PanelContentScope content( "InspectorEmptyState", @@ -156,13 +1110,13 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen if (!component) return; IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component); - const std::string name = component->GetName(); + const std::string name = editor ? std::string(editor->GetDisplayName()) : component->GetName(); const UI::ComponentSectionResult section = UI::BeginComponentSection( - (void*)typeid(*component).hash_code(), + component, name.c_str()); - if (UI::BeginContextMenuForLastItem("##InspectorComponentContext")) { + if (UI::BeginContextMenuForLastItem()) { DrawInspectorComponentContextMenu(*m_context, component, gameObject, m_deferredContextAction); UI::EndContextMenu(); }