feat: add mesh component editors and scene hierarchy serialization
This commit is contained in:
111
editor/src/ComponentEditors/AssetReferenceEditorUtils.h
Normal file
111
editor/src/ComponentEditors/AssetReferenceEditorUtils.h
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
77
editor/src/ComponentEditors/MeshFilterComponentEditor.h
Normal file
77
editor/src/ComponentEditors/MeshFilterComponentEditor.h
Normal 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
|
||||
116
editor/src/ComponentEditors/MeshRendererComponentEditor.h
Normal file
116
editor/src/ComponentEditors/MeshRendererComponentEditor.h
Normal 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
|
||||
Reference in New Issue
Block a user