feat: add mesh component editors and scene hierarchy serialization

This commit is contained in:
2026-03-31 21:25:59 +08:00
parent b92f9bfa70
commit be15bc2fc4
7 changed files with 341 additions and 7 deletions

View File

@@ -0,0 +1,111 @@
#pragma once
#include "Application.h"
#include "Actions/ProjectActionRouter.h"
#include "UI/UI.h"
#include "Utils/ProjectFileUtils.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <initializer_list>
#include <string>
namespace XCEngine {
namespace Editor {
namespace ComponentEditorAssetUI {
struct AssetReferenceInteraction {
std::string assignedPath;
bool clearRequested = false;
};
inline std::string ToProjectRelativeAssetPath(const std::string& assetPath) {
if (assetPath.empty()) {
return {};
}
const std::string& projectPath = Application::Get().GetEditorContext().GetProjectPath();
if (projectPath.empty()) {
return assetPath;
}
return ProjectFileUtils::MakeProjectRelativePath(projectPath, assetPath);
}
inline bool HasSupportedExtension(
const std::string& path,
std::initializer_list<const char*> supportedExtensions) {
std::string extension = std::filesystem::path(path).extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
for (const char* supportedExtension : supportedExtensions) {
if (supportedExtension != nullptr && extension == supportedExtension) {
return true;
}
}
return false;
}
inline AssetReferenceInteraction DrawAssetReferenceProperty(
const char* label,
const std::string& currentPath,
const char* emptyHint,
std::initializer_list<const char*> supportedExtensions) {
AssetReferenceInteraction interaction;
UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) {
constexpr float kClearButtonWidth = 52.0f;
std::array<char, 512> buffer{};
if (!currentPath.empty()) {
strncpy_s(buffer.data(), buffer.size(), currentPath.c_str(), _TRUNCATE);
}
const float spacing = ImGui::GetStyle().ItemSpacing.x;
const float fieldWidth = (std::max)(layout.controlWidth - kClearButtonWidth - spacing, 1.0f);
ImGui::SetNextItemWidth(fieldWidth);
ImGui::InputTextWithHint(
"##AssetPath",
emptyHint,
buffer.data(),
buffer.size(),
ImGuiInputTextFlags_ReadOnly);
if (ImGui::IsItemHovered() && !currentPath.empty()) {
ImGui::SetTooltip("%s", currentPath.c_str());
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
Actions::ProjectAssetPayloadType(),
ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) {
if (payload->Data != nullptr) {
const std::string droppedPath(static_cast<const char*>(payload->Data));
if (HasSupportedExtension(droppedPath, supportedExtensions)) {
interaction.assignedPath = ToProjectRelativeAssetPath(droppedPath);
}
}
}
ImGui::EndDragDropTarget();
}
ImGui::SameLine(0.0f, spacing);
ImGui::BeginDisabled(currentPath.empty());
interaction.clearRequested = UI::InspectorActionButton("Clear", ImVec2(kClearButtonWidth, 0.0f));
ImGui::EndDisabled();
return false;
});
return interaction;
}
} // namespace ComponentEditorAssetUI
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,6 +2,8 @@
#include "ComponentEditors/CameraComponentEditor.h"
#include "ComponentEditors/LightComponentEditor.h"
#include "ComponentEditors/MeshFilterComponentEditor.h"
#include "ComponentEditors/MeshRendererComponentEditor.h"
#include "ComponentEditors/TransformComponentEditor.h"
namespace XCEngine {
@@ -16,6 +18,8 @@ ComponentEditorRegistry::ComponentEditorRegistry() {
RegisterEditor(std::make_unique<TransformComponentEditor>());
RegisterEditor(std::make_unique<CameraComponentEditor>());
RegisterEditor(std::make_unique<LightComponentEditor>());
RegisterEditor(std::make_unique<MeshFilterComponentEditor>());
RegisterEditor(std::make_unique<MeshRendererComponentEditor>());
}
void ComponentEditorRegistry::RegisterEditor(std::unique_ptr<IComponentEditor> editor) {

View File

@@ -0,0 +1,77 @@
#pragma once
#include "AssetReferenceEditorUtils.h"
#include "IComponentEditor.h"
#include "Core/IUndoManager.h"
#include "UI/UI.h"
#include <XCEngine/Components/MeshFilterComponent.h>
namespace XCEngine {
namespace Editor {
class MeshFilterComponentEditor : public IComponentEditor {
public:
const char* GetComponentTypeName() const override {
return "MeshFilter";
}
const char* GetDisplayName() const override {
return "Mesh Filter";
}
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
auto* meshFilter = dynamic_cast<::XCEngine::Components::MeshFilterComponent*>(component);
if (!meshFilter) {
return false;
}
constexpr const char* kUndoLabel = "Modify Mesh Filter";
const ComponentEditorAssetUI::AssetReferenceInteraction interaction =
ComponentEditorAssetUI::DrawAssetReferenceProperty(
"Mesh",
meshFilter->GetMeshPath(),
"Drop Model Asset",
{ ".fbx", ".obj", ".gltf", ".glb" });
bool changed = false;
changed |= UI::ApplyPropertyChange(
interaction.clearRequested && !meshFilter->GetMeshPath().empty(),
undoManager,
kUndoLabel,
[&]() {
meshFilter->ClearMesh();
});
changed |= UI::ApplyPropertyChange(
!interaction.assignedPath.empty() && interaction.assignedPath != meshFilter->GetMeshPath(),
undoManager,
kUndoLabel,
[&]() {
meshFilter->SetMeshPath(interaction.assignedPath);
});
return changed;
}
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject && !gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
}
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
if (!gameObject) {
return "Invalid";
}
return gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>()
? "Already Added"
: nullptr;
}
bool CanRemove(::XCEngine::Components::Component* component) const override {
return CanEdit(component);
}
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,116 @@
#pragma once
#include "AssetReferenceEditorUtils.h"
#include "IComponentEditor.h"
#include "Core/IUndoManager.h"
#include "UI/UI.h"
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <algorithm>
#include <string>
namespace XCEngine {
namespace Editor {
class MeshRendererComponentEditor : public IComponentEditor {
public:
const char* GetComponentTypeName() const override {
return "MeshRenderer";
}
const char* GetDisplayName() const override {
return "Mesh Renderer";
}
bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override {
auto* meshRenderer = dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(component);
if (!meshRenderer) {
return false;
}
constexpr const char* kUndoLabel = "Modify Mesh Renderer";
bool changed = false;
const size_t slotCount = GetVisibleMaterialSlotCount(meshRenderer);
for (size_t slotIndex = 0; slotIndex < slotCount; ++slotIndex) {
const std::string label = "Material " + std::to_string(slotIndex);
const std::string& currentPath = meshRenderer->GetMaterialPath(slotIndex);
const ComponentEditorAssetUI::AssetReferenceInteraction interaction =
ComponentEditorAssetUI::DrawAssetReferenceProperty(
label.c_str(),
currentPath,
"Drop Material Asset",
{ ".mat" });
changed |= UI::ApplyPropertyChange(
interaction.clearRequested && !currentPath.empty(),
undoManager,
kUndoLabel,
[meshRenderer, slotIndex]() {
meshRenderer->SetMaterialPath(slotIndex, std::string());
});
changed |= UI::ApplyPropertyChange(
!interaction.assignedPath.empty() && interaction.assignedPath != currentPath,
undoManager,
kUndoLabel,
[meshRenderer, slotIndex, assignedPath = interaction.assignedPath]() {
meshRenderer->SetMaterialPath(slotIndex, assignedPath);
});
}
return changed;
}
bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override {
return gameObject && !gameObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
}
const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override {
if (!gameObject) {
return "Invalid";
}
return gameObject->GetComponent<::XCEngine::Components::MeshRendererComponent>()
? "Already Added"
: nullptr;
}
bool CanRemove(::XCEngine::Components::Component* component) const override {
return CanEdit(component);
}
private:
static size_t GetVisibleMaterialSlotCount(::XCEngine::Components::MeshRendererComponent* meshRenderer) {
size_t slotCount = (std::max)(static_cast<size_t>(1), meshRenderer->GetMaterialCount());
::XCEngine::Components::GameObject* gameObject = meshRenderer->GetGameObject();
if (!gameObject) {
return slotCount;
}
auto* meshFilter = gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
if (!meshFilter) {
return slotCount;
}
::XCEngine::Resources::Mesh* mesh = meshFilter->GetMesh();
if (!mesh || !mesh->IsValid()) {
return slotCount;
}
slotCount = (std::max)(slotCount, static_cast<size_t>(mesh->GetMaterials().Size()));
const auto& sections = mesh->GetSections();
for (size_t sectionIndex = 0; sectionIndex < sections.Size(); ++sectionIndex) {
slotCount = (std::max)(
slotCount,
static_cast<size_t>(sections[sectionIndex].materialID) + 1u);
}
return slotCount;
}
};
} // namespace Editor
} // namespace XCEngine