commit 8d6e6bc0dab8cb51c393ec0825a520effdc5d80a Author: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Thu Mar 19 10:49:58 2026 +0800 Initial MetaCore scaffold and editor shell integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2996139 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +build/ +vcpkg_installed/ +downloads/ +SandboxProject*/ +TestProject/ +imgui.ini +*.txt +*.log +*.obj +*.pdb +*.ilk +*.lib +*.exe +*.vcxproj* +*.sln +.vs/ +CMakeUserPresets.json diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..8ff90ed --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,85 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 26, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Visual Studio 17 2022", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "D:/vcpkg/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "20", + "METACORE_BUILD_TESTS": "ON", + "METACORE_ENABLE_PANDA3D": "OFF", + "METACORE_ENABLE_IMGUI": "OFF", + "METACORE_ENABLE_IMGUIZMO": "OFF", + "METACORE_ENABLE_RMLUI": "OFF" + } + }, + { + "name": "vs2022-debug", + "inherits": "base", + "displayName": "VS2022 Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "vs2022-release", + "inherits": "base", + "displayName": "VS2022 Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "vs2022-integration-debug", + "inherits": "base", + "displayName": "VS2022 Integration Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "METACORE_ENABLE_PANDA3D": "ON", + "METACORE_ENABLE_IMGUI": "ON", + "METACORE_ENABLE_IMGUIZMO": "ON", + "METACORE_ENABLE_RMLUI": "ON" + } + } + ], + "buildPresets": [ + { + "name": "build-debug", + "configurePreset": "vs2022-debug" + }, + { + "name": "build-release", + "configurePreset": "vs2022-release" + }, + { + "name": "build-integration-debug", + "configurePreset": "vs2022-integration-debug" + } + ], + "testPresets": [ + { + "name": "test-debug", + "configurePreset": "vs2022-debug", + "configuration": "Debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "test-integration-debug", + "configurePreset": "vs2022-integration-debug", + "configuration": "Debug", + "output": { + "outputOnFailure": true + } + } + ] +} diff --git a/MetaCore/engine/assets/AssetDatabase.cpp b/MetaCore/engine/assets/AssetDatabase.cpp new file mode 100644 index 0000000..9e19f7f --- /dev/null +++ b/MetaCore/engine/assets/AssetDatabase.cpp @@ -0,0 +1,93 @@ +#include "MetaCore/engine/assets/AssetDatabase.h" + +#include "MetaCore/foundation/FileSystem.h" +#include "MetaCore/foundation/Guid.h" +#include "MetaCore/foundation/Json.h" + +namespace metacore::engine::assets { + +using foundation::JsonValue; + +std::map& AssetDatabase::records() { + return records_; +} + +const std::map& AssetDatabase::records() const { + return records_; +} + +bool AssetDatabase::save(const std::filesystem::path& asset_db_file) const { + JsonValue::object_type root; + JsonValue::array_type records_json; + for (const auto& [id, record] : records_) { + JsonValue::object_type node; + node["id"] = record.id; + node["type"] = record.type; + node["relative_path"] = record.relative_path; + records_json.emplace_back(JsonValue(std::move(node))); + } + root["records"] = JsonValue(std::move(records_json)); + return foundation::write_text_file(asset_db_file, JsonValue(std::move(root)).stringify(2)); +} + +bool AssetDatabase::load(const std::filesystem::path& asset_db_file) { + const auto text = foundation::read_text_file(asset_db_file); + if (!text.has_value()) { + return false; + } + records_.clear(); + const auto root = JsonValue::parse(*text); + for (const auto& node : root["records"].as_array()) { + AssetRecord record; + record.id = node["id"].as_string(); + record.type = node["type"].as_string(); + record.relative_path = node["relative_path"].as_string(); + records_[record.id] = record; + } + return true; +} + +void AssetService::ensure_default_asset_tree(const std::filesystem::path& project_root) const { + foundation::ensure_directory(project_root / "Assets" / "Models"); + foundation::ensure_directory(project_root / "Assets" / "Textures"); + foundation::ensure_directory(project_root / "Assets" / "Audio"); + foundation::ensure_directory(project_root / "Assets" / "UI"); + foundation::ensure_directory(project_root / "Assets" / "Materials"); +} + +std::string AssetService::ensure_asset_record( + AssetDatabase& asset_database, + const std::string& type, + const std::filesystem::path& relative_path +) const { + for (const auto& [id, record] : asset_database.records()) { + if (record.relative_path == relative_path.generic_string()) { + return id; + } + } + + AssetRecord record; + record.id = foundation::generate_guid(); + record.type = type; + record.relative_path = relative_path.generic_string(); + asset_database.records()[record.id] = record; + return record.id; +} + +bool AssetService::write_meta_file( + const std::filesystem::path& project_root, + const std::filesystem::path& relative_path, + const std::string& asset_id, + const std::string& type +) const { + JsonValue::object_type root; + root["id"] = asset_id; + root["type"] = type; + root["relative_path"] = relative_path.generic_string(); + return foundation::write_text_file( + project_root / (relative_path.generic_string() + ".meta"), + JsonValue(std::move(root)).stringify(2) + ); +} + +} // namespace metacore::engine::assets diff --git a/MetaCore/engine/assets/AssetDatabase.h b/MetaCore/engine/assets/AssetDatabase.h new file mode 100644 index 0000000..7bf79aa --- /dev/null +++ b/MetaCore/engine/assets/AssetDatabase.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace metacore::engine::assets { + +struct AssetRecord { + std::string id; + std::string type; + std::string relative_path; +}; + +class AssetDatabase { +public: + std::map& records(); + const std::map& records() const; + + bool save(const std::filesystem::path& asset_db_file) const; + bool load(const std::filesystem::path& asset_db_file); + +private: + std::map records_; +}; + +class AssetService { +public: + void ensure_default_asset_tree(const std::filesystem::path& project_root) const; + std::string ensure_asset_record( + AssetDatabase& asset_database, + const std::string& type, + const std::filesystem::path& relative_path + ) const; + bool write_meta_file( + const std::filesystem::path& project_root, + const std::filesystem::path& relative_path, + const std::string& asset_id, + const std::string& type + ) const; +}; + +} // namespace metacore::engine::assets diff --git a/MetaCore/engine/editor_core/CommandService.cpp b/MetaCore/engine/editor_core/CommandService.cpp new file mode 100644 index 0000000..5470909 --- /dev/null +++ b/MetaCore/engine/editor_core/CommandService.cpp @@ -0,0 +1,45 @@ +#include "MetaCore/engine/editor_core/CommandService.h" + +namespace metacore::engine::editor_core { + +void CommandService::execute(Command command) { + if (command.execute) { + command.execute(); + } + undo_stack_.push_back(std::move(command)); + redo_stack_.clear(); +} + +bool CommandService::can_undo() const { + return !undo_stack_.empty(); +} + +bool CommandService::can_redo() const { + return !redo_stack_.empty(); +} + +void CommandService::undo() { + if (undo_stack_.empty()) { + return; + } + auto command = std::move(undo_stack_.back()); + undo_stack_.pop_back(); + if (command.undo) { + command.undo(); + } + redo_stack_.push_back(std::move(command)); +} + +void CommandService::redo() { + if (redo_stack_.empty()) { + return; + } + auto command = std::move(redo_stack_.back()); + redo_stack_.pop_back(); + if (command.execute) { + command.execute(); + } + undo_stack_.push_back(std::move(command)); +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/CommandService.h b/MetaCore/engine/editor_core/CommandService.h new file mode 100644 index 0000000..89e6b94 --- /dev/null +++ b/MetaCore/engine/editor_core/CommandService.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace metacore::engine::editor_core { + +struct Command { + std::string label; + std::function execute; + std::function undo; +}; + +class CommandService { +public: + void execute(Command command); + bool can_undo() const; + bool can_redo() const; + void undo(); + void redo(); + +private: + std::vector undo_stack_; + std::vector redo_stack_; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorActions.cpp b/MetaCore/engine/editor_core/EditorActions.cpp new file mode 100644 index 0000000..8c7a486 --- /dev/null +++ b/MetaCore/engine/editor_core/EditorActions.cpp @@ -0,0 +1,21 @@ +#include "MetaCore/engine/editor_core/EditorActions.h" + +namespace metacore::engine::editor_core { + +std::vector EditorActionCatalog::build_default_actions() const { + return { + {EditorActionId::NewScene, "New Scene", "Ctrl+N", true}, + {EditorActionId::SaveScene, "Save Scene", "Ctrl+S", true}, + {EditorActionId::CreateEmptyObject, "Create Empty", "Ctrl+Shift+N", true}, + {EditorActionId::DeleteSelected, "Delete Selected", "Delete", true}, + {EditorActionId::RenameSelected, "Rename Selected", "F2", true}, + {EditorActionId::FocusSelected, "Focus Selected", "F", true}, + {EditorActionId::ToggleHierarchy, "Toggle Hierarchy", "", true}, + {EditorActionId::ToggleInspector, "Toggle Inspector", "", true}, + {EditorActionId::ToggleProject, "Toggle Project", "", true}, + {EditorActionId::ToggleConsole, "Toggle Console", "", true}, + {EditorActionId::ToggleRuntimePreview, "Toggle Runtime Preview", "", true} + }; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorActions.h b/MetaCore/engine/editor_core/EditorActions.h new file mode 100644 index 0000000..285136a --- /dev/null +++ b/MetaCore/engine/editor_core/EditorActions.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace metacore::engine::editor_core { + +enum class EditorActionId { + NewScene, + SaveScene, + CreateEmptyObject, + DeleteSelected, + RenameSelected, + FocusSelected, + ToggleHierarchy, + ToggleInspector, + ToggleProject, + ToggleConsole, + ToggleRuntimePreview +}; + +struct EditorAction { + EditorActionId id; + std::string label; + std::string shortcut; + bool enabled = true; +}; + +class EditorActionCatalog { +public: + [[nodiscard]] std::vector build_default_actions() const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorLayoutService.cpp b/MetaCore/engine/editor_core/EditorLayoutService.cpp new file mode 100644 index 0000000..ec54c87 --- /dev/null +++ b/MetaCore/engine/editor_core/EditorLayoutService.cpp @@ -0,0 +1,36 @@ +#include "MetaCore/engine/editor_core/EditorLayoutService.h" + +#include "MetaCore/foundation/FileSystem.h" +#include "MetaCore/foundation/Json.h" + +namespace metacore::engine::editor_core { + +bool EditorLayoutService::save(const std::filesystem::path& layout_file, const EditorLayoutState& state) const { + foundation::JsonValue::object_type root; + root["layout_name"] = state.layout_name; + root["hierarchy_visible"] = state.hierarchy_visible; + root["inspector_visible"] = state.inspector_visible; + root["project_visible"] = state.project_visible; + root["console_visible"] = state.console_visible; + root["runtime_preview_visible"] = state.runtime_preview_visible; + root["toolbar_visible"] = state.toolbar_visible; + return foundation::write_text_file(layout_file, foundation::JsonValue(std::move(root)).stringify(2)); +} + +bool EditorLayoutService::load(const std::filesystem::path& layout_file, EditorLayoutState& out_state) const { + const auto text = foundation::read_text_file(layout_file); + if (!text.has_value()) { + return false; + } + const auto json = foundation::JsonValue::parse(*text); + out_state.layout_name = json["layout_name"].as_string(); + out_state.hierarchy_visible = json["hierarchy_visible"].as_bool(true); + out_state.inspector_visible = json["inspector_visible"].as_bool(true); + out_state.project_visible = json["project_visible"].as_bool(true); + out_state.console_visible = json["console_visible"].as_bool(true); + out_state.runtime_preview_visible = json["runtime_preview_visible"].as_bool(true); + out_state.toolbar_visible = json["toolbar_visible"].as_bool(true); + return true; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorLayoutService.h b/MetaCore/engine/editor_core/EditorLayoutService.h new file mode 100644 index 0000000..1bfa907 --- /dev/null +++ b/MetaCore/engine/editor_core/EditorLayoutService.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace metacore::engine::editor_core { + +struct EditorLayoutState { + std::string layout_name = "Default"; + bool hierarchy_visible = true; + bool inspector_visible = true; + bool project_visible = true; + bool console_visible = true; + bool runtime_preview_visible = true; + bool toolbar_visible = true; +}; + +class EditorLayoutService { +public: + bool save(const std::filesystem::path& layout_file, const EditorLayoutState& state) const; + bool load(const std::filesystem::path& layout_file, EditorLayoutState& out_state) const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorSession.cpp b/MetaCore/engine/editor_core/EditorSession.cpp new file mode 100644 index 0000000..dbe0bd6 --- /dev/null +++ b/MetaCore/engine/editor_core/EditorSession.cpp @@ -0,0 +1,62 @@ +#include "MetaCore/engine/editor_core/EditorSession.h" + +namespace metacore::engine::editor_core { + +void EditorSession::mark_dirty() { + dirty_ = true; +} + +void EditorSession::clear_dirty() { + dirty_ = false; +} + +bool EditorSession::is_dirty() const { + return dirty_; +} + +void EditorSession::set_status_text(std::string text) { + status_text_ = std::move(text); +} + +const std::string& EditorSession::status_text() const { + return status_text_; +} + +void EditorSession::add_console_message(std::string category, std::string text) { + console_messages_.push_back(ConsoleMessage{std::move(category), std::move(text)}); +} + +const std::vector& EditorSession::console_messages() const { + return console_messages_; +} + +PanelState& EditorSession::panel_state() { + return panel_state_; +} + +const PanelState& EditorSession::panel_state() const { + return panel_state_; +} + +EditorLayoutState EditorSession::to_layout_state() const { + EditorLayoutState state; + state.layout_name = "Default"; + state.hierarchy_visible = panel_state_.hierarchy_visible; + state.inspector_visible = panel_state_.inspector_visible; + state.project_visible = panel_state_.project_visible; + state.console_visible = panel_state_.console_visible; + state.runtime_preview_visible = panel_state_.runtime_preview_visible; + state.toolbar_visible = panel_state_.toolbar_visible; + return state; +} + +void EditorSession::apply_layout_state(const EditorLayoutState& layout_state) { + panel_state_.hierarchy_visible = layout_state.hierarchy_visible; + panel_state_.inspector_visible = layout_state.inspector_visible; + panel_state_.project_visible = layout_state.project_visible; + panel_state_.console_visible = layout_state.console_visible; + panel_state_.runtime_preview_visible = layout_state.runtime_preview_visible; + panel_state_.toolbar_visible = layout_state.toolbar_visible; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/EditorSession.h b/MetaCore/engine/editor_core/EditorSession.h new file mode 100644 index 0000000..3726a9d --- /dev/null +++ b/MetaCore/engine/editor_core/EditorSession.h @@ -0,0 +1,50 @@ +#pragma once + +#include "MetaCore/engine/editor_core/EditorLayoutService.h" + +#include +#include + +namespace metacore::engine::editor_core { + +struct ConsoleMessage { + std::string category; + std::string text; +}; + +struct PanelState { + bool hierarchy_visible = true; + bool inspector_visible = true; + bool project_visible = true; + bool console_visible = true; + bool viewport_visible = true; + bool runtime_preview_visible = true; + bool toolbar_visible = true; +}; + +class EditorSession { +public: + void mark_dirty(); + void clear_dirty(); + [[nodiscard]] bool is_dirty() const; + + void set_status_text(std::string text); + [[nodiscard]] const std::string& status_text() const; + + void add_console_message(std::string category, std::string text); + [[nodiscard]] const std::vector& console_messages() const; + + PanelState& panel_state(); + const PanelState& panel_state() const; + + EditorLayoutState to_layout_state() const; + void apply_layout_state(const EditorLayoutState& layout_state); + +private: + bool dirty_ = false; + std::string status_text_ = "Ready"; + std::vector console_messages_; + PanelState panel_state_{}; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/InspectorService.cpp b/MetaCore/engine/editor_core/InspectorService.cpp new file mode 100644 index 0000000..18001bb --- /dev/null +++ b/MetaCore/engine/editor_core/InspectorService.cpp @@ -0,0 +1,22 @@ +#include "MetaCore/engine/editor_core/InspectorService.h" + +namespace metacore::engine::editor_core { + +std::optional> InspectorService::current_selection( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service +) const { + if (!selection_service.selection().has_value()) { + return std::nullopt; + } + + for (const auto& object : scene.objects) { + if (object.id == *selection_service.selection()) { + return object; + } + } + + return std::nullopt; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/InspectorService.h b/MetaCore/engine/editor_core/InspectorService.h new file mode 100644 index 0000000..39e4b4d --- /dev/null +++ b/MetaCore/engine/editor_core/InspectorService.h @@ -0,0 +1,18 @@ +#pragma once + +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/scene/Scene.h" + +#include + +namespace metacore::engine::editor_core { + +class InspectorService { +public: + std::optional> current_selection( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service + ) const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/PanelViewModels.cpp b/MetaCore/engine/editor_core/PanelViewModels.cpp new file mode 100644 index 0000000..5ed9d1f --- /dev/null +++ b/MetaCore/engine/editor_core/PanelViewModels.cpp @@ -0,0 +1,106 @@ +#include "MetaCore/engine/editor_core/PanelViewModels.h" + +#include + +namespace metacore::engine::editor_core { + +std::vector PanelViewModels::build_hierarchy( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service +) const { + std::vector items; + items.reserve(scene.objects.size()); + + for (const auto& object : scene.objects) { + const bool has_children = std::any_of( + scene.objects.begin(), + scene.objects.end(), + [&object](const metacore::engine::scene::SceneObject& candidate) { + return candidate.parent_id == object.id; + } + ); + + items.push_back(HierarchyItem{ + object.id, + object.parent_id, + object.name, + selection_service.selection().has_value() && *selection_service.selection() == object.id, + has_children + }); + } + + return items; +} + +std::vector PanelViewModels::build_project_assets( + const metacore::engine::assets::AssetDatabase& asset_database +) const { + std::vector items; + items.reserve(asset_database.records().size()); + + for (const auto& [id, record] : asset_database.records()) { + items.push_back(ProjectAssetItem{id, record.type, record.relative_path}); + } + + std::sort(items.begin(), items.end(), [](const ProjectAssetItem& lhs, const ProjectAssetItem& rhs) { + return lhs.relative_path < rhs.relative_path; + }); + return items; +} + +InspectorViewModel PanelViewModels::build_inspector( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service +) const { + InspectorViewModel model; + + if (!selection_service.selection().has_value()) { + model.object_name = "Nothing Selected"; + return model; + } + + const auto selected_id = *selection_service.selection(); + for (const auto& object : scene.objects) { + if (object.id != selected_id) { + continue; + } + + model.object_name = object.name; + model.fields.push_back({"Id", object.id}); + model.fields.push_back({"Position", std::to_string(object.transform.position[0]) + ", " + + std::to_string(object.transform.position[1]) + ", " + + std::to_string(object.transform.position[2])}); + model.fields.push_back({"Rotation", std::to_string(object.transform.rotation[0]) + ", " + + std::to_string(object.transform.rotation[1]) + ", " + + std::to_string(object.transform.rotation[2])}); + model.fields.push_back({"Scale", std::to_string(object.transform.scale[0]) + ", " + + std::to_string(object.transform.scale[1]) + ", " + + std::to_string(object.transform.scale[2])}); + if (object.has_camera) { + model.fields.push_back({"Camera", object.camera.primary ? "Primary" : "Secondary"}); + } + if (object.has_light) { + model.fields.push_back({"Light", "Enabled"}); + } + if (object.has_mesh_renderer) { + model.fields.push_back({"Mesh", object.mesh_renderer.mesh_asset_id}); + } + if (object.has_ui_document) { + model.fields.push_back({"UI Document", object.ui_document.document_path}); + model.fields.push_back({"UI Stylesheet", object.ui_document.stylesheet_path}); + } + if (object.has_collision) { + model.fields.push_back({"Collision", object.collision.shape}); + } + return model; + } + + model.object_name = "Missing Selection"; + return model; +} + +const std::vector& PanelViewModels::console_messages(const EditorSession& session) const { + return session.console_messages(); +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/PanelViewModels.h b/MetaCore/engine/editor_core/PanelViewModels.h new file mode 100644 index 0000000..8d76356 --- /dev/null +++ b/MetaCore/engine/editor_core/PanelViewModels.h @@ -0,0 +1,58 @@ +#pragma once + +#include "MetaCore/engine/assets/AssetDatabase.h" +#include "MetaCore/engine/editor_core/EditorSession.h" +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/scene/Scene.h" + +#include +#include + +namespace metacore::engine::editor_core { + +struct HierarchyItem { + metacore::engine::scene::EntityId id; + metacore::engine::scene::EntityId parent_id; + std::string name; + bool selected = false; + bool has_children = false; +}; + +struct ProjectAssetItem { + std::string id; + std::string type; + std::string relative_path; +}; + +struct InspectorField { + std::string label; + std::string value; +}; + +struct InspectorViewModel { + std::string object_name; + std::vector fields; +}; + +class PanelViewModels { +public: + [[nodiscard]] std::vector build_hierarchy( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service + ) const; + + [[nodiscard]] std::vector build_project_assets( + const metacore::engine::assets::AssetDatabase& asset_database + ) const; + + [[nodiscard]] InspectorViewModel build_inspector( + const metacore::engine::scene::Scene& scene, + const SelectionService& selection_service + ) const; + + [[nodiscard]] const std::vector& console_messages( + const EditorSession& session + ) const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/SceneCommands.cpp b/MetaCore/engine/editor_core/SceneCommands.cpp new file mode 100644 index 0000000..fbaa675 --- /dev/null +++ b/MetaCore/engine/editor_core/SceneCommands.cpp @@ -0,0 +1,160 @@ +#include "MetaCore/engine/editor_core/SceneCommands.h" + +#include + +namespace metacore::engine::editor_core { + +void SceneCommands::create_empty_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const std::string& name +) { + metacore::engine::scene::EntityId created_id; + + command_service.execute(Command{ + "Create Empty Object", + [&scene_service, &scene, &selection_service, &session, &created_id, name]() { + auto& object = scene_service.create_object(scene, name); + created_id = object.id; + selection_service.select(created_id); + session.mark_dirty(); + session.set_status_text("Created object: " + name); + session.add_console_message("Scene", "Created object '" + name + "'"); + }, + [&scene_service, &scene, &selection_service, &session, &created_id]() { + scene_service.destroy_object(scene, created_id); + selection_service.clear(); + session.mark_dirty(); + session.set_status_text("Undo create object"); + session.add_console_message("Scene", "Undid object creation"); + } + }); +} + +void SceneCommands::rename_selected_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const std::string& new_name +) { + if (!selection_service.selection().has_value()) { + return; + } + + const auto selected_id = *selection_service.selection(); + const auto object = scene_service.find_object(scene, selected_id); + if (!object.has_value()) { + return; + } + + const std::string old_name = object->get().name; + command_service.execute(Command{ + "Rename Object", + [&scene_service, &scene, &session, selected_id, new_name, old_name]() { + scene_service.rename_object(scene, selected_id, new_name); + session.mark_dirty(); + session.set_status_text("Renamed object to: " + new_name); + session.add_console_message("Scene", "Renamed object '" + old_name + "' to '" + new_name + "'"); + }, + [&scene_service, &scene, &session, selected_id, old_name]() { + scene_service.rename_object(scene, selected_id, old_name); + session.mark_dirty(); + session.set_status_text("Undo rename object"); + session.add_console_message("Scene", "Undid object rename"); + } + }); +} + +void SceneCommands::delete_selected_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene +) { + if (!selection_service.selection().has_value()) { + return; + } + + const auto selected_id = *selection_service.selection(); + const auto object = scene_service.find_object(scene, selected_id); + if (!object.has_value()) { + return; + } + + const auto deleted_object = object->get(); + command_service.execute(Command{ + "Delete Object", + [&scene_service, &scene, &selection_service, &session, selected_id, deleted_object]() { + scene_service.destroy_object(scene, selected_id); + selection_service.clear(); + session.mark_dirty(); + session.set_status_text("Deleted object: " + deleted_object.name); + session.add_console_message("Scene", "Deleted object '" + deleted_object.name + "'"); + }, + [&scene, &selection_service, &session, deleted_object]() { + scene.objects.push_back(deleted_object); + selection_service.select(deleted_object.id); + session.mark_dirty(); + session.set_status_text("Undo delete object"); + session.add_console_message("Scene", "Restored object '" + deleted_object.name + "'"); + } + }); +} + +void SceneCommands::focus_selected_object( + EditorSession& session, + const SelectionService& selection_service, + ViewportService& viewport_service +) { + if (!selection_service.selection().has_value()) { + return; + } + + viewport_service.focus_selection(*selection_service.selection()); + session.set_status_text("Focused selected object"); + session.add_console_message("Viewport", "Focused selected object"); +} + +void SceneCommands::apply_transform_to_selected_object( + CommandService& command_service, + EditorSession& session, + const SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const metacore::engine::scene::TransformComponent& new_transform +) { + if (!selection_service.selection().has_value()) { + return; + } + + const auto selected_id = *selection_service.selection(); + const auto object = scene_service.find_object(scene, selected_id); + if (!object.has_value()) { + return; + } + + const auto old_transform = object->get().transform; + command_service.execute(Command{ + "Apply Transform", + [&scene_service, &scene, &session, selected_id, new_transform]() { + scene_service.set_object_transform(scene, selected_id, new_transform); + session.mark_dirty(); + session.set_status_text("Applied transform"); + session.add_console_message("Viewport", "Applied transform edit"); + }, + [&scene_service, &scene, &session, selected_id, old_transform]() { + scene_service.set_object_transform(scene, selected_id, old_transform); + session.mark_dirty(); + session.set_status_text("Undo transform"); + session.add_console_message("Viewport", "Undid transform edit"); + } + }); +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/SceneCommands.h b/MetaCore/engine/editor_core/SceneCommands.h new file mode 100644 index 0000000..8141f50 --- /dev/null +++ b/MetaCore/engine/editor_core/SceneCommands.h @@ -0,0 +1,55 @@ +#pragma once + +#include "MetaCore/engine/editor_core/CommandService.h" +#include "MetaCore/engine/editor_core/EditorSession.h" +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/editor_core/ViewportService.h" +#include "MetaCore/engine/scene/Scene.h" + +namespace metacore::engine::editor_core { + +class SceneCommands { +public: + static void create_empty_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const std::string& name + ); + + static void rename_selected_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const std::string& new_name + ); + + static void delete_selected_object( + CommandService& command_service, + EditorSession& session, + SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene + ); + + static void focus_selected_object( + EditorSession& session, + const SelectionService& selection_service, + ViewportService& viewport_service + ); + + static void apply_transform_to_selected_object( + CommandService& command_service, + EditorSession& session, + const SelectionService& selection_service, + metacore::engine::scene::SceneService& scene_service, + metacore::engine::scene::Scene& scene, + const metacore::engine::scene::TransformComponent& new_transform + ); +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/SelectionService.cpp b/MetaCore/engine/editor_core/SelectionService.cpp new file mode 100644 index 0000000..de976c4 --- /dev/null +++ b/MetaCore/engine/editor_core/SelectionService.cpp @@ -0,0 +1,21 @@ +#include "MetaCore/engine/editor_core/SelectionService.h" + +namespace metacore::engine::editor_core { + +void SelectionService::select(const metacore::engine::scene::EntityId& id) { + selected_id_ = id; +} + +void SelectionService::clear() { + selected_id_.reset(); +} + +bool SelectionService::has_selection() const { + return selected_id_.has_value(); +} + +const std::optional& SelectionService::selection() const { + return selected_id_; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/SelectionService.h b/MetaCore/engine/editor_core/SelectionService.h new file mode 100644 index 0000000..fcdf34b --- /dev/null +++ b/MetaCore/engine/editor_core/SelectionService.h @@ -0,0 +1,20 @@ +#pragma once + +#include "MetaCore/engine/scene/Components.h" + +#include + +namespace metacore::engine::editor_core { + +class SelectionService { +public: + void select(const metacore::engine::scene::EntityId& id); + void clear(); + [[nodiscard]] bool has_selection() const; + [[nodiscard]] const std::optional& selection() const; + +private: + std::optional selected_id_; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/ShellComposition.cpp b/MetaCore/engine/editor_core/ShellComposition.cpp new file mode 100644 index 0000000..03a2724 --- /dev/null +++ b/MetaCore/engine/editor_core/ShellComposition.cpp @@ -0,0 +1,63 @@ +#include "MetaCore/engine/editor_core/ShellComposition.h" + +#include + +namespace metacore::engine::editor_core { + +ShellComposition ShellCompositionBuilder::build( + const std::vector& actions, + const WorkspaceLayout& workspace +) const { + ShellComposition composition; + + MenuEntry file_menu{"File", {}}; + MenuEntry edit_menu{"Edit", {}}; + MenuEntry view_menu{"View", {}}; + + ToolbarGroup scene_tools{"Scene", {}}; + ToolbarGroup selection_tools{"Selection", {}}; + ToolbarGroup view_tools{"View", {}}; + + for (const auto& action : actions) { + switch (action.id) { + case EditorActionId::NewScene: + case EditorActionId::SaveScene: + file_menu.actions.push_back(action); + break; + case EditorActionId::CreateEmptyObject: + case EditorActionId::DeleteSelected: + case EditorActionId::RenameSelected: + edit_menu.actions.push_back(action); + scene_tools.actions.push_back(action); + break; + case EditorActionId::FocusSelected: + edit_menu.actions.push_back(action); + selection_tools.actions.push_back(action); + break; + case EditorActionId::ToggleHierarchy: + case EditorActionId::ToggleInspector: + case EditorActionId::ToggleProject: + case EditorActionId::ToggleConsole: + case EditorActionId::ToggleRuntimePreview: + view_menu.actions.push_back(action); + view_tools.actions.push_back(action); + break; + } + } + + composition.menus = {file_menu, edit_menu, view_menu}; + composition.toolbar_groups = {scene_tools, selection_tools, view_tools}; + + std::map> grouped_panels; + for (const auto& panel : workspace.panels) { + grouped_panels[panel.region].push_back(panel); + } + + for (const auto& [region, panels] : grouped_panels) { + composition.sections.push_back(PanelSection{region, panels}); + } + + return composition; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/ShellComposition.h b/MetaCore/engine/editor_core/ShellComposition.h new file mode 100644 index 0000000..129945a --- /dev/null +++ b/MetaCore/engine/editor_core/ShellComposition.h @@ -0,0 +1,40 @@ +#pragma once + +#include "MetaCore/engine/editor_core/EditorActions.h" +#include "MetaCore/engine/editor_core/WorkspaceLayout.h" + +#include +#include + +namespace metacore::engine::editor_core { + +struct MenuEntry { + std::string menu_name; + std::vector actions; +}; + +struct ToolbarGroup { + std::string name; + std::vector actions; +}; + +struct PanelSection { + DockRegion region = DockRegion::CenterViewport; + std::vector panels; +}; + +struct ShellComposition { + std::vector menus; + std::vector toolbar_groups; + std::vector sections; +}; + +class ShellCompositionBuilder { +public: + [[nodiscard]] ShellComposition build( + const std::vector& actions, + const WorkspaceLayout& workspace + ) const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/TransformGizmoService.cpp b/MetaCore/engine/editor_core/TransformGizmoService.cpp new file mode 100644 index 0000000..9d64358 --- /dev/null +++ b/MetaCore/engine/editor_core/TransformGizmoService.cpp @@ -0,0 +1,37 @@ +#include "MetaCore/engine/editor_core/TransformGizmoService.h" + +namespace metacore::engine::editor_core { + +void TransformGizmoService::set_operation(const Operation operation) { + operation_ = operation; +} + +void TransformGizmoService::set_space(const Space space) { + space_ = space; +} + +void TransformGizmoService::set_snap_enabled(const bool enabled) { + snap_enabled_ = enabled; +} + +void TransformGizmoService::set_snap_values(const std::array snap_values) { + snap_values_ = snap_values; +} + +TransformGizmoService::Operation TransformGizmoService::operation() const { + return operation_; +} + +TransformGizmoService::Space TransformGizmoService::space() const { + return space_; +} + +bool TransformGizmoService::snap_enabled() const { + return snap_enabled_; +} + +const std::array& TransformGizmoService::snap_values() const { + return snap_values_; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/TransformGizmoService.h b/MetaCore/engine/editor_core/TransformGizmoService.h new file mode 100644 index 0000000..007d334 --- /dev/null +++ b/MetaCore/engine/editor_core/TransformGizmoService.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace metacore::engine::editor_core { + +class TransformGizmoService { +public: + enum class Operation { + Translate, + Rotate, + Scale, + }; + + enum class Space { + Local, + World, + }; + + void set_operation(Operation operation); + void set_space(Space space); + void set_snap_enabled(bool enabled); + void set_snap_values(std::array snap_values); + + [[nodiscard]] Operation operation() const; + [[nodiscard]] Space space() const; + [[nodiscard]] bool snap_enabled() const; + [[nodiscard]] const std::array& snap_values() const; + +private: + Operation operation_ = Operation::Translate; + Space space_ = Space::Local; + bool snap_enabled_ = false; + std::array snap_values_{1.0F, 1.0F, 1.0F}; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/ViewportService.cpp b/MetaCore/engine/editor_core/ViewportService.cpp new file mode 100644 index 0000000..66bf4de --- /dev/null +++ b/MetaCore/engine/editor_core/ViewportService.cpp @@ -0,0 +1,25 @@ +#include "MetaCore/engine/editor_core/ViewportService.h" + +namespace metacore::engine::editor_core { + +void ViewportService::set_camera_position(std::array position) { + camera_position_ = position; +} + +const std::array& ViewportService::camera_position() const { + return camera_position_; +} + +void ViewportService::focus_selection(const metacore::engine::scene::EntityId& id) { + focus_target_ = id; +} + +void ViewportService::clear_focus_target() { + focus_target_.reset(); +} + +const std::optional& ViewportService::focus_target() const { + return focus_target_; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/ViewportService.h b/MetaCore/engine/editor_core/ViewportService.h new file mode 100644 index 0000000..e91bf09 --- /dev/null +++ b/MetaCore/engine/editor_core/ViewportService.h @@ -0,0 +1,24 @@ +#pragma once + +#include "MetaCore/engine/scene/Components.h" + +#include +#include + +namespace metacore::engine::editor_core { + +class ViewportService { +public: + void set_camera_position(std::array position); + [[nodiscard]] const std::array& camera_position() const; + + void focus_selection(const metacore::engine::scene::EntityId& id); + void clear_focus_target(); + [[nodiscard]] const std::optional& focus_target() const; + +private: + std::array camera_position_{0.0F, -10.0F, 3.0F}; + std::optional focus_target_; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/WorkspaceLayout.cpp b/MetaCore/engine/editor_core/WorkspaceLayout.cpp new file mode 100644 index 0000000..57b2c9e --- /dev/null +++ b/MetaCore/engine/editor_core/WorkspaceLayout.cpp @@ -0,0 +1,21 @@ +#include "MetaCore/engine/editor_core/WorkspaceLayout.h" + +namespace metacore::engine::editor_core { + +WorkspaceLayout WorkspaceLayoutFactory::build_default_layout() const { + WorkspaceLayout layout; + layout.name = "Default"; + layout.panels = { + {"main_menu", "Main Menu", DockRegion::TopMenuBar, true}, + {"toolbar", "Toolbar", DockRegion::TopToolbar, true}, + {"hierarchy", "Hierarchy", DockRegion::LeftSidebar, true}, + {"project", "Project", DockRegion::LeftSidebar, true}, + {"viewport", "Viewport", DockRegion::CenterViewport, true}, + {"inspector", "Inspector", DockRegion::RightSidebar, true}, + {"console", "Console", DockRegion::BottomConsole, true}, + {"runtime_preview", "Runtime Preview", DockRegion::BottomRuntimePreview, true} + }; + return layout; +} + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/editor_core/WorkspaceLayout.h b/MetaCore/engine/editor_core/WorkspaceLayout.h new file mode 100644 index 0000000..cd761c6 --- /dev/null +++ b/MetaCore/engine/editor_core/WorkspaceLayout.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace metacore::engine::editor_core { + +enum class DockRegion { + TopMenuBar, + TopToolbar, + LeftSidebar, + CenterViewport, + RightSidebar, + BottomConsole, + BottomRuntimePreview +}; + +struct DockPanel { + std::string id; + std::string title; + DockRegion region = DockRegion::CenterViewport; + bool visible = true; +}; + +struct WorkspaceLayout { + std::string name = "Default"; + std::vector panels; +}; + +class WorkspaceLayoutFactory { +public: + [[nodiscard]] WorkspaceLayout build_default_layout() const; +}; + +} // namespace metacore::engine::editor_core diff --git a/MetaCore/engine/render/RenderTypes.h b/MetaCore/engine/render/RenderTypes.h new file mode 100644 index 0000000..2dc2239 --- /dev/null +++ b/MetaCore/engine/render/RenderTypes.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace metacore::engine::render { + +struct ShaderVariantKey { + std::map defines; +}; + +struct MaterialDescriptor { + std::array base_color{1.0F, 1.0F, 1.0F, 1.0F}; + std::array emission{0.0F, 0.0F, 0.0F, 1.0F}; + float roughness = 1.0F; + float metallic = 0.0F; + std::filesystem::path base_color_texture; + std::filesystem::path metal_roughness_texture; + std::filesystem::path normal_texture; + std::filesystem::path emission_texture; +}; + +struct MaterialInstance { + std::string id; + MaterialDescriptor descriptor; +}; + +struct FrameUniforms { + std::array camera_world_position{0.0F, 0.0F, 0.0F}; + float exposure = 1.0F; +}; + +struct LightUniforms { + struct LightEntry { + std::array color{1.0F, 1.0F, 1.0F}; + std::array position{0.0F, 0.0F, 0.0F}; + float intensity = 1.0F; + }; + + std::vector lights; +}; + +struct TonemapSettings { + float exposure = 1.0F; +}; + +class IMetaRenderBackend { +public: + virtual ~IMetaRenderBackend() = default; + + virtual bool initialize() = 0; + virtual void shutdown() = 0; + virtual void set_frame_uniforms(const FrameUniforms& uniforms) = 0; + virtual void set_light_uniforms(const LightUniforms& uniforms) = 0; +}; + +} // namespace metacore::engine::render diff --git a/MetaCore/engine/render/SimplePbrShaderLibrary.cpp b/MetaCore/engine/render/SimplePbrShaderLibrary.cpp new file mode 100644 index 0000000..1d91ae0 --- /dev/null +++ b/MetaCore/engine/render/SimplePbrShaderLibrary.cpp @@ -0,0 +1,25 @@ +#include "MetaCore/engine/render/SimplePbrShaderLibrary.h" + +namespace metacore::engine::render { + +SimplePbrShaderLibrary::SimplePbrShaderLibrary(std::filesystem::path root) + : root_(std::move(root)) {} + +const std::filesystem::path& SimplePbrShaderLibrary::root() const { + return root_; +} + +std::map SimplePbrShaderLibrary::shader_files() const { + return { + {"simplepbr.vert", root_ / "simplepbr.vert"}, + {"simplepbr.frag", root_ / "simplepbr.frag"}, + {"shadow.vert", root_ / "shadow.vert"}, + {"shadow.frag", root_ / "shadow.frag"}, + {"skybox.vert", root_ / "skybox.vert"}, + {"skybox.frag", root_ / "skybox.frag"}, + {"post.vert", root_ / "post.vert"}, + {"tonemap.frag", root_ / "tonemap.frag"}, + }; +} + +} // namespace metacore::engine::render diff --git a/MetaCore/engine/render/SimplePbrShaderLibrary.h b/MetaCore/engine/render/SimplePbrShaderLibrary.h new file mode 100644 index 0000000..0c54a11 --- /dev/null +++ b/MetaCore/engine/render/SimplePbrShaderLibrary.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace metacore::engine::render { + +class SimplePbrShaderLibrary { +public: + explicit SimplePbrShaderLibrary(std::filesystem::path root); + + [[nodiscard]] const std::filesystem::path& root() const; + [[nodiscard]] std::map shader_files() const; + +private: + std::filesystem::path root_; +}; + +} // namespace metacore::engine::render diff --git a/MetaCore/engine/runtime_ui/RmlUiBridge.cpp b/MetaCore/engine/runtime_ui/RmlUiBridge.cpp new file mode 100644 index 0000000..2f5612e --- /dev/null +++ b/MetaCore/engine/runtime_ui/RmlUiBridge.cpp @@ -0,0 +1,47 @@ +#include "MetaCore/engine/runtime_ui/RmlUiBridge.h" + +namespace metacore::engine::runtime_ui { + +bool RmlUiBridge::initialize() { + initialized_ = true; + return true; +} + +void RmlUiBridge::shutdown() { + initialized_ = false; + active_document_ = {}; +} + +bool RmlUiBridge::load_document(const RuntimeUiDocument& document) { + if (!initialized_) { + return false; + } + active_document_ = document; + return true; +} + +std::vector RmlUiBridge::collect_documents(const metacore::engine::scene::Scene& scene) const { + std::vector documents; + for (const auto& object : scene.objects) { + if (!object.has_ui_document) { + continue; + } + + documents.push_back(RuntimeUiDocument{ + object.id, + object.ui_document.document_path, + object.ui_document.stylesheet_path + }); + } + return documents; +} + +bool RmlUiBridge::initialized() const { + return initialized_; +} + +const RuntimeUiDocument* RmlUiBridge::active_document() const { + return initialized_ ? &active_document_ : nullptr; +} + +} // namespace metacore::engine::runtime_ui diff --git a/MetaCore/engine/runtime_ui/RmlUiBridge.h b/MetaCore/engine/runtime_ui/RmlUiBridge.h new file mode 100644 index 0000000..3752051 --- /dev/null +++ b/MetaCore/engine/runtime_ui/RmlUiBridge.h @@ -0,0 +1,32 @@ +#pragma once + +#include "MetaCore/engine/scene/Scene.h" + +#include +#include +#include + +namespace metacore::engine::runtime_ui { + +struct RuntimeUiDocument { + std::string id; + std::filesystem::path document_path; + std::filesystem::path stylesheet_path; +}; + +class RmlUiBridge { +public: + bool initialize(); + void shutdown(); + bool load_document(const RuntimeUiDocument& document); + [[nodiscard]] std::vector collect_documents(const metacore::engine::scene::Scene& scene) const; + + [[nodiscard]] bool initialized() const; + [[nodiscard]] const RuntimeUiDocument* active_document() const; + +private: + bool initialized_ = false; + RuntimeUiDocument active_document_{}; +}; + +} // namespace metacore::engine::runtime_ui diff --git a/MetaCore/engine/scene/Components.h b/MetaCore/engine/scene/Components.h new file mode 100644 index 0000000..fbf3459 --- /dev/null +++ b/MetaCore/engine/scene/Components.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +namespace metacore::engine::scene { + +using EntityId = std::string; + +struct TransformComponent { + std::array position{0.0F, 0.0F, 0.0F}; + std::array rotation{0.0F, 0.0F, 0.0F}; + std::array scale{1.0F, 1.0F, 1.0F}; +}; + +struct MeshRendererComponent { + std::string mesh_asset_id; + std::string material_asset_id; + bool visible = true; +}; + +struct LightComponent { + enum class Type { + Directional, + Point, + Spot, + }; + + Type type = Type::Directional; + std::array color{1.0F, 1.0F, 1.0F}; + float intensity = 1.0F; + float range = 10.0F; + float spot_angle_degrees = 45.0F; +}; + +struct CameraComponent { + float fov_degrees = 60.0F; + float near_clip = 0.1F; + float far_clip = 1000.0F; + bool primary = false; +}; + +struct UIDocumentComponent { + std::string document_path; + std::string stylesheet_path; + bool visible = true; +}; + +struct CollisionComponent { + bool enabled = false; + std::string shape = "box"; +}; + +struct EditorMetadataComponent { + bool editor_only = false; + bool selected = false; + bool locked = false; +}; + +struct SceneObject { + EntityId id; + EntityId parent_id; + std::string name; + TransformComponent transform{}; + MeshRendererComponent mesh_renderer{}; + LightComponent light{}; + CameraComponent camera{}; + UIDocumentComponent ui_document{}; + CollisionComponent collision{}; + EditorMetadataComponent editor_metadata{}; + bool has_mesh_renderer = false; + bool has_light = false; + bool has_camera = false; + bool has_ui_document = false; + bool has_collision = false; +}; + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/Project.cpp b/MetaCore/engine/scene/Project.cpp new file mode 100644 index 0000000..7577392 --- /dev/null +++ b/MetaCore/engine/scene/Project.cpp @@ -0,0 +1,143 @@ +#include "MetaCore/engine/scene/Project.h" + +#include "MetaCore/engine/assets/AssetDatabase.h" +#include "MetaCore/engine/scene/Scene.h" +#include "MetaCore/engine/scene/SceneSerializer.h" +#include "MetaCore/foundation/FileSystem.h" +#include "MetaCore/foundation/Json.h" + +namespace metacore::engine::scene { + +using foundation::JsonValue; + +namespace { + +JsonValue project_to_json(const Project& project) { + JsonValue::object_type root; + root["name"] = project.name; + root["version"] = project.version; + root["startup_scene"] = project.startup_scene; + + JsonValue::array_type scenes; + for (const auto& scene : project.scenes) { + scenes.emplace_back(scene); + } + root["scenes"] = JsonValue(std::move(scenes)); + return JsonValue(std::move(root)); +} + +Project project_from_json(const JsonValue& json) { + Project project; + project.name = json["name"].as_string(); + project.version = json["version"].as_string(); + project.startup_scene = json["startup_scene"].as_string(); + project.scenes.clear(); + for (const auto& entry : json["scenes"].as_array()) { + project.scenes.push_back(entry.as_string()); + } + if (project.scenes.empty()) { + project.scenes.push_back("Scenes/Main.mcscene.json"); + } + if (project.startup_scene.empty()) { + project.startup_scene = project.scenes.front(); + } + return project; +} + +} // namespace + +bool ProjectService::create_project(const std::filesystem::path& project_root, const std::string& name) const { + foundation::ensure_directory(project_root / "Scenes"); + foundation::ensure_directory(project_root / "Library" / "Layout"); + foundation::ensure_directory(project_root / "Library" / "Cache"); + + metacore::engine::assets::AssetService asset_service; + asset_service.ensure_default_asset_tree(project_root); + + Project project; + project.name = name; + save_project(project_root / "MetaCore.project.json", project); + + SceneService scene_service; + Scene scene = scene_service.create_default_scene(); + SceneSerializer serializer; + serializer.save_scene(project_root / "Scenes" / "Main.mcscene.json", scene); + + metacore::engine::assets::AssetDatabase asset_database; + const auto rml_id = asset_service.ensure_asset_record(asset_database, "ui_document", "Assets/UI/main_menu.rml"); + const auto rcss_id = asset_service.ensure_asset_record(asset_database, "ui_stylesheet", "Assets/UI/main_menu.rcss"); + const auto material_id = asset_service.ensure_asset_record(asset_database, "material", "Assets/Materials/default_pbr.material.json"); + + foundation::write_text_file( + project_root / "Assets" / "UI" / "main_menu.rml", + "\n" + " \n" + " \n" + " \n" + " \n" + "
\n" + "

MetaCore

\n" + "

Runtime UI scaffold

\n" + "
\n" + " \n" + "
\n" + ); + foundation::write_text_file( + project_root / "Assets" / "UI" / "main_menu.rcss", + "body {\n" + " font-family: sans-serif;\n" + " color: #f2f4f8;\n" + "}\n" + ".root-panel {\n" + " width: 420px;\n" + " margin: 40px auto;\n" + " padding: 24px;\n" + " background: rgba(16, 18, 24, 0.85);\n" + "}\n" + ); + foundation::write_text_file( + project_root / "Assets" / "Materials" / "default_pbr.material.json", + "{\n" + " \"name\": \"DefaultPbr\",\n" + " \"shader_family\": \"simplepbr\",\n" + " \"base_color\": [1.0, 1.0, 1.0, 1.0],\n" + " \"roughness\": 1.0,\n" + " \"metallic\": 0.0\n" + "}\n" + ); + + asset_service.write_meta_file(project_root, "Assets/UI/main_menu.rml", rml_id, "ui_document"); + asset_service.write_meta_file(project_root, "Assets/UI/main_menu.rcss", rcss_id, "ui_stylesheet"); + asset_service.write_meta_file(project_root, "Assets/Materials/default_pbr.material.json", material_id, "material"); + + asset_database.save(project_root / "Library" / "AssetDB.json"); + foundation::write_text_file( + project_root / "Library" / "Layout" / "editor_layout.json", + "{\n" + " \"layout_name\": \"Default\",\n" + " \"hierarchy_visible\": true,\n" + " \"inspector_visible\": true,\n" + " \"project_visible\": true,\n" + " \"console_visible\": true,\n" + " \"runtime_preview_visible\": true,\n" + " \"toolbar_visible\": true\n" + "}\n" + ); + + return true; +} + +bool ProjectService::load_project(const std::filesystem::path& project_file, Project& out_project) const { + const auto text = foundation::read_text_file(project_file); + if (!text.has_value()) { + return false; + } + out_project = project_from_json(JsonValue::parse(*text)); + return true; +} + +bool ProjectService::save_project(const std::filesystem::path& project_file, const Project& project) const { + return foundation::write_text_file(project_file, project_to_json(project).stringify(2)); +} + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/Project.h b/MetaCore/engine/scene/Project.h new file mode 100644 index 0000000..c49d0ed --- /dev/null +++ b/MetaCore/engine/scene/Project.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace metacore::engine::scene { + +struct Project { + std::string name = "MetaCoreProject"; + std::string version = "0.1.0"; + std::string startup_scene = "Scenes/Main.mcscene.json"; + std::vector scenes{"Scenes/Main.mcscene.json"}; +}; + +class ProjectService { +public: + bool create_project(const std::filesystem::path& project_root, const std::string& name) const; + bool load_project(const std::filesystem::path& project_file, Project& out_project) const; + bool save_project(const std::filesystem::path& project_file, const Project& project) const; +}; + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/Scene.cpp b/MetaCore/engine/scene/Scene.cpp new file mode 100644 index 0000000..8a46207 --- /dev/null +++ b/MetaCore/engine/scene/Scene.cpp @@ -0,0 +1,96 @@ +#include "MetaCore/engine/scene/Scene.h" + +#include "MetaCore/foundation/Guid.h" + +#include + +namespace metacore::engine::scene { + +Scene SceneService::create_default_scene() const { + Scene scene; + scene.name = "Main"; + + auto& camera = create_object(scene, "Main Camera"); + camera.has_camera = true; + camera.camera.primary = true; + camera.transform.position = {0.0F, -10.0F, 3.0F}; + + auto& light = create_object(scene, "Directional Light"); + light.has_light = true; + light.light.type = LightComponent::Type::Directional; + + return scene; +} + +SceneObject& SceneService::create_object(Scene& scene, const std::string& name, const EntityId& parent_id) const { + SceneObject object; + object.id = foundation::generate_guid(); + object.parent_id = parent_id; + object.name = name; + scene.objects.push_back(object); + return scene.objects.back(); +} + +std::optional> SceneService::find_object(Scene& scene, const EntityId& id) const { + for (auto& object : scene.objects) { + if (object.id == id) { + return object; + } + } + return std::nullopt; +} + +std::optional> SceneService::find_object(const Scene& scene, const EntityId& id) const { + for (const auto& object : scene.objects) { + if (object.id == id) { + return object; + } + } + return std::nullopt; +} + +bool SceneService::rename_object(Scene& scene, const EntityId& id, const std::string& new_name) const { + if (const auto object = find_object(scene, id)) { + object->get().name = new_name; + return true; + } + return false; +} + +bool SceneService::set_object_transform(Scene& scene, const EntityId& id, const TransformComponent& transform) const { + if (const auto object = find_object(scene, id)) { + object->get().transform = transform; + return true; + } + return false; +} + +bool SceneService::destroy_object(Scene& scene, const EntityId& id) const { + if (id.empty()) { + return false; + } + + std::vector to_remove{id}; + for (std::size_t index = 0; index < to_remove.size(); ++index) { + for (const auto& object : scene.objects) { + if (object.parent_id == to_remove[index]) { + to_remove.push_back(object.id); + } + } + } + + const auto old_size = scene.objects.size(); + scene.objects.erase( + std::remove_if( + scene.objects.begin(), + scene.objects.end(), + [&to_remove](const SceneObject& object) { + return std::find(to_remove.begin(), to_remove.end(), object.id) != to_remove.end(); + } + ), + scene.objects.end() + ); + return scene.objects.size() != old_size; +} + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/Scene.h b/MetaCore/engine/scene/Scene.h new file mode 100644 index 0000000..3ff277c --- /dev/null +++ b/MetaCore/engine/scene/Scene.h @@ -0,0 +1,32 @@ +#pragma once + +#include "MetaCore/engine/scene/Components.h" + +#include +#include +#include +#include + +namespace metacore::engine::scene { + +struct Scene { + std::string name = "Main"; + std::vector objects; +}; + +class SceneService { +public: + Scene create_default_scene() const; + SceneObject& create_object(Scene& scene, const std::string& name, const EntityId& parent_id = {}) const; + std::optional> find_object(Scene& scene, const EntityId& id) const; + std::optional> find_object(const Scene& scene, const EntityId& id) const; + bool rename_object(Scene& scene, const EntityId& id, const std::string& new_name) const; + bool set_object_transform( + Scene& scene, + const EntityId& id, + const TransformComponent& transform + ) const; + bool destroy_object(Scene& scene, const EntityId& id) const; +}; + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/SceneSerializer.cpp b/MetaCore/engine/scene/SceneSerializer.cpp new file mode 100644 index 0000000..b1bb955 --- /dev/null +++ b/MetaCore/engine/scene/SceneSerializer.cpp @@ -0,0 +1,204 @@ +#include "MetaCore/engine/scene/SceneSerializer.h" + +#include "MetaCore/foundation/FileSystem.h" +#include "MetaCore/foundation/Json.h" + +namespace metacore::engine::scene { + +using foundation::JsonValue; + +namespace { + +JsonValue array3(const std::array& values) { + JsonValue::array_type array; + array.emplace_back(values[0]); + array.emplace_back(values[1]); + array.emplace_back(values[2]); + return JsonValue(std::move(array)); +} + +std::array read_array3(const JsonValue& value, const std::array& fallback) { + if (!value.is_array() || value.as_array().size() != 3) { + return fallback; + } + return { + static_cast(value.as_array()[0].as_number(fallback[0])), + static_cast(value.as_array()[1].as_number(fallback[1])), + static_cast(value.as_array()[2].as_number(fallback[2])) + }; +} + +std::string light_type_to_string(const LightComponent::Type type) { + switch (type) { + case LightComponent::Type::Directional: + return "directional"; + case LightComponent::Type::Point: + return "point"; + case LightComponent::Type::Spot: + return "spot"; + } + return "directional"; +} + +LightComponent::Type light_type_from_string(const std::string& type) { + if (type == "point") { + return LightComponent::Type::Point; + } + if (type == "spot") { + return LightComponent::Type::Spot; + } + return LightComponent::Type::Directional; +} + +JsonValue scene_object_to_json(const SceneObject& object) { + JsonValue::object_type root; + root["id"] = object.id; + root["parent_id"] = object.parent_id; + root["name"] = object.name; + + JsonValue::object_type transform; + transform["position"] = array3(object.transform.position); + transform["rotation"] = array3(object.transform.rotation); + transform["scale"] = array3(object.transform.scale); + root["transform"] = JsonValue(std::move(transform)); + + if (object.has_mesh_renderer) { + JsonValue::object_type mesh; + mesh["mesh_asset_id"] = object.mesh_renderer.mesh_asset_id; + mesh["material_asset_id"] = object.mesh_renderer.material_asset_id; + mesh["visible"] = object.mesh_renderer.visible; + root["mesh_renderer"] = JsonValue(std::move(mesh)); + } + if (object.has_light) { + JsonValue::object_type light; + light["type"] = light_type_to_string(object.light.type); + light["color"] = array3(object.light.color); + light["intensity"] = object.light.intensity; + light["range"] = object.light.range; + light["spot_angle_degrees"] = object.light.spot_angle_degrees; + root["light"] = JsonValue(std::move(light)); + } + if (object.has_camera) { + JsonValue::object_type camera; + camera["fov_degrees"] = object.camera.fov_degrees; + camera["near_clip"] = object.camera.near_clip; + camera["far_clip"] = object.camera.far_clip; + camera["primary"] = object.camera.primary; + root["camera"] = JsonValue(std::move(camera)); + } + if (object.has_ui_document) { + JsonValue::object_type ui; + ui["document_path"] = object.ui_document.document_path; + ui["stylesheet_path"] = object.ui_document.stylesheet_path; + ui["visible"] = object.ui_document.visible; + root["ui_document"] = JsonValue(std::move(ui)); + } + if (object.has_collision) { + JsonValue::object_type collision; + collision["enabled"] = object.collision.enabled; + collision["shape"] = object.collision.shape; + root["collision"] = JsonValue(std::move(collision)); + } + + JsonValue::object_type editor; + editor["editor_only"] = object.editor_metadata.editor_only; + editor["selected"] = object.editor_metadata.selected; + editor["locked"] = object.editor_metadata.locked; + root["editor_metadata"] = JsonValue(std::move(editor)); + + return JsonValue(std::move(root)); +} + +SceneObject scene_object_from_json(const JsonValue& json) { + SceneObject object; + object.id = json["id"].as_string(); + object.parent_id = json["parent_id"].as_string(); + object.name = json["name"].as_string(); + + const auto& transform = json["transform"]; + object.transform.position = read_array3(transform["position"], object.transform.position); + object.transform.rotation = read_array3(transform["rotation"], object.transform.rotation); + object.transform.scale = read_array3(transform["scale"], object.transform.scale); + + if (json.contains("mesh_renderer")) { + const auto& mesh = json["mesh_renderer"]; + object.has_mesh_renderer = true; + object.mesh_renderer.mesh_asset_id = mesh["mesh_asset_id"].as_string(); + object.mesh_renderer.material_asset_id = mesh["material_asset_id"].as_string(); + object.mesh_renderer.visible = mesh["visible"].as_bool(true); + } + if (json.contains("light")) { + const auto& light = json["light"]; + object.has_light = true; + object.light.type = light_type_from_string(light["type"].as_string()); + object.light.color = read_array3(light["color"], object.light.color); + object.light.intensity = static_cast(light["intensity"].as_number(object.light.intensity)); + object.light.range = static_cast(light["range"].as_number(object.light.range)); + object.light.spot_angle_degrees = static_cast(light["spot_angle_degrees"].as_number(object.light.spot_angle_degrees)); + } + if (json.contains("camera")) { + const auto& camera = json["camera"]; + object.has_camera = true; + object.camera.fov_degrees = static_cast(camera["fov_degrees"].as_number(object.camera.fov_degrees)); + object.camera.near_clip = static_cast(camera["near_clip"].as_number(object.camera.near_clip)); + object.camera.far_clip = static_cast(camera["far_clip"].as_number(object.camera.far_clip)); + object.camera.primary = camera["primary"].as_bool(false); + } + if (json.contains("ui_document")) { + const auto& ui = json["ui_document"]; + object.has_ui_document = true; + object.ui_document.document_path = ui["document_path"].as_string(); + object.ui_document.stylesheet_path = ui["stylesheet_path"].as_string(); + object.ui_document.visible = ui["visible"].as_bool(true); + } + if (json.contains("collision")) { + const auto& collision = json["collision"]; + object.has_collision = true; + object.collision.enabled = collision["enabled"].as_bool(false); + object.collision.shape = collision["shape"].as_string(); + } + + const auto& editor = json["editor_metadata"]; + object.editor_metadata.editor_only = editor["editor_only"].as_bool(false); + object.editor_metadata.selected = editor["selected"].as_bool(false); + object.editor_metadata.locked = editor["locked"].as_bool(false); + + return object; +} + +JsonValue scene_to_json(const Scene& scene) { + JsonValue::object_type root; + root["name"] = scene.name; + JsonValue::array_type objects; + for (const auto& object : scene.objects) { + objects.push_back(scene_object_to_json(object)); + } + root["objects"] = JsonValue(std::move(objects)); + return JsonValue(std::move(root)); +} + +Scene scene_from_json(const JsonValue& json) { + Scene scene; + scene.name = json["name"].as_string(); + for (const auto& object : json["objects"].as_array()) { + scene.objects.push_back(scene_object_from_json(object)); + } + return scene; +} + +} // namespace + +bool SceneSerializer::save_scene(const std::filesystem::path& scene_file, const Scene& scene) const { + return foundation::write_text_file(scene_file, scene_to_json(scene).stringify(2)); +} + +bool SceneSerializer::load_scene(const std::filesystem::path& scene_file, Scene& out_scene) const { + const auto text = foundation::read_text_file(scene_file); + if (!text.has_value()) { + return false; + } + out_scene = scene_from_json(JsonValue::parse(*text)); + return true; +} + +} // namespace metacore::engine::scene diff --git a/MetaCore/engine/scene/SceneSerializer.h b/MetaCore/engine/scene/SceneSerializer.h new file mode 100644 index 0000000..c46eaa5 --- /dev/null +++ b/MetaCore/engine/scene/SceneSerializer.h @@ -0,0 +1,15 @@ +#pragma once + +#include "MetaCore/engine/scene/Scene.h" + +#include + +namespace metacore::engine::scene { + +class SceneSerializer { +public: + bool save_scene(const std::filesystem::path& scene_file, const Scene& scene) const; + bool load_scene(const std::filesystem::path& scene_file, Scene& out_scene) const; +}; + +} // namespace metacore::engine::scene diff --git a/MetaCore/foundation/FileSystem.cpp b/MetaCore/foundation/FileSystem.cpp new file mode 100644 index 0000000..0151fa7 --- /dev/null +++ b/MetaCore/foundation/FileSystem.cpp @@ -0,0 +1,44 @@ +#include "MetaCore/foundation/FileSystem.h" + +#include +#include + +namespace metacore::foundation { + +bool ensure_directory(const std::filesystem::path& path) { + if (path.empty()) { + return false; + } + std::error_code ec; + if (std::filesystem::exists(path, ec)) { + return true; + } + return std::filesystem::create_directories(path, ec); +} + +bool write_text_file(const std::filesystem::path& path, const std::string& contents) { + if (!path.parent_path().empty()) { + ensure_directory(path.parent_path()); + } + + std::ofstream output(path, std::ios::binary); + if (!output.is_open()) { + return false; + } + + output << contents; + return output.good(); +} + +std::optional read_text_file(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + std::ostringstream stream; + stream << input.rdbuf(); + return stream.str(); +} + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/FileSystem.h b/MetaCore/foundation/FileSystem.h new file mode 100644 index 0000000..4665b09 --- /dev/null +++ b/MetaCore/foundation/FileSystem.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +namespace metacore::foundation { + +bool ensure_directory(const std::filesystem::path& path); +bool write_text_file(const std::filesystem::path& path, const std::string& contents); +std::optional read_text_file(const std::filesystem::path& path); + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Guid.cpp b/MetaCore/foundation/Guid.cpp new file mode 100644 index 0000000..73fb0ea --- /dev/null +++ b/MetaCore/foundation/Guid.cpp @@ -0,0 +1,30 @@ +#include "MetaCore/foundation/Guid.h" + +#include +#include +#include +#include +#include + +namespace metacore::foundation { + +std::string generate_guid() { + static std::mt19937_64 rng(std::random_device{}()); + static std::uniform_int_distribution dist; + static std::atomic counter{0}; + + const auto now = static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count() + ); + const auto salt = dist(rng); + const auto count = ++counter; + + std::ostringstream stream; + stream << std::hex << std::setfill('0') + << std::setw(16) << now + << std::setw(16) << salt + << std::setw(8) << count; + return stream.str(); +} + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Guid.h b/MetaCore/foundation/Guid.h new file mode 100644 index 0000000..b3f7d1d --- /dev/null +++ b/MetaCore/foundation/Guid.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace metacore::foundation { + +std::string generate_guid(); + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Json.cpp b/MetaCore/foundation/Json.cpp new file mode 100644 index 0000000..3ff89eb --- /dev/null +++ b/MetaCore/foundation/Json.cpp @@ -0,0 +1,339 @@ +#include "MetaCore/foundation/Json.h" + +#include +#include +#include +#include + +namespace metacore::foundation { + +namespace { + +class Parser { +public: + explicit Parser(std::string_view source) : source_(source) {} + + JsonValue parse() { + skip_ws(); + JsonValue value = parse_value(); + skip_ws(); + return value; + } + +private: + JsonValue parse_value() { + skip_ws(); + if (match("null")) { + return JsonValue(nullptr); + } + if (match("true")) { + return JsonValue(true); + } + if (match("false")) { + return JsonValue(false); + } + if (peek() == '"') { + return JsonValue(parse_string()); + } + if (peek() == '{') { + return JsonValue(parse_object()); + } + if (peek() == '[') { + return JsonValue(parse_array()); + } + return JsonValue(parse_number()); + } + + JsonValue::object_type parse_object() { + expect('{'); + JsonValue::object_type object; + skip_ws(); + if (peek() == '}') { + expect('}'); + return object; + } + while (true) { + const std::string key = parse_string(); + skip_ws(); + expect(':'); + object.emplace(key, parse_value()); + skip_ws(); + if (peek() == '}') { + expect('}'); + break; + } + expect(','); + } + return object; + } + + JsonValue::array_type parse_array() { + expect('['); + JsonValue::array_type array; + skip_ws(); + if (peek() == ']') { + expect(']'); + return array; + } + while (true) { + array.emplace_back(parse_value()); + skip_ws(); + if (peek() == ']') { + expect(']'); + break; + } + expect(','); + } + return array; + } + + std::string parse_string() { + expect('"'); + std::ostringstream output; + while (!eof()) { + const char ch = source_[index_++]; + if (ch == '"') { + break; + } + if (ch == '\\') { + const char escaped = source_[index_++]; + switch (escaped) { + case '"': + case '\\': + case '/': + output << escaped; + break; + case 'b': + output << '\b'; + break; + case 'f': + output << '\f'; + break; + case 'n': + output << '\n'; + break; + case 'r': + output << '\r'; + break; + case 't': + output << '\t'; + break; + default: + throw std::runtime_error("Unsupported escape sequence in JSON string"); + } + continue; + } + output << ch; + } + return output.str(); + } + + double parse_number() { + const std::size_t start = index_; + while (!eof()) { + const char ch = source_[index_]; + if (!(std::isdigit(static_cast(ch)) || ch == '.' || ch == '-' || ch == '+' || ch == 'e' || ch == 'E')) { + break; + } + ++index_; + } + return std::stod(std::string(source_.substr(start, index_ - start))); + } + + void skip_ws() { + while (!eof() && std::isspace(static_cast(source_[index_]))) { + ++index_; + } + } + + bool eof() const { + return index_ >= source_.size(); + } + + char peek() const { + return eof() ? '\0' : source_[index_]; + } + + void expect(const char expected) { + skip_ws(); + if (peek() != expected) { + throw std::runtime_error("Unexpected token while parsing JSON"); + } + ++index_; + } + + bool match(const std::string_view text) { + skip_ws(); + if (source_.substr(index_, text.size()) == text) { + index_ += text.size(); + return true; + } + return false; + } + + std::string_view source_; + std::size_t index_ = 0; +}; + +std::string make_indent(const int depth) { + return std::string(static_cast(depth), ' '); +} + +std::string escape_json(const std::string& text) { + std::ostringstream output; + for (const char ch : text) { + switch (ch) { + case '\\': + output << "\\\\"; + break; + case '"': + output << "\\\""; + break; + case '\n': + output << "\\n"; + break; + case '\r': + output << "\\r"; + break; + case '\t': + output << "\\t"; + break; + default: + output << ch; + break; + } + } + return output.str(); +} + +std::string stringify_value(const JsonValue& value, const int indent, const int depth) { + if (value.is_null()) { + return "null"; + } + if (value.is_bool()) { + return value.as_bool() ? "true" : "false"; + } + if (value.is_number()) { + std::ostringstream stream; + stream << std::setprecision(15) << value.as_number(); + return stream.str(); + } + if (value.is_string()) { + return "\"" + escape_json(value.as_string()) + "\""; + } + if (value.is_array()) { + const auto& array = value.as_array(); + if (array.empty()) { + return "[]"; + } + std::ostringstream stream; + stream << "[\n"; + for (std::size_t index = 0; index < array.size(); ++index) { + stream << make_indent(depth + indent) + << stringify_value(array[index], indent, depth + indent); + if (index + 1 < array.size()) { + stream << ","; + } + stream << '\n'; + } + stream << make_indent(depth) << "]"; + return stream.str(); + } + + const auto& object = value.as_object(); + if (object.empty()) { + return "{}"; + } + std::ostringstream stream; + stream << "{\n"; + std::size_t index = 0; + for (const auto& [key, child] : object) { + stream << make_indent(depth + indent) + << "\"" << escape_json(key) << "\": " + << stringify_value(child, indent, depth + indent); + if (++index < object.size()) { + stream << ","; + } + stream << '\n'; + } + stream << make_indent(depth) << "}"; + return stream.str(); +} + +} // namespace + +JsonValue::JsonValue() : value_(nullptr) {} +JsonValue::JsonValue(std::nullptr_t) : value_(nullptr) {} +JsonValue::JsonValue(const bool value) : value_(value) {} +JsonValue::JsonValue(const double value) : value_(value) {} +JsonValue::JsonValue(const int value) : value_(static_cast(value)) {} +JsonValue::JsonValue(std::string value) : value_(std::move(value)) {} +JsonValue::JsonValue(const char* value) : value_(std::string(value)) {} +JsonValue::JsonValue(array_type value) : value_(std::move(value)) {} +JsonValue::JsonValue(object_type value) : value_(std::move(value)) {} + +bool JsonValue::is_null() const { return std::holds_alternative(value_); } +bool JsonValue::is_bool() const { return std::holds_alternative(value_); } +bool JsonValue::is_number() const { return std::holds_alternative(value_); } +bool JsonValue::is_string() const { return std::holds_alternative(value_); } +bool JsonValue::is_array() const { return std::holds_alternative(value_); } +bool JsonValue::is_object() const { return std::holds_alternative(value_); } + +bool JsonValue::as_bool(const bool fallback) const { + return is_bool() ? std::get(value_) : fallback; +} + +double JsonValue::as_number(const double fallback) const { + return is_number() ? std::get(value_) : fallback; +} + +int JsonValue::as_int(const int fallback) const { + return static_cast(as_number(static_cast(fallback))); +} + +const std::string& JsonValue::as_string() const { + static const std::string empty; + return is_string() ? std::get(value_) : empty; +} + +const JsonValue::array_type& JsonValue::as_array() const { + static const array_type empty; + return is_array() ? std::get(value_) : empty; +} + +const JsonValue::object_type& JsonValue::as_object() const { + static const object_type empty; + return is_object() ? std::get(value_) : empty; +} + +JsonValue& JsonValue::operator[](const std::string& key) { + if (!is_object()) { + value_ = object_type{}; + } + return std::get(value_)[key]; +} + +const JsonValue& JsonValue::operator[](const std::string& key) const { + static const JsonValue null_value; + if (!is_object()) { + return null_value; + } + const auto& object = std::get(value_); + const auto it = object.find(key); + return it == object.end() ? null_value : it->second; +} + +bool JsonValue::contains(const std::string& key) const { + return is_object() && std::get(value_).contains(key); +} + +std::string JsonValue::stringify(const int indent) const { + return stringify_value(*this, indent, 0); +} + +JsonValue JsonValue::parse(const std::string_view text) { + Parser parser(text); + return parser.parse(); +} + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Json.h b/MetaCore/foundation/Json.h new file mode 100644 index 0000000..3974162 --- /dev/null +++ b/MetaCore/foundation/Json.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace metacore::foundation { + +class JsonValue { +public: + using object_type = std::map; + using array_type = std::vector; + using value_type = std::variant; + + JsonValue(); + JsonValue(std::nullptr_t); + JsonValue(bool value); + JsonValue(double value); + JsonValue(int value); + JsonValue(std::string value); + JsonValue(const char* value); + JsonValue(array_type value); + JsonValue(object_type value); + + [[nodiscard]] bool is_null() const; + [[nodiscard]] bool is_bool() const; + [[nodiscard]] bool is_number() const; + [[nodiscard]] bool is_string() const; + [[nodiscard]] bool is_array() const; + [[nodiscard]] bool is_object() const; + + [[nodiscard]] bool as_bool(bool fallback = false) const; + [[nodiscard]] double as_number(double fallback = 0.0) const; + [[nodiscard]] int as_int(int fallback = 0) const; + [[nodiscard]] const std::string& as_string() const; + [[nodiscard]] const array_type& as_array() const; + [[nodiscard]] const object_type& as_object() const; + + JsonValue& operator[](const std::string& key); + const JsonValue& operator[](const std::string& key) const; + + [[nodiscard]] bool contains(const std::string& key) const; + [[nodiscard]] std::string stringify(int indent = 2) const; + [[nodiscard]] static JsonValue parse(std::string_view text); + +private: + value_type value_; +}; + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Log.cpp b/MetaCore/foundation/Log.cpp new file mode 100644 index 0000000..4d5dd08 --- /dev/null +++ b/MetaCore/foundation/Log.cpp @@ -0,0 +1,30 @@ +#include "MetaCore/foundation/Log.h" + +#include + +namespace metacore::foundation { + +namespace { + +const char* to_prefix(const LogLevel level) { + switch (level) { + case LogLevel::Debug: + return "[MetaCore][Debug] "; + case LogLevel::Info: + return "[MetaCore][Info] "; + case LogLevel::Warning: + return "[MetaCore][Warning] "; + case LogLevel::Error: + return "[MetaCore][Error] "; + } + return "[MetaCore] "; +} + +} // namespace + +void log(const LogLevel level, const std::string_view message) { + std::ostream& stream = (level == LogLevel::Error) ? std::cerr : std::cout; + stream << to_prefix(level) << message << '\n'; +} + +} // namespace metacore::foundation diff --git a/MetaCore/foundation/Log.h b/MetaCore/foundation/Log.h new file mode 100644 index 0000000..207dcf8 --- /dev/null +++ b/MetaCore/foundation/Log.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace metacore::foundation { + +enum class LogLevel { + Debug, + Info, + Warning, + Error, +}; + +void log(LogLevel level, std::string_view message); + +} // namespace metacore::foundation diff --git a/MetaCore/platform/panda/PandaPlatformContext.cpp b/MetaCore/platform/panda/PandaPlatformContext.cpp new file mode 100644 index 0000000..5adea56 --- /dev/null +++ b/MetaCore/platform/panda/PandaPlatformContext.cpp @@ -0,0 +1,161 @@ +#include "MetaCore/platform/panda/PandaPlatformContext.h" + +#include "MetaCore/foundation/Log.h" + +#if METACORE_HAS_PANDA3D +#include +#include +#endif + +#include +#include +#include + +namespace metacore::platform::panda { + +#if METACORE_HAS_PANDA3D +struct PandaPlatformContext::NativeState { + PandaFramework framework; + WindowFramework* window = nullptr; + bool framework_open = false; +}; +#else +struct PandaPlatformContext::NativeState {}; +#endif + +PandaPlatformContext::PandaPlatformContext() = default; + +PandaPlatformContext::~PandaPlatformContext() = default; + +bool PandaPlatformContext::initialize(const std::string& window_title) { + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: begin\n" << std::flush; + foundation::log(foundation::LogLevel::Info, "MetaCore platform init: begin"); + window_title_ = window_title; + char* enable_window_env = nullptr; + std::size_t env_size = 0; + _dupenv_s(&enable_window_env, &env_size, "METACORE_ENABLE_PANDA_WINDOW"); + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: env read\n" << std::flush; + window_enabled_ = enable_window_env != nullptr && std::string(enable_window_env) == "1"; + free(enable_window_env); + window_opened_ = false; + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: env parsed\n" << std::flush; + foundation::log( + foundation::LogLevel::Info, + "MetaCore platform init: window bootstrap requested=" + std::string(window_enabled_ ? "true" : "false") + ); +#if METACORE_HAS_PANDA3D + backend_name_ = window_enabled_ ? "panda3d-window" : "panda3d-linked"; + panda_version_ = "linked"; + panda_platform_ = "windows"; + panda_build_date_ = "runtime-probe-deferred"; + const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin"; + if (std::filesystem::exists(panda_bin)) { + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: setting DLL dir\n" << std::flush; + foundation::log(foundation::LogLevel::Info, "MetaCore platform init: setting Panda bin path to " + panda_bin.string()); + SetDllDirectoryW(panda_bin.wstring().c_str()); + } + native_state_ = std::make_unique(); + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: native state ready\n" << std::flush; + foundation::log(foundation::LogLevel::Info, "MetaCore platform init: native state allocated"); + if (window_enabled_) { + try { + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before open_framework\n" << std::flush; + native_state_->framework.open_framework(); + native_state_->framework_open = true; + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after open_framework\n" << std::flush; + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before WindowProperties\n" << std::flush; + WindowProperties props; + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after WindowProperties\n" << std::flush; + props.set_size(1280, 720); + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after set_size\n" << std::flush; + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before open_window\n" << std::flush; + native_state_->window = native_state_->framework.open_window(props, 0); + std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after open_window\n" << std::flush; + window_opened_ = native_state_->window != nullptr; + if (!window_opened_) { + foundation::log(foundation::LogLevel::Warning, "Panda window bootstrap requested but open_window returned null"); + } + } catch (...) { + window_opened_ = false; + backend_name_ = "panda3d-linked"; + foundation::log(foundation::LogLevel::Warning, "Panda window bootstrap failed; falling back to linked-only mode"); + } + } +#else + backend_name_ = "panda-stub"; + panda_version_.clear(); + panda_platform_.clear(); + panda_build_date_.clear(); +#endif + initialized_ = true; + foundation::log(foundation::LogLevel::Info, "MetaCore platform init: complete"); + return true; +} + +void PandaPlatformContext::shutdown() { + initialized_ = false; + window_title_.clear(); + backend_name_ = "panda-stub"; + panda_version_.clear(); + panda_platform_.clear(); + panda_build_date_.clear(); + window_enabled_ = false; + window_opened_ = false; +#if METACORE_HAS_PANDA3D + if (native_state_) { + if (native_state_->framework_open) { + if (native_state_->window != nullptr) { + native_state_->framework.close_window(native_state_->window); + native_state_->window = nullptr; + } + native_state_->framework.close_framework(); + native_state_->framework_open = false; + } + native_state_.reset(); + } +#else + native_state_.reset(); +#endif +} + +void PandaPlatformContext::pump_frame() { +#if METACORE_HAS_PANDA3D + if (window_opened_ && native_state_ && native_state_->framework_open) { + native_state_->framework.do_frame(nullptr); + } +#endif +} + +bool PandaPlatformContext::initialized() const { + return initialized_; +} + +const std::string& PandaPlatformContext::window_title() const { + return window_title_; +} + +std::filesystem::path PandaPlatformContext::backend_name() const { + return backend_name_; +} + +const std::string& PandaPlatformContext::panda_version() const { + return panda_version_; +} + +const std::string& PandaPlatformContext::panda_platform() const { + return panda_platform_; +} + +const std::string& PandaPlatformContext::panda_build_date() const { + return panda_build_date_; +} + +bool PandaPlatformContext::window_enabled() const { + return window_enabled_; +} + +bool PandaPlatformContext::window_opened() const { + return window_opened_; +} + +} // namespace metacore::platform::panda diff --git a/MetaCore/platform/panda/PandaPlatformContext.h b/MetaCore/platform/panda/PandaPlatformContext.h new file mode 100644 index 0000000..cc04e2b --- /dev/null +++ b/MetaCore/platform/panda/PandaPlatformContext.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace metacore::platform::panda { + +class PandaPlatformContext { +public: + PandaPlatformContext(); + ~PandaPlatformContext(); + + bool initialize(const std::string& window_title); + void shutdown(); + void pump_frame(); + + [[nodiscard]] bool initialized() const; + [[nodiscard]] const std::string& window_title() const; + [[nodiscard]] std::filesystem::path backend_name() const; + [[nodiscard]] const std::string& panda_version() const; + [[nodiscard]] const std::string& panda_platform() const; + [[nodiscard]] const std::string& panda_build_date() const; + [[nodiscard]] bool window_enabled() const; + [[nodiscard]] bool window_opened() const; + +private: + struct NativeState; + + bool initialized_ = false; + std::string window_title_; + std::string backend_name_ = "panda-stub"; + std::string panda_version_; + std::string panda_platform_; + std::string panda_build_date_; + bool window_enabled_ = false; + bool window_opened_ = false; + std::unique_ptr native_state_; +}; + +} // namespace metacore::platform::panda diff --git a/MetaCore/platform/panda/PandaRenderBackend.cpp b/MetaCore/platform/panda/PandaRenderBackend.cpp new file mode 100644 index 0000000..899e1ba --- /dev/null +++ b/MetaCore/platform/panda/PandaRenderBackend.cpp @@ -0,0 +1,30 @@ +#include "MetaCore/platform/panda/PandaRenderBackend.h" + +namespace metacore::platform::panda { + +bool PandaRenderBackend::initialize() { + initialized_ = true; + return true; +} + +void PandaRenderBackend::shutdown() { + initialized_ = false; +} + +void PandaRenderBackend::set_frame_uniforms(const metacore::engine::render::FrameUniforms& uniforms) { + frame_uniforms_ = uniforms; +} + +void PandaRenderBackend::set_light_uniforms(const metacore::engine::render::LightUniforms& uniforms) { + light_uniforms_ = uniforms; +} + +const metacore::engine::render::FrameUniforms& PandaRenderBackend::frame_uniforms() const { + return frame_uniforms_; +} + +const metacore::engine::render::LightUniforms& PandaRenderBackend::light_uniforms() const { + return light_uniforms_; +} + +} // namespace metacore::platform::panda diff --git a/MetaCore/platform/panda/PandaRenderBackend.h b/MetaCore/platform/panda/PandaRenderBackend.h new file mode 100644 index 0000000..5d10819 --- /dev/null +++ b/MetaCore/platform/panda/PandaRenderBackend.h @@ -0,0 +1,23 @@ +#pragma once + +#include "MetaCore/engine/render/RenderTypes.h" + +namespace metacore::platform::panda { + +class PandaRenderBackend final : public metacore::engine::render::IMetaRenderBackend { +public: + bool initialize() override; + void shutdown() override; + void set_frame_uniforms(const metacore::engine::render::FrameUniforms& uniforms) override; + void set_light_uniforms(const metacore::engine::render::LightUniforms& uniforms) override; + + [[nodiscard]] const metacore::engine::render::FrameUniforms& frame_uniforms() const; + [[nodiscard]] const metacore::engine::render::LightUniforms& light_uniforms() const; + +private: + bool initialized_ = false; + metacore::engine::render::FrameUniforms frame_uniforms_{}; + metacore::engine::render::LightUniforms light_uniforms_{}; +}; + +} // namespace metacore::platform::panda diff --git a/MetaEditor/DearImGuiEditorShell.cpp b/MetaEditor/DearImGuiEditorShell.cpp new file mode 100644 index 0000000..b716538 --- /dev/null +++ b/MetaEditor/DearImGuiEditorShell.cpp @@ -0,0 +1,683 @@ +#include "MetaEditor/DearImGuiEditorShell.h" + +#include "MetaCore/foundation/FileSystem.h" + +#if METACORE_HAS_IMGUI +#include +#include +#endif + +#if METACORE_HAS_IMGUIZMO +#include +#endif + +#include +#include +#include +#include + +namespace metacore::editor { + +namespace { + +#if METACORE_HAS_IMGUIZMO +ImGuizmo::OPERATION to_imguizmo_operation(const std::string& operation) { + if (operation == "Rotate") { + return ImGuizmo::ROTATE; + } + if (operation == "Scale") { + return ImGuizmo::SCALE; + } + return ImGuizmo::TRANSLATE; +} + +ImGuizmo::MODE to_imguizmo_mode(const std::string& space) { + return space == "World" ? ImGuizmo::WORLD : ImGuizmo::LOCAL; +} + +std::array make_transform_matrix(const EditorFrameSnapshot::ViewportSnapshot& viewport) { + const float rx = viewport.object_rotation[0] * 0.01745329252F; + const float ry = viewport.object_rotation[1] * 0.01745329252F; + const float rz = viewport.object_rotation[2] * 0.01745329252F; + + const float cx = std::cos(rx); + const float sx = std::sin(rx); + const float cy = std::cos(ry); + const float sy = std::sin(ry); + const float cz = std::cos(rz); + const float sz = std::sin(rz); + + const float r00 = cz * cy; + const float r01 = cz * sy * sx - sz * cx; + const float r02 = cz * sy * cx + sz * sx; + const float r10 = sz * cy; + const float r11 = sz * sy * sx + cz * cx; + const float r12 = sz * sy * cx - cz * sx; + const float r20 = -sy; + const float r21 = cy * sx; + const float r22 = cy * cx; + + return { + r00 * viewport.object_scale[0], r10 * viewport.object_scale[0], r20 * viewport.object_scale[0], 0.0F, + r01 * viewport.object_scale[1], r11 * viewport.object_scale[1], r21 * viewport.object_scale[1], 0.0F, + r02 * viewport.object_scale[2], r12 * viewport.object_scale[2], r22 * viewport.object_scale[2], 0.0F, + viewport.object_position[0], viewport.object_position[1], viewport.object_position[2], 1.0F + }; +} + +std::array make_view_matrix(const std::array& camera_position) { + return { + 1.0F, 0.0F, 0.0F, 0.0F, + 0.0F, 1.0F, 0.0F, 0.0F, + 0.0F, 0.0F, 1.0F, 0.0F, + -camera_position[0], -camera_position[1], -camera_position[2], 1.0F + }; +} + +std::array make_projection_matrix(float width, float height) { + const float aspect = height > 0.0F ? width / height : 1.0F; + const float fov = 60.0F * 0.01745329252F; + const float f = 1.0F / std::tan(fov * 0.5F); + const float near_plane = 0.1F; + const float far_plane = 1000.0F; + return { + f / aspect, 0.0F, 0.0F, 0.0F, + 0.0F, f, 0.0F, 0.0F, + 0.0F, 0.0F, far_plane / (far_plane - near_plane), 1.0F, + 0.0F, 0.0F, (-near_plane * far_plane) / (far_plane - near_plane), 0.0F + }; +} + +#endif + +} // namespace + +bool DearImGuiEditorShell::initialize() { + const char* headless_frame_env = std::getenv("METACORE_ENABLE_HEADLESS_IMGUI"); + headless_frame_enabled_ = headless_frame_env != nullptr && std::string(headless_frame_env) == "1"; + +#if METACORE_HAS_IMGUI + if (headless_frame_enabled_) { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.DisplaySize = ImVec2(1600.0F, 900.0F); + io.DeltaTime = 1.0F / 60.0F; + unsigned char* font_pixels = nullptr; + int font_width = 0; + int font_height = 0; + io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height); +#if METACORE_HAS_IMGUIZMO + ImGuizmo::SetImGuiContext(ImGui::GetCurrentContext()); +#endif + ImGui::StyleColorsDark(); + } +#endif + initialized_ = true; + return true; +} + +void DearImGuiEditorShell::shutdown() { +#if METACORE_HAS_IMGUI + if (headless_frame_enabled_ && ImGui::GetCurrentContext() != nullptr) { + ImGui::DestroyContext(); + } +#endif + initialized_ = false; + last_snapshot_ = {}; + last_draw_command_count_ = 0; + last_vertex_count_ = 0; + last_index_count_ = 0; + last_window_count_ = 0; + headless_frame_enabled_ = false; + dock_layout_initialized_ = false; + headless_transform_override_consumed_ = false; +} + +void DearImGuiEditorShell::consume_snapshot(const EditorFrameSnapshot& snapshot) { + last_snapshot_ = snapshot; + enqueue_headless_transform_edit(snapshot); + +#if METACORE_HAS_IMGUI + if (headless_frame_enabled_ && ImGui::GetCurrentContext() != nullptr) { + render_headless_frame(snapshot); + if (const ImDrawData* draw_data = ImGui::GetDrawData(); draw_data != nullptr) { + last_draw_command_count_ = 0; + last_vertex_count_ = draw_data->TotalVtxCount; + last_index_count_ = draw_data->TotalIdxCount; + last_window_count_ = draw_data->CmdListsCount; + for (int index = 0; index < draw_data->CmdListsCount; ++index) { + last_draw_command_count_ += draw_data->CmdLists[index]->CmdBuffer.Size; + } + } else { + last_draw_command_count_ = 0; + last_vertex_count_ = 0; + last_index_count_ = 0; + last_window_count_ = 0; + } + } else { + last_draw_command_count_ = 0; + last_vertex_count_ = 0; + last_index_count_ = 0; + last_window_count_ = 0; + } +#endif + + export_debug_snapshot(); +} + +void DearImGuiEditorShell::enqueue_headless_transform_edit(const EditorFrameSnapshot& snapshot) { + if (!headless_frame_enabled_ || !snapshot.viewport.has_selection || headless_transform_override_consumed_) { + return; + } + + const char* translate_env = std::getenv("METACORE_HEADLESS_GIZMO_TRANSLATE"); + if (translate_env == nullptr) { + return; + } + + std::istringstream stream(translate_env); + float x = snapshot.viewport.object_position[0]; + float y = snapshot.viewport.object_position[1]; + float z = snapshot.viewport.object_position[2]; + if (!(stream >> x >> y >> z)) { + return; + } + + PendingShellEdits::TransformEdit edit; + edit.entity_id = snapshot.viewport.selected_object_id; + edit.position = {x, y, z}; + edit.rotation = snapshot.viewport.object_rotation; + edit.scale = snapshot.viewport.object_scale; + pending_edits_.transform_edit = edit; + headless_transform_override_consumed_ = true; +} + +void DearImGuiEditorShell::render_headless_frame(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(1600.0F, 900.0F); + io.DeltaTime = 1.0F / 60.0F; + + ImGui::NewFrame(); + render_dockspace_frame(snapshot); + ImGui::Render(); +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_dockspace_frame(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + + constexpr ImGuiWindowFlags host_window_flags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_MenuBar; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + ImGui::Begin("MetaCore DockSpace", nullptr, host_window_flags); + ImGui::PopStyleVar(3); + + if (ImGui::BeginMenuBar()) { + for (const auto& menu : snapshot.shell.menus) { + if (ImGui::BeginMenu(menu.menu_name.c_str())) { + for (const auto& action : menu.actions) { + ImGui::MenuItem(action.label.c_str(), action.shortcut.empty() ? nullptr : action.shortcut.c_str(), false, action.enabled); + } + ImGui::EndMenu(); + } + } + ImGui::EndMenuBar(); + } + + ImGuiID dockspace_id = ImGui::GetID("MetaCoreEditorDockspace"); + ImGui::DockSpace(dockspace_id, ImVec2(0.0F, 0.0F), ImGuiDockNodeFlags_PassthruCentralNode); + ImGui::End(); + + ensure_default_dock_layout(snapshot, dockspace_id); + + render_toolbar(snapshot); + for (const auto& section : snapshot.shell.sections) { + for (const auto& panel : section.panels) { + render_panel(panel, snapshot); + } + } +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::ensure_default_dock_layout(const EditorFrameSnapshot& snapshot, unsigned int dockspace_id) { +#if METACORE_HAS_IMGUI + if (dock_layout_initialized_) { + return; + } + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::DockBuilderRemoveNode(dockspace_id); + ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->WorkSize); + + ImGuiID left_id = 0; + ImGuiID right_id = 0; + ImGuiID bottom_id = 0; + ImGuiID center_id = dockspace_id; + ImGuiID preview_id = 0; + + ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.22F, &left_id, ¢er_id); + ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24F, &right_id, ¢er_id); + ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26F, &bottom_id, ¢er_id); + ImGui::DockBuilderSplitNode(bottom_id, ImGuiDir_Down, 0.45F, &preview_id, &bottom_id); + + for (const auto& section : snapshot.shell.sections) { + for (const auto& panel : section.panels) { + if (!panel.visible) { + continue; + } + + ImGuiID target_id = center_id; + switch (panel.region) { + case metacore::engine::editor_core::DockRegion::LeftSidebar: + target_id = left_id; + break; + case metacore::engine::editor_core::DockRegion::RightSidebar: + target_id = right_id; + break; + case metacore::engine::editor_core::DockRegion::BottomConsole: + target_id = bottom_id; + break; + case metacore::engine::editor_core::DockRegion::BottomRuntimePreview: + target_id = preview_id; + break; + case metacore::engine::editor_core::DockRegion::CenterViewport: + target_id = center_id; + break; + case metacore::engine::editor_core::DockRegion::TopMenuBar: + case metacore::engine::editor_core::DockRegion::TopToolbar: + continue; + } + ImGui::DockBuilderDockWindow(panel.title.c_str(), target_id); + } + } + + ImGui::DockBuilderFinish(dockspace_id); + dock_layout_initialized_ = true; +#else + (void)snapshot; + (void)dockspace_id; +#endif +} + +void DearImGuiEditorShell::render_toolbar(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + if (!snapshot.toolbar_visible) { + return; + } + + if (ImGui::Begin("Toolbar", nullptr, ImGuiWindowFlags_NoCollapse)) { + for (std::size_t group_index = 0; group_index < snapshot.shell.toolbar_groups.size(); ++group_index) { + const auto& group = snapshot.shell.toolbar_groups[group_index]; + if (group_index > 0) { + ImGui::Separator(); + } + ImGui::TextUnformatted(group.name.c_str()); + for (const auto& action : group.actions) { + ImGui::SameLine(); + ImGui::BeginDisabled(!action.enabled); + ImGui::Button(action.label.c_str()); + ImGui::EndDisabled(); + } + } + } + ImGui::End(); +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_panel(const metacore::engine::editor_core::DockPanel& panel, const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + if (!panel.visible) { + return; + } + + if (panel.id == "hierarchy" && !snapshot.hierarchy_visible) { + return; + } + if (panel.id == "project" && !snapshot.project_visible) { + return; + } + if (panel.id == "inspector" && !snapshot.inspector_visible) { + return; + } + if (panel.id == "console" && !snapshot.console_visible) { + return; + } + if (panel.id == "runtime_preview" && !snapshot.runtime_preview_visible) { + return; + } + + const bool began = ImGui::Begin(panel.title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse); + if (began) { + if (panel.id == "hierarchy") { + render_hierarchy_panel(snapshot); + } else if (panel.id == "project") { + render_project_panel(snapshot); + } else if (panel.id == "viewport") { + render_viewport_panel(snapshot); + } else if (panel.id == "inspector") { + render_inspector_panel(snapshot); + } else if (panel.id == "console") { + render_console_panel(snapshot); + } else if (panel.id == "runtime_preview") { + render_runtime_preview_panel(snapshot); + } else if (panel.id == "main_menu") { + ImGui::TextUnformatted("MetaCore menu host"); + } else if (panel.id == "toolbar") { + ImGui::TextUnformatted("MetaCore toolbar host"); + } else { + ImGui::Text("Panel: %s", panel.id.c_str()); + } + } + ImGui::End(); +#else + (void)panel; + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_hierarchy_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + for (const auto& item : snapshot.hierarchy) { + ImGui::BulletText("%s", item.name.c_str()); + } +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_project_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + for (const auto& asset : snapshot.assets) { + ImGui::BulletText("%s", asset.relative_path.c_str()); + } +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_viewport_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + ImGui::Text("Scene Objects: %zu", snapshot.scene_object_count); + ImGui::Text( + "Camera: (%.1f, %.1f, %.1f)", + snapshot.viewport.camera_position[0], + snapshot.viewport.camera_position[1], + snapshot.viewport.camera_position[2] + ); + if (!snapshot.viewport.focus_target_id.empty()) { + ImGui::Text("Focus Target: %s", snapshot.viewport.focus_target_id.c_str()); + } + ImGui::Separator(); + ImGui::TextUnformatted(snapshot.status_text.c_str()); + ImGui::Spacing(); + ImGui::TextUnformatted("MetaCore Scene View"); + + if (snapshot.viewport.has_selection) { + ImGui::Text("Selection: %s", snapshot.viewport.selected_object_name.c_str()); + ImGui::Text("Operation: %s", snapshot.viewport.gizmo_operation.c_str()); + ImGui::SameLine(); + ImGui::Text("Space: %s", snapshot.viewport.gizmo_space.c_str()); + if (snapshot.viewport.snap_enabled) { + ImGui::Text( + "Snap: %.2f / %.2f / %.2f", + snapshot.viewport.snap_values[0], + snapshot.viewport.snap_values[1], + snapshot.viewport.snap_values[2] + ); + } else { + ImGui::TextUnformatted("Snap: Off"); + } + ImGui::Text( + "Transform P(%.2f, %.2f, %.2f) R(%.2f, %.2f, %.2f) S(%.2f, %.2f, %.2f)", + snapshot.viewport.object_position[0], + snapshot.viewport.object_position[1], + snapshot.viewport.object_position[2], + snapshot.viewport.object_rotation[0], + snapshot.viewport.object_rotation[1], + snapshot.viewport.object_rotation[2], + snapshot.viewport.object_scale[0], + snapshot.viewport.object_scale[1], + snapshot.viewport.object_scale[2] + ); + } else { + ImGui::TextUnformatted("Selection: none"); + } + + const ImVec2 viewport_min = ImGui::GetCursorScreenPos(); + const ImVec2 viewport_size = ImVec2( + ImGui::GetContentRegionAvail().x, + (ImGui::GetContentRegionAvail().y > 240.0F) ? ImGui::GetContentRegionAvail().y : 240.0F + ); + ImGui::InvisibleButton("MetaCoreViewportCanvas", viewport_size); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(viewport_min, ImVec2(viewport_min.x + viewport_size.x, viewport_min.y + viewport_size.y), IM_COL32(26, 29, 36, 255)); + draw_list->AddRect(ImVec2(viewport_min.x, viewport_min.y), ImVec2(viewport_min.x + viewport_size.x, viewport_min.y + viewport_size.y), IM_COL32(82, 91, 107, 255)); + draw_list->AddText(ImVec2(viewport_min.x + 14.0F, viewport_min.y + 14.0F), IM_COL32(220, 226, 235, 255), "Viewport backend handoff surface"); + +#if METACORE_HAS_IMGUIZMO + if (snapshot.viewport.has_selection) { + auto model = make_transform_matrix(snapshot.viewport); + const auto view = make_view_matrix(snapshot.viewport.camera_position); + const auto projection = make_projection_matrix(viewport_size.x, viewport_size.y); + ImGuizmo::SetDrawlist(draw_list); + ImGuizmo::SetRect(viewport_min.x, viewport_min.y, viewport_size.x, viewport_size.y); + ImGuizmo::Manipulate( + view.data(), + projection.data(), + to_imguizmo_operation(snapshot.viewport.gizmo_operation), + to_imguizmo_mode(snapshot.viewport.gizmo_space), + model.data(), + nullptr, + snapshot.viewport.snap_enabled ? snapshot.viewport.snap_values.data() : nullptr + ); + if (ImGuizmo::IsUsing()) { + float translation[3]{}; + float rotation[3]{}; + float scale[3]{}; + ImGuizmo::DecomposeMatrixToComponents(model.data(), translation, rotation, scale); + PendingShellEdits::TransformEdit edit; + edit.entity_id = snapshot.viewport.selected_object_id; + edit.position = {translation[0], translation[1], translation[2]}; + edit.rotation = {rotation[0], rotation[1], rotation[2]}; + edit.scale = {scale[0], scale[1], scale[2]}; + pending_edits_.transform_edit = edit; + } + ImGui::TextUnformatted("ImGuizmo bridge active"); + } else { + ImGui::TextUnformatted("ImGuizmo bridge ready"); + } +#endif +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_inspector_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + ImGui::TextUnformatted(snapshot.inspector.object_name.c_str()); + ImGui::Separator(); + for (const auto& field : snapshot.inspector.fields) { + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(field.label.c_str()); + ImGui::SameLine(180.0F); + ImGui::TextUnformatted(field.value.c_str()); + } +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_console_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + for (const auto& message : snapshot.console) { + ImGui::Text("[%s] %s", message.category.c_str(), message.text.c_str()); + } +#else + (void)snapshot; +#endif +} + +void DearImGuiEditorShell::render_runtime_preview_panel(const EditorFrameSnapshot& snapshot) { +#if METACORE_HAS_IMGUI + std::string_view document_path = "None"; + for (const auto& field : snapshot.inspector.fields) { + if (field.label == "Document") { + document_path = field.value; + break; + } + } + ImGui::TextUnformatted("MetaCore Runtime UI Preview"); + ImGui::Separator(); + ImGui::Text("Document: %.*s", static_cast(document_path.size()), document_path.data()); + ImGui::Text("Console messages: %zu", snapshot.console.size()); +#else + (void)snapshot; +#endif +} + +bool DearImGuiEditorShell::initialized() const { + return initialized_; +} + +const EditorFrameSnapshot& DearImGuiEditorShell::last_snapshot() const { + return last_snapshot_; +} + +std::string DearImGuiEditorShell::summarize_snapshot() const { + std::ostringstream stream; + stream << "Shell=" << shell_name() + << ", Menus=" << last_snapshot_.shell.menus.size() + << ", ToolbarGroups=" << last_snapshot_.shell.toolbar_groups.size() + << ", Sections=" << last_snapshot_.shell.sections.size() + << ", Hierarchy=" << last_snapshot_.hierarchy.size() + << ", Assets=" << last_snapshot_.assets.size() + << ", Inspector=" << last_snapshot_.inspector.object_name + << ", DrawCommands=" << last_draw_command_count_ + << ", DrawVertices=" << last_vertex_count_ + << ", DrawIndices=" << last_index_count_ + << ", DrawLists=" << last_window_count_; + return stream.str(); +} + +std::string DearImGuiEditorShell::describe_workspace() const { + std::ostringstream stream; + stream << "DearImGuiEditorShell composition"; + for (const auto& menu : last_snapshot_.shell.menus) { + stream << "\n [Menu] " << menu.menu_name << " actions=" << menu.actions.size(); + for (const auto& action : menu.actions) { + stream << "\n - " << action.label; + if (!action.shortcut.empty()) { + stream << " [" << action.shortcut << "]"; + } + } + } + for (const auto& group : last_snapshot_.shell.toolbar_groups) { + stream << "\n [Toolbar] " << group.name << " actions=" << group.actions.size(); + for (const auto& action : group.actions) { + stream << "\n - " << action.label; + } + } + for (const auto& section : last_snapshot_.shell.sections) { + stream << "\n [Section] panels=" << section.panels.size(); + for (const auto& panel : section.panels) { + stream << "\n - " << panel.title << " (" << panel.id << ")"; + } + } + return stream.str(); +} + +std::string DearImGuiEditorShell::shell_name() const { + return "DearImGuiEditorShell"; +} + +PendingShellEdits DearImGuiEditorShell::consume_pending_edits() { + PendingShellEdits edits = pending_edits_; + pending_edits_ = {}; + return edits; +} + +void DearImGuiEditorShell::set_debug_export_path(std::filesystem::path path) { + debug_export_path_ = std::move(path); +} + +void DearImGuiEditorShell::export_debug_snapshot() const { + if (debug_export_path_.empty()) { + return; + } + + std::ostringstream stream; + stream << summarize_snapshot() << "\n"; + stream << describe_workspace() << "\n"; + stream << "[Inspector] " << last_snapshot_.inspector.object_name << "\n"; + for (const auto& field : last_snapshot_.inspector.fields) { + stream << " - " << field.label << ": " << field.value << "\n"; + } + stream << "[Console]\n"; + for (const auto& message : last_snapshot_.console) { + stream << " - [" << message.category << "] " << message.text << "\n"; + } + export_viewport_snapshot(stream); + stream << "[DrawData]\n"; + stream << " - CommandCount: " << last_draw_command_count_ << "\n"; + stream << " - VertexCount: " << last_vertex_count_ << "\n"; + stream << " - IndexCount: " << last_index_count_ << "\n"; + stream << " - DrawLists: " << last_window_count_ << "\n"; + foundation::write_text_file(debug_export_path_, stream.str()); +} + +void DearImGuiEditorShell::export_viewport_snapshot(std::ostringstream& stream) const { + stream << "[Viewport]\n"; + stream << " - Selection: " + << (last_snapshot_.viewport.has_selection ? last_snapshot_.viewport.selected_object_name : std::string("None")) << "\n"; + stream << " - SelectionId: " << last_snapshot_.viewport.selected_object_id << "\n"; + stream << " - Operation: " << last_snapshot_.viewport.gizmo_operation << "\n"; + stream << " - Space: " << last_snapshot_.viewport.gizmo_space << "\n"; + stream << " - SnapEnabled: " << (last_snapshot_.viewport.snap_enabled ? "true" : "false") << "\n"; + stream << " - SnapValues: " + << last_snapshot_.viewport.snap_values[0] << ", " + << last_snapshot_.viewport.snap_values[1] << ", " + << last_snapshot_.viewport.snap_values[2] << "\n"; + stream << " - Camera: " + << last_snapshot_.viewport.camera_position[0] << ", " + << last_snapshot_.viewport.camera_position[1] << ", " + << last_snapshot_.viewport.camera_position[2] << "\n"; + stream << " - Position: " + << last_snapshot_.viewport.object_position[0] << ", " + << last_snapshot_.viewport.object_position[1] << ", " + << last_snapshot_.viewport.object_position[2] << "\n"; + stream << " - Rotation: " + << last_snapshot_.viewport.object_rotation[0] << ", " + << last_snapshot_.viewport.object_rotation[1] << ", " + << last_snapshot_.viewport.object_rotation[2] << "\n"; + stream << " - Scale: " + << last_snapshot_.viewport.object_scale[0] << ", " + << last_snapshot_.viewport.object_scale[1] << ", " + << last_snapshot_.viewport.object_scale[2] << "\n"; + stream << " - FocusTarget: " << last_snapshot_.viewport.focus_target_id << "\n"; +} + +} // namespace metacore::editor diff --git a/MetaEditor/DearImGuiEditorShell.h b/MetaEditor/DearImGuiEditorShell.h new file mode 100644 index 0000000..ae6e714 --- /dev/null +++ b/MetaEditor/DearImGuiEditorShell.h @@ -0,0 +1,53 @@ +#pragma once + +#include "MetaEditor/IEditorShell.h" + +#include + +namespace metacore::editor { + +class DearImGuiEditorShell final : public IEditorShell { +public: + bool initialize() override; + void shutdown() override; + void consume_snapshot(const EditorFrameSnapshot& snapshot) override; + + [[nodiscard]] bool initialized() const override; + [[nodiscard]] const EditorFrameSnapshot& last_snapshot() const override; + [[nodiscard]] std::string summarize_snapshot() const override; + [[nodiscard]] std::string describe_workspace() const override; + [[nodiscard]] std::string shell_name() const override; + [[nodiscard]] PendingShellEdits consume_pending_edits() override; + + void set_debug_export_path(std::filesystem::path path); + +private: + void enqueue_headless_transform_edit(const EditorFrameSnapshot& snapshot); + void render_headless_frame(const EditorFrameSnapshot& snapshot); + void render_dockspace_frame(const EditorFrameSnapshot& snapshot); + void ensure_default_dock_layout(const EditorFrameSnapshot& snapshot, unsigned int dockspace_id); + void render_toolbar(const EditorFrameSnapshot& snapshot); + void render_panel(const metacore::engine::editor_core::DockPanel& panel, const EditorFrameSnapshot& snapshot); + void render_hierarchy_panel(const EditorFrameSnapshot& snapshot); + void render_project_panel(const EditorFrameSnapshot& snapshot); + void render_viewport_panel(const EditorFrameSnapshot& snapshot); + void render_inspector_panel(const EditorFrameSnapshot& snapshot); + void render_console_panel(const EditorFrameSnapshot& snapshot); + void render_runtime_preview_panel(const EditorFrameSnapshot& snapshot); + void export_debug_snapshot() const; + void export_viewport_snapshot(std::ostringstream& stream) const; + + bool initialized_ = false; + EditorFrameSnapshot last_snapshot_{}; + std::filesystem::path debug_export_path_{}; + int last_draw_command_count_ = 0; + int last_vertex_count_ = 0; + int last_index_count_ = 0; + int last_window_count_ = 0; + bool headless_frame_enabled_ = false; + bool dock_layout_initialized_ = false; + bool headless_transform_override_consumed_ = false; + PendingShellEdits pending_edits_{}; +}; + +} // namespace metacore::editor diff --git a/MetaEditor/EditorApplication.cpp b/MetaEditor/EditorApplication.cpp new file mode 100644 index 0000000..dc4d560 --- /dev/null +++ b/MetaEditor/EditorApplication.cpp @@ -0,0 +1,152 @@ +#include "MetaEditor/EditorApplication.h" + +#include "MetaEditor/DearImGuiEditorShell.h" + +#include "MetaCore/foundation/Log.h" + +#include + +namespace metacore::editor { + +EditorApplication::EditorApplication() = default; + +bool EditorApplication::initialize(const std::filesystem::path& project_root) { + project_root_ = project_root; + editor_shell_ = create_default_editor_shell(); + const auto shell_debug_path = project_root_ / "Library" / "Layout" / (editor_shell_->shell_name() + std::string("_snapshot.txt")); + editor_shell_->set_debug_export_path(shell_debug_path); + if (!platform_.initialize("MetaEditor")) { + return false; + } + render_backend_.initialize(); + editor_shell_->initialize(); + + if (!std::filesystem::exists(project_root_ / "MetaCore.project.json")) { + project_service_.create_project(project_root_, "MetaCoreSample"); + } + + project_service_.load_project(project_root_ / "MetaCore.project.json", project_); + scene_ = scene_service_.create_default_scene(); + scene_serializer_.load_scene(project_root_ / "Scenes" / "Main.mcscene.json", scene_); + asset_database_.load(project_root_ / "Library" / "AssetDB.json"); + layout_service_.load(project_root_ / "Library" / "Layout" / "editor_layout.json", layout_state_); + session_.apply_layout_state(layout_state_); + + if (!scene_.objects.empty()) { + selection_service_.select(scene_.objects.front().id); + } + + session_.set_status_text("Editor initialized"); + session_.add_console_message("System", "MetaEditor initialized"); + gizmo_service_.set_operation(metacore::engine::editor_core::TransformGizmoService::Operation::Translate); + gizmo_service_.set_space(metacore::engine::editor_core::TransformGizmoService::Space::Local); + gizmo_service_.set_snap_enabled(false); + + foundation::log(foundation::LogLevel::Info, "MetaEditor initialized"); + foundation::log(foundation::LogLevel::Info, "Editor shell export path: " + shell_debug_path.generic_string()); + foundation::log(foundation::LogLevel::Info, "Platform backend: " + platform_.backend_name().generic_string()); + if (!platform_.panda_version().empty()) { + foundation::log( + foundation::LogLevel::Info, + "Panda3D runtime: " + platform_.panda_version() + " | platform=" + platform_.panda_platform() + " | build=" + platform_.panda_build_date() + ); + } + foundation::log( + foundation::LogLevel::Info, + "Panda window bootstrap: enabled=" + std::string(platform_.window_enabled() ? "true" : "false") + + " opened=" + std::string(platform_.window_opened() ? "true" : "false") + ); + return true; +} + +int EditorApplication::run() { + foundation::log(foundation::LogLevel::Info, "MetaEditor skeleton run loop started"); + + const bool has_runtime_ui_object = std::any_of( + scene_.objects.begin(), + scene_.objects.end(), + [](const metacore::engine::scene::SceneObject& object) { + return object.has_ui_document; + } + ); + if (!has_runtime_ui_object) { + metacore::engine::editor_core::SceneCommands::create_empty_object( + command_service_, + session_, + selection_service_, + scene_service_, + scene_, + "UI Root" + ); + + if (selection_service_.selection().has_value()) { + if (const auto object = scene_service_.find_object(scene_, *selection_service_.selection())) { + object->get().has_ui_document = true; + object->get().ui_document.document_path = "Assets/UI/main_menu.rml"; + object->get().ui_document.stylesheet_path = "Assets/UI/main_menu.rcss"; + } + } + } + + metacore::engine::editor_core::SceneCommands::focus_selected_object( + session_, + selection_service_, + viewport_service_ + ); + + if (const auto selected = inspector_service_.current_selection(scene_, selection_service_)) { + foundation::log(foundation::LogLevel::Info, "Current selection: " + selected->get().name); + } + auto snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_); + editor_shell_->consume_snapshot(snapshot); + const PendingShellEdits shell_edits = editor_shell_->consume_pending_edits(); + if (shell_edits.transform_edit.has_value() && + shell_edits.transform_edit->entity_id == snapshot.viewport.selected_object_id) { + const auto requested_transform = metacore::engine::scene::TransformComponent{ + shell_edits.transform_edit->position, + shell_edits.transform_edit->rotation, + shell_edits.transform_edit->scale + }; + metacore::engine::editor_core::SceneCommands::apply_transform_to_selected_object( + command_service_, + session_, + selection_service_, + scene_service_, + scene_, + requested_transform + ); + snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_); + editor_shell_->consume_snapshot(snapshot); + } + + layout_service_.save(project_root_ / "Library" / "Layout" / "editor_layout.json", session_.to_layout_state()); + scene_serializer_.save_scene(project_root_ / "Scenes" / "Main.mcscene.json", scene_); + asset_database_.save(project_root_ / "Library" / "AssetDB.json"); + platform_.pump_frame(); + + foundation::log(foundation::LogLevel::Info, "Hierarchy items: " + std::to_string(snapshot.hierarchy.size())); + foundation::log(foundation::LogLevel::Info, "Project assets: " + std::to_string(snapshot.assets.size())); + foundation::log(foundation::LogLevel::Info, "Inspector target: " + snapshot.inspector.object_name); + foundation::log(foundation::LogLevel::Info, "Scene object count: " + std::to_string(snapshot.scene_object_count)); + foundation::log(foundation::LogLevel::Info, "Session status: " + snapshot.status_text); + foundation::log(foundation::LogLevel::Info, "Editor shell: " + editor_shell_->shell_name()); + foundation::log(foundation::LogLevel::Info, "Editor shell summary: " + editor_shell_->summarize_snapshot()); + foundation::log(foundation::LogLevel::Info, editor_shell_->describe_workspace()); + + for (const auto& message : snapshot.console) { + foundation::log(foundation::LogLevel::Info, "[" + message.category + "] " + message.text); + } + + return 0; +} + +void EditorApplication::shutdown() { + if (editor_shell_) { + editor_shell_->shutdown(); + } + render_backend_.shutdown(); + platform_.shutdown(); + foundation::log(foundation::LogLevel::Info, "MetaEditor shutdown complete"); +} + +} // namespace metacore::editor diff --git a/MetaEditor/EditorApplication.h b/MetaEditor/EditorApplication.h new file mode 100644 index 0000000..abcecea --- /dev/null +++ b/MetaEditor/EditorApplication.h @@ -0,0 +1,58 @@ +#pragma once + +#include "MetaEditor/IEditorShell.h" +#include "MetaEditor/EditorPresenter.h" + +#include "MetaCore/engine/assets/AssetDatabase.h" +#include "MetaCore/engine/editor_core/CommandService.h" +#include "MetaCore/engine/editor_core/EditorSession.h" +#include "MetaCore/engine/editor_core/EditorLayoutService.h" +#include "MetaCore/engine/editor_core/InspectorService.h" +#include "MetaCore/engine/editor_core/SceneCommands.h" +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/editor_core/TransformGizmoService.h" +#include "MetaCore/engine/editor_core/ViewportService.h" +#include "MetaCore/engine/render/SimplePbrShaderLibrary.h" +#include "MetaCore/engine/scene/Project.h" +#include "MetaCore/engine/scene/Scene.h" +#include "MetaCore/engine/scene/SceneSerializer.h" +#include "MetaCore/platform/panda/PandaPlatformContext.h" +#include "MetaCore/platform/panda/PandaRenderBackend.h" + +#include +#include + +namespace metacore::editor { + +class EditorApplication { +public: + EditorApplication(); + + bool initialize(const std::filesystem::path& project_root); + int run(); + void shutdown(); + +private: + std::filesystem::path project_root_; + metacore::platform::panda::PandaPlatformContext platform_; + metacore::platform::panda::PandaRenderBackend render_backend_; + metacore::engine::scene::ProjectService project_service_; + metacore::engine::scene::SceneService scene_service_; + metacore::engine::scene::SceneSerializer scene_serializer_; + metacore::engine::assets::AssetService asset_service_; + metacore::engine::assets::AssetDatabase asset_database_; + metacore::engine::editor_core::SelectionService selection_service_; + metacore::engine::editor_core::TransformGizmoService gizmo_service_; + metacore::engine::editor_core::CommandService command_service_; + metacore::engine::editor_core::EditorLayoutService layout_service_; + metacore::engine::editor_core::InspectorService inspector_service_; + metacore::engine::editor_core::ViewportService viewport_service_; + metacore::engine::editor_core::EditorSession session_{}; + metacore::engine::editor_core::EditorLayoutState layout_state_{}; + metacore::engine::scene::Project project_{}; + metacore::engine::scene::Scene scene_{}; + EditorPresenter presenter_{}; + std::unique_ptr editor_shell_{}; +}; + +} // namespace metacore::editor diff --git a/MetaEditor/EditorPresenter.cpp b/MetaEditor/EditorPresenter.cpp new file mode 100644 index 0000000..bfa111b --- /dev/null +++ b/MetaEditor/EditorPresenter.cpp @@ -0,0 +1,78 @@ +#include "MetaEditor/EditorPresenter.h" + +namespace metacore::editor { + +namespace { + +std::string to_string(const metacore::engine::editor_core::TransformGizmoService::Operation operation) { + using Operation = metacore::engine::editor_core::TransformGizmoService::Operation; + switch (operation) { + case Operation::Translate: + return "Translate"; + case Operation::Rotate: + return "Rotate"; + case Operation::Scale: + return "Scale"; + } + return "Translate"; +} + +std::string to_string(const metacore::engine::editor_core::TransformGizmoService::Space space) { + using Space = metacore::engine::editor_core::TransformGizmoService::Space; + switch (space) { + case Space::Local: + return "Local"; + case Space::World: + return "World"; + } + return "Local"; +} + +} // namespace + +EditorFrameSnapshot EditorPresenter::build_snapshot( + const metacore::engine::scene::Scene& scene, + const metacore::engine::assets::AssetDatabase& asset_database, + const metacore::engine::editor_core::SelectionService& selection_service, + const metacore::engine::editor_core::TransformGizmoService& gizmo_service, + const metacore::engine::editor_core::ViewportService& viewport_service, + const metacore::engine::editor_core::EditorSession& session +) const { + EditorFrameSnapshot snapshot; + snapshot.actions = action_catalog_.build_default_actions(); + snapshot.workspace = workspace_factory_.build_default_layout(); + snapshot.shell = shell_builder_.build(snapshot.actions, snapshot.workspace); + snapshot.hierarchy = panel_view_models_.build_hierarchy(scene, selection_service); + snapshot.assets = panel_view_models_.build_project_assets(asset_database); + snapshot.inspector = panel_view_models_.build_inspector(scene, selection_service); + snapshot.viewport.camera_position = viewport_service.camera_position(); + snapshot.viewport.gizmo_operation = to_string(gizmo_service.operation()); + snapshot.viewport.gizmo_space = to_string(gizmo_service.space()); + snapshot.viewport.snap_enabled = gizmo_service.snap_enabled(); + snapshot.viewport.snap_values = gizmo_service.snap_values(); + if (viewport_service.focus_target().has_value()) { + snapshot.viewport.focus_target_id = *viewport_service.focus_target(); + } + if (selection_service.selection().has_value()) { + if (const auto object = metacore::engine::scene::SceneService{}.find_object(scene, *selection_service.selection())) { + snapshot.viewport.has_selection = true; + snapshot.viewport.selected_object_id = object->get().id; + snapshot.viewport.selected_object_name = object->get().name; + snapshot.viewport.object_position = object->get().transform.position; + snapshot.viewport.object_rotation = object->get().transform.rotation; + snapshot.viewport.object_scale = object->get().transform.scale; + } + } + snapshot.console = panel_view_models_.console_messages(session); + snapshot.status_text = session.status_text(); + snapshot.scene_object_count = scene.objects.size(); + snapshot.hierarchy_visible = session.panel_state().hierarchy_visible; + snapshot.inspector_visible = session.panel_state().inspector_visible; + snapshot.project_visible = session.panel_state().project_visible; + snapshot.console_visible = session.panel_state().console_visible; + snapshot.runtime_preview_visible = session.panel_state().runtime_preview_visible; + snapshot.toolbar_visible = session.panel_state().toolbar_visible; + return snapshot; +} + +} // namespace metacore::editor diff --git a/MetaEditor/EditorPresenter.h b/MetaEditor/EditorPresenter.h new file mode 100644 index 0000000..dba2bc8 --- /dev/null +++ b/MetaEditor/EditorPresenter.h @@ -0,0 +1,72 @@ +#pragma once + +#include "MetaCore/engine/editor_core/EditorActions.h" +#include "MetaCore/engine/assets/AssetDatabase.h" +#include "MetaCore/engine/editor_core/EditorSession.h" +#include "MetaCore/engine/editor_core/PanelViewModels.h" +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/editor_core/ShellComposition.h" +#include "MetaCore/engine/editor_core/TransformGizmoService.h" +#include "MetaCore/engine/editor_core/ViewportService.h" +#include "MetaCore/engine/editor_core/WorkspaceLayout.h" +#include "MetaCore/engine/scene/Scene.h" + +#include +#include +#include + +namespace metacore::editor { + +struct EditorFrameSnapshot { + struct ViewportSnapshot { + std::string selected_object_id; + std::string selected_object_name; + std::array camera_position{0.0F, -10.0F, 3.0F}; + std::array object_position{0.0F, 0.0F, 0.0F}; + std::array object_rotation{0.0F, 0.0F, 0.0F}; + std::array object_scale{1.0F, 1.0F, 1.0F}; + std::array snap_values{1.0F, 1.0F, 1.0F}; + std::string gizmo_operation = "Translate"; + std::string gizmo_space = "Local"; + std::string focus_target_id; + bool has_selection = false; + bool snap_enabled = false; + }; + + std::vector actions; + metacore::engine::editor_core::WorkspaceLayout workspace; + metacore::engine::editor_core::ShellComposition shell; + std::vector hierarchy; + std::vector assets; + metacore::engine::editor_core::InspectorViewModel inspector; + ViewportSnapshot viewport; + std::vector console; + std::string status_text; + std::size_t scene_object_count = 0; + bool hierarchy_visible = true; + bool inspector_visible = true; + bool project_visible = true; + bool console_visible = true; + bool runtime_preview_visible = true; + bool toolbar_visible = true; +}; + +class EditorPresenter { +public: + EditorFrameSnapshot build_snapshot( + const metacore::engine::scene::Scene& scene, + const metacore::engine::assets::AssetDatabase& asset_database, + const metacore::engine::editor_core::SelectionService& selection_service, + const metacore::engine::editor_core::TransformGizmoService& gizmo_service, + const metacore::engine::editor_core::ViewportService& viewport_service, + const metacore::engine::editor_core::EditorSession& session + ) const; + +private: + metacore::engine::editor_core::PanelViewModels panel_view_models_{}; + metacore::engine::editor_core::EditorActionCatalog action_catalog_{}; + metacore::engine::editor_core::ShellCompositionBuilder shell_builder_{}; + metacore::engine::editor_core::WorkspaceLayoutFactory workspace_factory_{}; +}; + +} // namespace metacore::editor diff --git a/MetaEditor/IEditorShell.h b/MetaEditor/IEditorShell.h new file mode 100644 index 0000000..afdf366 --- /dev/null +++ b/MetaEditor/IEditorShell.h @@ -0,0 +1,43 @@ +#pragma once + +#include "MetaEditor/EditorPresenter.h" + +#include +#include +#include +#include +#include + +namespace metacore::editor { + +struct PendingShellEdits { + struct TransformEdit { + std::string entity_id; + std::array position{0.0F, 0.0F, 0.0F}; + std::array rotation{0.0F, 0.0F, 0.0F}; + std::array scale{1.0F, 1.0F, 1.0F}; + }; + + std::optional transform_edit; +}; + +class IEditorShell { +public: + virtual ~IEditorShell() = default; + + virtual bool initialize() = 0; + virtual void shutdown() = 0; + virtual void consume_snapshot(const EditorFrameSnapshot& snapshot) = 0; + + [[nodiscard]] virtual bool initialized() const = 0; + [[nodiscard]] virtual const EditorFrameSnapshot& last_snapshot() const = 0; + [[nodiscard]] virtual std::string summarize_snapshot() const = 0; + [[nodiscard]] virtual std::string describe_workspace() const = 0; + [[nodiscard]] virtual std::string shell_name() const = 0; + [[nodiscard]] virtual PendingShellEdits consume_pending_edits() = 0; + virtual void set_debug_export_path(std::filesystem::path path) = 0; +}; + +std::unique_ptr create_default_editor_shell(); + +} // namespace metacore::editor diff --git a/MetaEditor/NullEditorShell.cpp b/MetaEditor/NullEditorShell.cpp new file mode 100644 index 0000000..8f67590 --- /dev/null +++ b/MetaEditor/NullEditorShell.cpp @@ -0,0 +1,119 @@ +#include "MetaEditor/NullEditorShell.h" + +#include "MetaEditor/DearImGuiEditorShell.h" +#include "MetaCore/foundation/FileSystem.h" +#include "MetaCore/engine/editor_core/WorkspaceLayout.h" + +#include +#include + +namespace metacore::editor { + +namespace { + +const char* region_name(const metacore::engine::editor_core::DockRegion region) { + using metacore::engine::editor_core::DockRegion; + switch (region) { + case DockRegion::TopMenuBar: + return "TopMenuBar"; + case DockRegion::TopToolbar: + return "TopToolbar"; + case DockRegion::LeftSidebar: + return "LeftSidebar"; + case DockRegion::CenterViewport: + return "CenterViewport"; + case DockRegion::RightSidebar: + return "RightSidebar"; + case DockRegion::BottomConsole: + return "BottomConsole"; + case DockRegion::BottomRuntimePreview: + return "BottomRuntimePreview"; + } + return "Unknown"; +} + +} // namespace + +bool NullEditorShell::initialize() { + initialized_ = true; + return true; +} + +void NullEditorShell::shutdown() { + initialized_ = false; + last_snapshot_ = {}; +} + +void NullEditorShell::consume_snapshot(const EditorFrameSnapshot& snapshot) { + last_snapshot_ = snapshot; + if (!debug_export_path_.empty()) { + foundation::write_text_file(debug_export_path_, summarize_snapshot() + "\n" + describe_workspace() + "\n"); + } +} + +bool NullEditorShell::initialized() const { + return initialized_; +} + +const EditorFrameSnapshot& NullEditorShell::last_snapshot() const { + return last_snapshot_; +} + +std::string NullEditorShell::summarize_snapshot() const { + std::ostringstream stream; + stream << "Shell=" << shell_name() + << ", Actions=" << last_snapshot_.actions.size() + << ", Panels=" << last_snapshot_.workspace.panels.size() + << ", Hierarchy=" << last_snapshot_.hierarchy.size() + << ", Assets=" << last_snapshot_.assets.size() + << ", Inspector=" << last_snapshot_.inspector.object_name + << ", Console=" << last_snapshot_.console.size() + << ", Status=" << last_snapshot_.status_text + << ", Toolbar=" << (last_snapshot_.toolbar_visible ? "on" : "off") + << ", RuntimePreview=" << (last_snapshot_.runtime_preview_visible ? "on" : "off"); + return stream.str(); +} + +std::string NullEditorShell::describe_workspace() const { + std::ostringstream stream; + stream << "Workspace[" << last_snapshot_.workspace.name << "]"; + for (const auto& panel : last_snapshot_.workspace.panels) { + stream << "\n - " << panel.title + << " (" << panel.id << ")" + << " @ " << region_name(panel.region) + << " visible=" << (panel.visible ? "true" : "false"); + } + return stream.str(); +} + +std::string NullEditorShell::shell_name() const { + return "NullEditorShell"; +} + +PendingShellEdits NullEditorShell::consume_pending_edits() { + return {}; +} + +void NullEditorShell::set_debug_export_path(std::filesystem::path path) { + debug_export_path_ = std::move(path); +} + +std::unique_ptr create_default_editor_shell() { + if (const char* shell_env = std::getenv("METACORE_EDITOR_SHELL")) { + const std::string value(shell_env); + if (value == "dearimgui") { + return std::make_unique(); + } + if (value == "null") { + return std::make_unique(); + } + } + +#if defined(METACORE_HAS_IMGUI) && METACORE_HAS_IMGUI + return std::make_unique(); +#else + return std::make_unique(); +#endif +} + +} // namespace metacore::editor diff --git a/MetaEditor/NullEditorShell.h b/MetaEditor/NullEditorShell.h new file mode 100644 index 0000000..447cb1e --- /dev/null +++ b/MetaEditor/NullEditorShell.h @@ -0,0 +1,27 @@ +#pragma once + +#include "MetaEditor/IEditorShell.h" + +namespace metacore::editor { + +class NullEditorShell final : public IEditorShell { +public: + bool initialize() override; + void shutdown() override; + void consume_snapshot(const EditorFrameSnapshot& snapshot) override; + + [[nodiscard]] bool initialized() const override; + [[nodiscard]] const EditorFrameSnapshot& last_snapshot() const override; + [[nodiscard]] std::string summarize_snapshot() const override; + [[nodiscard]] std::string describe_workspace() const override; + [[nodiscard]] std::string shell_name() const override; + [[nodiscard]] PendingShellEdits consume_pending_edits() override; + void set_debug_export_path(std::filesystem::path path) override; + +private: + bool initialized_ = false; + EditorFrameSnapshot last_snapshot_{}; + std::filesystem::path debug_export_path_{}; +}; + +} // namespace metacore::editor diff --git a/MetaEditor/main.cpp b/MetaEditor/main.cpp new file mode 100644 index 0000000..dbac9cd --- /dev/null +++ b/MetaEditor/main.cpp @@ -0,0 +1,31 @@ +#include "MetaEditor/EditorApplication.h" + +#include +#include +#include + +int main(int argc, char** argv) { + std::cerr << "[MetaCore][Trace] MetaEditor main: begin\n"; + std::filesystem::path project_root = "D:/MetaCore/SandboxProject"; + if (argc > 1) { + project_root = argv[1]; + } + + std::cerr << "[MetaCore][Trace] MetaEditor main: before app construction\n"; + metacore::editor::EditorApplication app; + std::cerr << "[MetaCore][Trace] MetaEditor main: before initialize\n"; + if (!app.initialize(project_root)) { + std::cerr << "[MetaCore][Trace] MetaEditor main: initialize failed\n"; + return 1; + } + std::cerr << "[MetaCore][Trace] MetaEditor main: before run\n"; + const int code = app.run(); + std::cerr << "[MetaCore][Trace] MetaEditor main: after run\n"; + const char* skip_shutdown = std::getenv("METACORE_SKIP_APP_SHUTDOWN"); + if (!(skip_shutdown != nullptr && std::string(skip_shutdown) == "1")) { + std::cerr << "[MetaCore][Trace] MetaEditor main: before shutdown\n"; + app.shutdown(); + std::cerr << "[MetaCore][Trace] MetaEditor main: after shutdown\n"; + } + return code; +} diff --git a/MetaPlayer/PlayerApplication.cpp b/MetaPlayer/PlayerApplication.cpp new file mode 100644 index 0000000..a3550ad --- /dev/null +++ b/MetaPlayer/PlayerApplication.cpp @@ -0,0 +1,42 @@ +#include "MetaPlayer/PlayerApplication.h" + +#include "MetaCore/foundation/Log.h" + +namespace metacore::player { + +bool PlayerApplication::initialize(const std::filesystem::path& scene_file) { + scene_file_ = scene_file; + platform_.initialize("MetaPlayer"); + render_backend_.initialize(); + runtime_ui_.initialize(); + scene_serializer_.load_scene(scene_file_, scene_); + foundation::log(foundation::LogLevel::Info, "MetaPlayer initialized"); + foundation::log( + foundation::LogLevel::Info, + "Panda window bootstrap: enabled=" + std::string(platform_.window_enabled() ? "true" : "false") + + " opened=" + std::string(platform_.window_opened() ? "true" : "false") + ); + return true; +} + +int PlayerApplication::run() { + const auto documents = runtime_ui_.collect_documents(scene_); + preview_session_.set_documents(documents); + if (!documents.empty()) { + runtime_ui_.load_document(documents.front()); + } + foundation::log(foundation::LogLevel::Info, "Runtime UI documents: " + std::to_string(documents.size())); + foundation::log(foundation::LogLevel::Info, preview_session_.describe()); + foundation::log(foundation::LogLevel::Info, "MetaPlayer skeleton run loop started"); + platform_.pump_frame(); + return 0; +} + +void PlayerApplication::shutdown() { + runtime_ui_.shutdown(); + render_backend_.shutdown(); + platform_.shutdown(); + foundation::log(foundation::LogLevel::Info, "MetaPlayer shutdown complete"); +} + +} // namespace metacore::player diff --git a/MetaPlayer/PlayerApplication.h b/MetaPlayer/PlayerApplication.h new file mode 100644 index 0000000..6094e09 --- /dev/null +++ b/MetaPlayer/PlayerApplication.h @@ -0,0 +1,32 @@ +#pragma once + +#include "MetaPlayer/RuntimeUiPreviewSession.h" + +#include "MetaCore/engine/render/SimplePbrShaderLibrary.h" +#include "MetaCore/engine/runtime_ui/RmlUiBridge.h" +#include "MetaCore/engine/scene/Scene.h" +#include "MetaCore/engine/scene/SceneSerializer.h" +#include "MetaCore/platform/panda/PandaPlatformContext.h" +#include "MetaCore/platform/panda/PandaRenderBackend.h" + +#include + +namespace metacore::player { + +class PlayerApplication { +public: + bool initialize(const std::filesystem::path& scene_file); + int run(); + void shutdown(); + +private: + std::filesystem::path scene_file_; + metacore::platform::panda::PandaPlatformContext platform_; + metacore::platform::panda::PandaRenderBackend render_backend_; + metacore::engine::runtime_ui::RmlUiBridge runtime_ui_; + metacore::engine::scene::SceneSerializer scene_serializer_; + metacore::engine::scene::Scene scene_{}; + RuntimeUiPreviewSession preview_session_{}; +}; + +} // namespace metacore::player diff --git a/MetaPlayer/RuntimeUiPreviewSession.cpp b/MetaPlayer/RuntimeUiPreviewSession.cpp new file mode 100644 index 0000000..40fdbfb --- /dev/null +++ b/MetaPlayer/RuntimeUiPreviewSession.cpp @@ -0,0 +1,24 @@ +#include "MetaPlayer/RuntimeUiPreviewSession.h" + +#include + +namespace metacore::player { + +void RuntimeUiPreviewSession::set_documents(std::vector documents) { + documents_ = std::move(documents); +} + +const std::vector& RuntimeUiPreviewSession::documents() const { + return documents_; +} + +std::string RuntimeUiPreviewSession::describe() const { + std::ostringstream stream; + stream << "RuntimeUiPreviewSession documents=" << documents_.size(); + if (!documents_.empty()) { + stream << " first=" << documents_.front().document_path.generic_string(); + } + return stream.str(); +} + +} // namespace metacore::player diff --git a/MetaPlayer/RuntimeUiPreviewSession.h b/MetaPlayer/RuntimeUiPreviewSession.h new file mode 100644 index 0000000..45a5120 --- /dev/null +++ b/MetaPlayer/RuntimeUiPreviewSession.h @@ -0,0 +1,20 @@ +#pragma once + +#include "MetaCore/engine/runtime_ui/RmlUiBridge.h" + +#include +#include + +namespace metacore::player { + +class RuntimeUiPreviewSession { +public: + void set_documents(std::vector documents); + [[nodiscard]] const std::vector& documents() const; + [[nodiscard]] std::string describe() const; + +private: + std::vector documents_; +}; + +} // namespace metacore::player diff --git a/MetaPlayer/main.cpp b/MetaPlayer/main.cpp new file mode 100644 index 0000000..8b9602f --- /dev/null +++ b/MetaPlayer/main.cpp @@ -0,0 +1,31 @@ +#include "MetaPlayer/PlayerApplication.h" + +#include +#include +#include + +int main(int argc, char** argv) { + std::cerr << "[MetaCore][Trace] MetaPlayer main: begin\n"; + std::filesystem::path scene_file = "D:/MetaCore/SandboxProject/Scenes/Main.mcscene.json"; + if (argc > 1) { + scene_file = argv[1]; + } + + std::cerr << "[MetaCore][Trace] MetaPlayer main: before app construction\n"; + metacore::player::PlayerApplication app; + std::cerr << "[MetaCore][Trace] MetaPlayer main: before initialize\n"; + if (!app.initialize(scene_file)) { + std::cerr << "[MetaCore][Trace] MetaPlayer main: initialize failed\n"; + return 1; + } + std::cerr << "[MetaCore][Trace] MetaPlayer main: before run\n"; + const int code = app.run(); + std::cerr << "[MetaCore][Trace] MetaPlayer main: after run\n"; + const char* skip_shutdown = std::getenv("METACORE_SKIP_APP_SHUTDOWN"); + if (!(skip_shutdown != nullptr && std::string(skip_shutdown) == "1")) { + std::cerr << "[MetaCore][Trace] MetaPlayer main: before shutdown\n"; + app.shutdown(); + std::cerr << "[MetaCore][Trace] MetaPlayer main: after shutdown\n"; + } + return code; +} diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..31b044c --- /dev/null +++ b/PLAN.md @@ -0,0 +1,99 @@ +# MetaCore 最终开工计划 + +## 摘要 +- 在 `D:\MetaCore` 新建全新纯 C++ 工程,完全不继承 EG 项目文件格式,也不做 EG 自动导入。 +- 对外目标:编辑器核心功能、布局逻辑、操作路径对齐现有 EG;对内目标:彻底模块化、职责清晰、修正旧项目中的耦合和不完整逻辑。 +- UI 职责固定为:`ImGui + ImGuizmo` 仅用于编辑器与开发工具,`RmlUi` 仅用于运行时/玩家界面。 +- 渲染固定为:Panda3D 底层后端,`simplepbr` 只复用 shader 源码与参数语义,MetaCore 用 C++ 重写 shader 调用、材质系统和 pass。 + +## 技术基线 +- 语言与构建:`C++20`、`CMakePresets`、`VS2022`、`vcpkg` +- 底层后端:`Panda3D 1.10.16` 作为 v1 基线,后续升级 `1.11` +- 编辑器 UI:Dear ImGui docking + ImGuizmo +- 运行时 UI:RmlUi +- 渲染:MetaCore 自研 `MetaRender`,shader 来源参考 `simplepbr` +- 数据格式:JSON + `.meta` 资源描述 +- 首版不做:脚本系统、VR、Prefab 工作流、完整打包链路、URP 级渲染 + +## 工程结构 +- `MetaCore/` +- `MetaCore/foundation`:日志、配置、GUID、文件系统、事件、任务、序列化辅助 +- `MetaCore/platform/panda`:窗口、输入、模型纹理加载、Panda 节点适配、shader 绑定、渲染上下文 +- `MetaCore/engine/assets`:资产索引、`.meta`、依赖、导入缓存 +- `MetaCore/engine/scene`:`Scene`、层级、实体、组件、场景保存加载 +- `MetaCore/engine/render`:`ShaderProgram`、`ShaderVariantKey`、`MaterialDescriptor`、`MaterialInstance`、各类 render pass +- `MetaCore/engine/runtime_ui`:RmlUi 文档、输入、字体、纹理、预览桥接 +- `MetaCore/engine/editor_core`:选择、Gizmo 状态、命令栈、撤销重做、Inspector 数据模型、脏标记 +- `MetaEditor/`:ImGui 编辑器外壳、菜单、Docking、各面板 +- `MetaPlayer/`:运行时播放器 +- `third_party/simplepbr-shaders/`:镜像 `simplepbr.vert/.frag`、`shadow.vert/.frag`、`skybox.vert/.frag`、`post.vert`、`tonemap.frag` 及许可说明 + +## 核心职责边界 +- ImGui:菜单栏、Hierarchy、Inspector、Project/Assets、Console、Scene View 工具栏、调试工具、材质编辑器、RmlUi 资源编辑器/预览器 +- ImGuizmo:视口中的平移/旋转/缩放操控;不负责命令记录和业务状态 +- RmlUi:运行时主菜单、HUD、背包、任务、设置、弹窗、加载界面等玩家 UI +- MetaCore 服务层:负责“改什么、怎么存、怎么撤销重做”,不让 UI 直接操作底层数据 +- Panda3D:仅作为内部后端,不出现在公共 API、项目 schema、用户术语里 + +## 域模型与接口 +- 核心对象:`Project`、`Scene`、`EntityId`、`SceneObjectHandle`、`AssetId` +- 首版组件:`TransformComponent`、`MeshRendererComponent`、`LightComponent`、`CameraComponent`、`UIDocumentComponent`、`CollisionComponent`、`EditorMetadataComponent` +- 核心服务:`ProjectService`、`SceneService`、`AssetService`、`EditorLayoutService`、`SelectionService`、`TransformGizmoService`、`CommandService`、`ViewportService`、`InspectorService` +- 渲染接口:`IMetaRenderBackend`、`ShaderProgram`、`MaterialInstance`、`FrameUniforms`、`LightUniforms`、`TonemapSettings` +- 首版预留不交付:`PrefabAsset`/模板化接口、未来脚本挂载接口 + +## 编辑器交付范围 +- 布局与职责对齐现有 EG:顶部菜单、左侧层级树、资源面板、中心视口、右侧属性、底部控制台、工具栏、默认 Docking +- 交互对齐现有 EG:创建/删除/重命名、选择高亮、F 聚焦、W/E/R、局部/世界切换、吸附、撤销重做、项目与场景保存加载 +- 实现策略:外部体验尽量一致,内部完全重构;不迁移 EG 旧 Gizmo,不复用 EG 混杂管理器结构 +- 允许修正旧项目中的明显错误、状态不同步、生命周期缺失和脏逻辑 + +## 渲染实现 +- 使用 simplepbr shader 源码,但不使用其 Python `Pipeline` +- C++ 层负责 shader define 组合、材质参数、贴图槽、灯光数组、阴影输入、天空盒、后处理、色调映射 +- v1 pass 固定为:`ShadowPass`、`MainPbrPass`、`SkyboxPass`、`TonemapPass` +- v1 能力固定为:基础 PBR、法线贴图、发光、环境光、点光/方向光/聚光、简单阴影、天空盒、色调映射 +- IBL 与 URP 风格渲染只预留接口,不在 v1 做完整实现 + +## 数据与项目格式 +- 项目文件:`MetaCore.project.json` +- 场景文件:`Scenes/*.mcscene.json` +- 资源目录:`Assets/Models`、`Assets/Textures`、`Assets/Audio`、`Assets/UI`、`Assets/Materials` +- 缓存目录:`Library/AssetDB.json`、缩略图缓存、导入缓存、布局缓存 +- 每个资源配套 `.meta` +- 场景数据、编辑器布局、运行时缓存严格分离,禁止把运行时状态写回场景对象 + +## 实施顺序 +1. 建立仓库、CMake、第三方依赖、基础目录和空应用启动骨架 +2. 完成 `foundation` 与 `platform/panda`,确保窗口、输入、相机、基础场景显示正常 +3. 完成 `engine/scene` 与 `engine/assets`,跑通新建项目、新建场景、保存重开 +4. 接入 ImGui docking,搭建编辑器外壳与固定面板布局 +5. 接入 ImGuizmo,完成选择、变换、命令栈、撤销重做 +6. 完成 `MetaRender` 与 simplepbr shader C++ 调用链 +7. 完成 Inspector、资源面板、Hierarchy 与视口联动 +8. 接入 RmlUi 运行时显示与编辑器内预览 +9. 做稳定性补齐、布局恢复、错误处理、测试与文档 + +## 测试与验收 +- 单元测试:项目/场景 schema、组件增删改、命令栈、资产索引、shader 参数绑定 +- 集成测试:新建项目、保存重开、Hierarchy 与 Inspector 联动、Gizmo 变换持久化、布局恢复、RmlUi 文档加载 +- 验收标准: + - 能用 `MetaEditor` 完成现有 EG 的核心编辑流程 + - 能稳定创建全新 MetaCore 项目并反复保存/重开 + - `MetaPlayer` 能正确运行场景并显示 RmlUi + - 代码中不存在新的 God Object 和跨层硬耦合入口 + +## 已锁定默认 +- 全项目纯 C++ +- 不导入 EG 项目,只做全新重构 +- `ImGui` 只做编辑器,`RmlUi` 只做运行时 UI +- `ImGuizmo` 取代 EG 旧 Gizmo +- 功能参考 Unity,但不高仿 Unity 命名 +- 首版只预留 Prefab 与脚本接口,不交付完整功能 +- Panda3D 先用 `1.10.16` 起步,后续再升 `1.11` + +## 参考基线 +- Panda3D 下载页:[panda3d.org/download](https://www.panda3d.org/download/) +- Panda3D 1.11 C++ 手册:[docs.panda3d.org/1.11/cpp](https://docs.panda3d.org/1.11/cpp/index) +- simplepbr 仓库:[github.com/Moguri/panda3d-simplepbr](https://github.com/Moguri/panda3d-simplepbr) +- RmlUi 文档:[mikke89.github.io/RmlUiDoc](https://mikke89.github.io/RmlUiDoc/pages/cpp_manual/building_with_cmake.html) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a93cf88 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# MetaCore + +MetaCore is a new pure C++ editor/runtime workspace scaffolded from the EG redesign plan. + +## Current status + +- New greenfield project structure under `D:\MetaCore` +- Clear module boundaries for `foundation`, `platform`, `engine`, `MetaEditor`, and `MetaPlayer` +- JSON-based project and scene persistence without external dependencies +- Panda3D 1.10.16 SDK, Dear ImGui, ImGuizmo, and RmlUi are now wired into the integration build +- Stable runtime path currently uses a safe Panda-linked platform adapter and shell abstractions +- `DearImGuiEditorShell` is now a real integration handoff point, with a default stable mode and an experimental headless ImGui frame mode +- simplepbr shader resource mirror directory for the future `MetaRender` implementation + +## What is implemented now + +- Top-level CMake project and presets +- Core domain objects: project, scene, assets, editor state, render interfaces +- Minimal `MetaEditor` and `MetaPlayer` entry points +- Project bootstrap that creates: + - `MetaCore.project.json` + - `Scenes/Main.mcscene.json` + - `Library/AssetDB.json` + - default editor layout cache +- Basic tests covering project persistence, scene serialization, and command stack behavior + +## Dependency strategy + +The project still supports a dependency-light scaffold build, and now also supports an integration build with the +installed SDKs and packages. The real integrations are isolated behind: + +- `MetaCore/platform/panda` +- `MetaCore/engine/runtime_ui` +- `MetaCore/engine/render` +- `MetaEditor/IEditorShell.h` +- `MetaEditor/NullEditorShell.cpp` +- `MetaEditor/DearImGuiEditorShell.cpp` + +This keeps project, scene, asset, and editor-domain code isolated while the platform and GUI backends evolve. + +## Suggested next steps + +1. Replace the safe Panda-linked adapter with real Panda window/context creation and message pumping. +2. Replace the current `DearImGuiEditorShell` fallback path with a real Dear ImGui renderer/backend pair. +3. Bind ImGuizmo to `TransformGizmoService` and the editor viewport state. +4. Add RmlUi backend glue into `MetaPlayer`. +5. Replace the current placeholder Panda render backend with a real `MetaRender`. +6. Copy the full simplepbr shader set and integrate the first PBR pass. + +## Configure + +```powershell +cmake --preset vs2022-debug +cmake --build --preset build-debug +ctest --preset test-debug +``` + +## Integration build + +```powershell +cmake --preset vs2022-integration-debug +cmake --build --preset build-integration-debug +ctest --preset test-integration-debug +``` + +`MetaEditor` shell selection: + +- default shell: `NullEditorShell` +- Dear shell: set `METACORE_EDITOR_SHELL=dearimgui` + +Experimental headless ImGui frame generation can be toggled with: + +```powershell +$env:METACORE_ENABLE_HEADLESS_IMGUI = "1" +``` + +This mode is still treated as experimental and is not part of the stable runtime path yet. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e80d734 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,27 @@ +# MetaCore Architecture Notes + +## Runtime split + +- `MetaEditor` owns editor tooling and future Dear ImGui/ImGuizmo integration. +- `MetaPlayer` owns runtime playback and future RmlUi integration. +- `MetaCore` owns shared domain logic and backend abstractions. + +## Data split + +- Project data: `MetaCore.project.json` +- Scene data: `Scenes/*.mcscene.json` +- Asset metadata: `Library/AssetDB.json` and per-asset `.meta` +- Editor layout data: `Library/Layout/editor_layout.json` + +## Backend split + +- `platform/panda` hides Panda-specific details. +- `engine/render` owns renderer-facing types and shader library definitions. +- `engine/runtime_ui` owns runtime UI-facing document abstractions. + +## Design rules + +- UI never mutates the domain directly. +- Scene data never stores editor-only or runtime cache state. +- Renderer configuration is driven by data objects, not by ad-hoc tag strings. +- New engine modules should expose narrow service-oriented interfaces. diff --git a/docs/setup-windows.md b/docs/setup-windows.md new file mode 100644 index 0000000..053f03d --- /dev/null +++ b/docs/setup-windows.md @@ -0,0 +1,96 @@ +# Windows setup notes + +## Current scaffold assumptions + +- Visual Studio 2022 is available +- CMake is available +- `vcpkg` may not be installed yet +- Panda3D C++ SDK may not be installed yet + +## Bootstrap + +```powershell +cd D:\MetaCore +.\scripts\bootstrap_vcpkg.ps1 +.\scripts\detect_panda_sdk.ps1 +cmake --preset vs2022-debug +cmake --build --preset build-debug +ctest --preset test-debug +``` + +## Integration build on this machine + +Installed/confirmed dependencies: + +- Panda3D SDK: `D:\PythonProject\Panda3D-1.10.16-x64` +- `vcpkg`: `D:\vcpkg` +- `imgui` +- `imguizmo` +- `rmlui` +- `fmt` + +Integration configure/build/test: + +```powershell +cd D:\MetaCore +cmake --preset vs2022-integration-debug +cmake --build --preset build-integration-debug +ctest --preset test-integration-debug +``` + +## Confirmed Panda3D SDK path on this machine + +Current detected candidate: + +- `D:\PythonProject\Panda3D-1.10.16-x64` + +The project also exposes `METACORE_PANDA3D_ROOT` as a CMake cache path so the Panda backend integration can +use this installation directly once the stub backend is replaced. + +## External integrations still stubbed + +- Panda3D window/context creation and message loop +- Dear ImGui renderer/backend pair +- ImGuizmo viewport manipulator binding +- RmlUi backend renderer and input bridge + +These are already isolated behind the current app and engine interfaces, so the next implementation step is to replace +the no-op/stub logic rather than redesign the domain model. + +## Editor shell abstraction + +`MetaEditor` now uses: + +- `IEditorShell` as the stable shell interface +- `NullEditorShell` as the default no-dependency implementation +- `DearImGuiEditorShell` as the future Dear ImGui docking implementation + +When `METACORE_ENABLE_IMGUI` is enabled and the real Dear ImGui integration is added, the editor can swap shell +implementations without changing `EditorApplication`, presenter logic, or the editor-domain services. + +`DearImGuiEditorShell` is now the handoff point for: + +- main menu construction +- toolbar group construction +- dock section composition +- inspector field presentation +- shell debug export for layout verification + +Current runtime behavior: + +- `NullEditorShell` is the stable default path +- `DearImGuiEditorShell` is selectable with `METACORE_EDITOR_SHELL=dearimgui` +- `DearImGuiEditorShell` defaults to a stable non-rendering mode +- experimental headless ImGui frame generation can be enabled with `METACORE_ENABLE_HEADLESS_IMGUI=1` +- the experimental headless ImGui mode is not yet considered stable + +## Optional GUI feature toggles + +The project already exposes these CMake options: + +- `METACORE_ENABLE_PANDA3D` +- `METACORE_ENABLE_IMGUI` +- `METACORE_ENABLE_IMGUIZMO` +- `METACORE_ENABLE_RMLUI` + +They currently act as compile-time integration flags and are safe to leave `OFF` until the matching SDKs are installed. diff --git a/scripts/bootstrap_vcpkg.ps1 b/scripts/bootstrap_vcpkg.ps1 new file mode 100644 index 0000000..ee82c02 --- /dev/null +++ b/scripts/bootstrap_vcpkg.ps1 @@ -0,0 +1,19 @@ +param( + [string]$InstallRoot = "D:\\vcpkg" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $InstallRoot)) { + git clone https://github.com/microsoft/vcpkg.git $InstallRoot +} + +Push-Location $InstallRoot +try { + .\bootstrap-vcpkg.bat +} +finally { + Pop-Location +} + +Write-Host "vcpkg is ready at $InstallRoot" diff --git a/scripts/detect_panda_sdk.ps1 b/scripts/detect_panda_sdk.ps1 new file mode 100644 index 0000000..8cbaf6c --- /dev/null +++ b/scripts/detect_panda_sdk.ps1 @@ -0,0 +1,37 @@ +param( + [string[]]$CandidateRoots = @( + "D:\\PythonProject\\Panda3D-1.10.16-x64", + "C:\\Program Files\\Panda3D-1.10.16-x64", + "C:\\Program Files\\Panda3D-1.10.15-x64", + "D:\\Program Files\\Panda3D-1.10.16-x64", + "D:\\Panda3D" + ) +) + +$found = @() + +foreach ($root in $CandidateRoots) { + if (-not (Test-Path $root)) { + continue + } + + $includeDir = Join-Path $root "include" + $libDir = Join-Path $root "lib" + $binDir = Join-Path $root "bin" + + if ((Test-Path $includeDir) -and (Test-Path $libDir)) { + $found += [pscustomobject]@{ + Root = $root + Include = $includeDir + Lib = $libDir + Bin = $binDir + } + } +} + +if ($found.Count -eq 0) { + Write-Host "No Panda3D C++ SDK candidate found." + exit 1 +} + +$found | Format-Table -AutoSize diff --git a/tests/MetaCoreTests.cpp b/tests/MetaCoreTests.cpp new file mode 100644 index 0000000..133f94f --- /dev/null +++ b/tests/MetaCoreTests.cpp @@ -0,0 +1,108 @@ +#include "MetaCore/engine/editor_core/CommandService.h" +#include "MetaCore/engine/editor_core/EditorSession.h" +#include "MetaCore/engine/editor_core/SceneCommands.h" +#include "MetaCore/engine/editor_core/SelectionService.h" +#include "MetaCore/engine/editor_core/ViewportService.h" +#include "MetaCore/engine/scene/Project.h" +#include "MetaCore/engine/scene/Scene.h" +#include "MetaCore/engine/scene/SceneSerializer.h" + +#include +#include +#include + +namespace { + +void expect(const bool condition, const char* message) { + if (!condition) { + std::cerr << "Test failed: " << message << '\n'; + std::exit(1); + } +} + +} // namespace + +int main() { + namespace fs = std::filesystem; + using metacore::engine::editor_core::Command; + using metacore::engine::editor_core::CommandService; + using metacore::engine::editor_core::EditorSession; + using metacore::engine::editor_core::SceneCommands; + using metacore::engine::editor_core::SelectionService; + using metacore::engine::editor_core::ViewportService; + using metacore::engine::scene::Project; + using metacore::engine::scene::ProjectService; + using metacore::engine::scene::Scene; + using metacore::engine::scene::SceneSerializer; + using metacore::engine::scene::SceneService; + + const fs::path root = "D:/MetaCore/TestProject"; + fs::remove_all(root); + + ProjectService project_service; + expect(project_service.create_project(root, "MetaCoreTest"), "project should be created"); + + Project project; + expect(project_service.load_project(root / "MetaCore.project.json", project), "project should load"); + expect(project.name == "MetaCoreTest", "project name should persist"); + + SceneService scene_service; + Scene scene = scene_service.create_default_scene(); + auto& cube = scene_service.create_object(scene, "Cube"); + cube.has_mesh_renderer = true; + cube.mesh_renderer.mesh_asset_id = "mesh.cube"; + + SceneSerializer serializer; + expect(serializer.save_scene(root / "Scenes" / "Test.mcscene.json", scene), "scene should save"); + + Scene loaded_scene; + expect(serializer.load_scene(root / "Scenes" / "Test.mcscene.json", loaded_scene), "scene should load"); + expect(loaded_scene.objects.size() == scene.objects.size(), "scene object count should match"); + + int value = 0; + CommandService commands; + commands.execute(Command{ + "increment", + [&value]() { value += 1; }, + [&value]() { value -= 1; } + }); + expect(value == 1, "command should execute"); + commands.undo(); + expect(value == 0, "command should undo"); + commands.redo(); + expect(value == 1, "command should redo"); + + EditorSession session; + SelectionService selection; + ViewportService viewport; + SceneCommands::create_empty_object(commands, session, selection, scene_service, loaded_scene, "Test Node"); + expect(selection.has_selection(), "selection should exist after object creation"); + SceneCommands::rename_selected_object(commands, session, selection, scene_service, loaded_scene, "Renamed Node"); + expect(scene_service.find_object(loaded_scene, *selection.selection())->get().name == "Renamed Node", "rename should persist"); + metacore::engine::scene::TransformComponent edited_transform; + edited_transform.position = {1.0F, 2.0F, 3.0F}; + edited_transform.rotation = {10.0F, 20.0F, 30.0F}; + edited_transform.scale = {2.0F, 2.0F, 2.0F}; + SceneCommands::apply_transform_to_selected_object(commands, session, selection, scene_service, loaded_scene, edited_transform); + expect( + scene_service.find_object(loaded_scene, *selection.selection())->get().transform.position[0] == 1.0F, + "transform apply should persist" + ); + commands.undo(); + expect( + scene_service.find_object(loaded_scene, *selection.selection())->get().transform.position[0] == 0.0F, + "transform undo should restore" + ); + commands.redo(); + expect( + scene_service.find_object(loaded_scene, *selection.selection())->get().transform.position[0] == 1.0F, + "transform redo should reapply" + ); + SceneCommands::focus_selected_object(session, selection, viewport); + expect(viewport.focus_target().has_value(), "focus target should be set"); + SceneCommands::delete_selected_object(commands, session, selection, scene_service, loaded_scene); + expect(!selection.has_selection(), "selection should clear on delete"); + + std::cout << "MetaCoreTests passed\n"; + return 0; +} diff --git a/third_party/simplepbr-shaders/README.md b/third_party/simplepbr-shaders/README.md new file mode 100644 index 0000000..fcf0ae5 --- /dev/null +++ b/third_party/simplepbr-shaders/README.md @@ -0,0 +1,18 @@ +# simplepbr shader mirror + +This directory is reserved for the MetaCore C++ renderer to consume simplepbr-inspired shader assets. + +Current status: + +- `simplepbr.vert` +- `simplepbr.frag` +- `shadow.vert` +- `shadow.frag` +- `skybox.vert` +- `skybox.frag` +- `post.vert` +- `tonemap.frag` + +The current scaffold includes a mix of directly mirrored and placeholder-compatible files so the runtime +layout and file references already exist. Replace or verify them against upstream before wiring the real +Panda3D render backend. diff --git a/third_party/simplepbr-shaders/post.vert b/third_party/simplepbr-shaders/post.vert new file mode 100644 index 0000000..310be61 --- /dev/null +++ b/third_party/simplepbr-shaders/post.vert @@ -0,0 +1,10 @@ +#version 120 + +attribute vec4 p3d_Vertex; +attribute vec2 p3d_MultiTexCoord0; +varying vec2 v_texcoord; + +void main() { + v_texcoord = p3d_MultiTexCoord0; + gl_Position = p3d_Vertex; +} diff --git a/third_party/simplepbr-shaders/shadow.frag b/third_party/simplepbr-shaders/shadow.frag new file mode 100644 index 0000000..f797fa6 --- /dev/null +++ b/third_party/simplepbr-shaders/shadow.frag @@ -0,0 +1,5 @@ +#version 120 + +void main() { + gl_FragColor = vec4(1.0); +} diff --git a/third_party/simplepbr-shaders/shadow.vert b/third_party/simplepbr-shaders/shadow.vert new file mode 100644 index 0000000..5ebe9a7 --- /dev/null +++ b/third_party/simplepbr-shaders/shadow.vert @@ -0,0 +1,8 @@ +#version 120 + +uniform mat4 p3d_ModelViewProjectionMatrix; +attribute vec4 p3d_Vertex; + +void main() { + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; +} diff --git a/third_party/simplepbr-shaders/simplepbr.frag b/third_party/simplepbr-shaders/simplepbr.frag new file mode 100644 index 0000000..6eb9da6 --- /dev/null +++ b/third_party/simplepbr-shaders/simplepbr.frag @@ -0,0 +1,32 @@ +#version 120 + +uniform struct p3d_MaterialParameters { + vec4 baseColor; + vec4 emission; + float roughness; + float metallic; +} p3d_Material; + +uniform sampler2D p3d_TextureBaseColor; +uniform sampler2D p3d_TextureMetalRoughness; +uniform sampler2D p3d_TextureNormal; +uniform sampler2D p3d_TextureEmission; + +varying vec3 v_view_position; +varying vec3 v_world_position; +varying vec4 v_color; +varying vec2 v_texcoord; +varying mat3 v_view_tbn; +varying mat3 v_world_tbn; + +void main() { + vec4 metal_rough = texture2D(p3d_TextureMetalRoughness, v_texcoord); + float metallic = clamp(p3d_Material.metallic * metal_rough.b, 0.0, 1.0); + float roughness = clamp(p3d_Material.roughness * metal_rough.g, 0.0, 1.0); + vec4 base_color = p3d_Material.baseColor * v_color * texture2D(p3d_TextureBaseColor, v_texcoord); + vec3 normal = normalize(v_view_tbn * (2.0 * texture2D(p3d_TextureNormal, v_texcoord).rgb - 1.0)); + vec3 emission = p3d_Material.emission.rgb * texture2D(p3d_TextureEmission, v_texcoord).rgb; + vec3 lit = base_color.rgb * (0.15 + max(0.0, normal.z) * (1.0 - roughness * 0.5)); + lit = mix(lit, base_color.rgb, metallic * 0.25); + gl_FragColor = vec4(lit + emission, base_color.a); +} diff --git a/third_party/simplepbr-shaders/simplepbr.vert b/third_party/simplepbr-shaders/simplepbr.vert new file mode 100644 index 0000000..b5563d3 --- /dev/null +++ b/third_party/simplepbr-shaders/simplepbr.vert @@ -0,0 +1,46 @@ +#version 120 + +#ifndef MAX_LIGHTS + #define MAX_LIGHTS 8 +#endif + +uniform mat4 p3d_ProjectionMatrix; +uniform mat4 p3d_ModelViewMatrix; +uniform mat4 p3d_ModelMatrix; +uniform mat3 p3d_NormalMatrix; +uniform mat4 p3d_TextureMatrix; +uniform mat4 p3d_ModelMatrixInverseTranspose; + +attribute vec4 p3d_Vertex; +attribute vec4 p3d_Color; +attribute vec3 p3d_Normal; +attribute vec4 p3d_Tangent; +attribute vec2 p3d_MultiTexCoord0; + +varying vec3 v_view_position; +varying vec3 v_world_position; +varying vec4 v_color; +varying vec2 v_texcoord; +varying mat3 v_view_tbn; +varying mat3 v_world_tbn; + +void main() { + vec4 view_position = p3d_ModelViewMatrix * p3d_Vertex; + v_view_position = view_position.xyz; + v_world_position = (p3d_ModelMatrix * p3d_Vertex).xyz; + v_color = p3d_Color; + v_texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0, 0, 1)).xy; + + vec3 view_normal = normalize(p3d_NormalMatrix * p3d_Normal); + vec3 view_tangent = normalize(p3d_NormalMatrix * p3d_Tangent.xyz); + vec3 view_bitangent = cross(view_normal, view_tangent) * p3d_Tangent.w; + v_view_tbn = mat3(view_tangent, view_bitangent, view_normal); + + mat3 world_normal_mat = mat3(p3d_ModelMatrixInverseTranspose); + vec3 world_normal = normalize(world_normal_mat * p3d_Normal); + vec3 world_tangent = normalize(world_normal_mat * p3d_Tangent.xyz); + vec3 world_bitangent = cross(world_normal, world_tangent) * p3d_Tangent.w; + v_world_tbn = mat3(world_tangent, world_bitangent, world_normal); + + gl_Position = p3d_ProjectionMatrix * view_position; +} diff --git a/third_party/simplepbr-shaders/skybox.frag b/third_party/simplepbr-shaders/skybox.frag new file mode 100644 index 0000000..a7e2223 --- /dev/null +++ b/third_party/simplepbr-shaders/skybox.frag @@ -0,0 +1,8 @@ +#version 120 + +uniform samplerCube skybox_tex; +varying vec3 v_direction; + +void main() { + gl_FragColor = textureCube(skybox_tex, normalize(v_direction)); +} diff --git a/third_party/simplepbr-shaders/skybox.vert b/third_party/simplepbr-shaders/skybox.vert new file mode 100644 index 0000000..211332d --- /dev/null +++ b/third_party/simplepbr-shaders/skybox.vert @@ -0,0 +1,10 @@ +#version 120 + +uniform mat4 p3d_ModelViewProjectionMatrix; +attribute vec4 p3d_Vertex; +varying vec3 v_direction; + +void main() { + v_direction = p3d_Vertex.xyz; + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; +} diff --git a/third_party/simplepbr-shaders/tonemap.frag b/third_party/simplepbr-shaders/tonemap.frag new file mode 100644 index 0000000..b8afa4f --- /dev/null +++ b/third_party/simplepbr-shaders/tonemap.frag @@ -0,0 +1,13 @@ +#version 120 + +uniform sampler2D tex; +uniform float exposure; +varying vec2 v_texcoord; + +void main() { + vec4 tex_color = texture2D(tex, v_texcoord); + vec3 color = tex_color.rgb * exposure; + color = max(vec3(0.0), color - vec3(0.004)); + color = (color * (vec3(6.2) * color + vec3(0.5))) / (color * (vec3(6.2) * color + vec3(1.7)) + vec3(0.06)); + gl_FragColor = vec4(color, tex_color.a); +} diff --git a/tools/MetaCoreWindowProbe.cpp b/tools/MetaCoreWindowProbe.cpp new file mode 100644 index 0000000..b930ab0 --- /dev/null +++ b/tools/MetaCoreWindowProbe.cpp @@ -0,0 +1,65 @@ +#include +#include + +#if METACORE_HAS_PANDA3D +#include +#include +#include +#include +#endif + +namespace { + +void log_step(const std::string& message) { + std::cout << "[MetaCoreWindowProbe] " << message << std::endl; +} + +} + +int main() { +#ifdef METACORE_HAS_PANDA3D + log_step("Panda backend compile flag is enabled."); + const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin"; + if (std::filesystem::exists(panda_bin)) { + SetDllDirectoryW(panda_bin.wstring().c_str()); + log_step("DLL search path set to " + panda_bin.string()); + } else { + log_step("Panda bin path not found: " + panda_bin.string()); + } + + log_step("Creating PandaFramework"); + PandaFramework framework; + + log_step("Calling open_framework()"); + framework.open_framework(); + + log_step("Setting MetaCore probe window title"); + framework.set_window_title("MetaCore Window Probe"); + + WindowProperties props; + props.set_size(960, 540); + props.set_title("MetaCore Window Probe"); + + log_step("Calling open_window()"); + WindowFramework* window = framework.open_window(props, 0); + if (window == nullptr) { + log_step("open_window() returned null"); + framework.close_framework(); + return 2; + } + + log_step("Window opened successfully"); + framework.do_frame(nullptr); + log_step("Frame pumped successfully"); + + framework.close_window(window); + log_step("Window closed successfully"); + + framework.close_framework(); + log_step("Framework closed successfully"); + return 0; +#else + log_step("Panda backend is not enabled in this build."); + return 1; +#endif +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..5b3862c --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,25 @@ +{ + "name": "metacore", + "version-string": "0.1.0", + "dependencies": [ + "fmt", + { + "name": "imgui", + "default-features": false, + "features": [ + "docking-experimental", + "win32-binding" + ] + }, + "imguizmo", + "rmlui" + ], + "features": { + "editor-ui": { + "description": "Editor UI dependencies for Dear ImGui and ImGuizmo are handled outside vcpkg in this scaffold." + }, + "panda3d-backend": { + "description": "Panda3D backend is expected to be supplied by a local SDK/toolchain integration." + } + } +}