Initial MetaCore scaffold and editor shell integration

This commit is contained in:
ayuan9957 2026-03-19 10:49:58 +08:00
commit 8d6e6bc0da
85 changed files with 5010 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
build/
vcpkg_installed/
downloads/
SandboxProject*/
TestProject/
imgui.ini
*.txt
*.log
*.obj
*.pdb
*.ilk
*.lib
*.exe
*.vcxproj*
*.sln
.vs/
CMakeUserPresets.json

85
CMakePresets.json Normal file
View File

@ -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
}
}
]
}

View File

@ -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<std::string, AssetRecord>& AssetDatabase::records() {
return records_;
}
const std::map<std::string, AssetRecord>& 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

View File

@ -0,0 +1,43 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
namespace metacore::engine::assets {
struct AssetRecord {
std::string id;
std::string type;
std::string relative_path;
};
class AssetDatabase {
public:
std::map<std::string, AssetRecord>& records();
const std::map<std::string, AssetRecord>& records() const;
bool save(const std::filesystem::path& asset_db_file) const;
bool load(const std::filesystem::path& asset_db_file);
private:
std::map<std::string, AssetRecord> 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

View File

@ -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

View File

@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
namespace metacore::engine::editor_core {
struct Command {
std::string label;
std::function<void()> execute;
std::function<void()> undo;
};
class CommandService {
public:
void execute(Command command);
bool can_undo() const;
bool can_redo() const;
void undo();
void redo();
private:
std::vector<Command> undo_stack_;
std::vector<Command> redo_stack_;
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,21 @@
#include "MetaCore/engine/editor_core/EditorActions.h"
namespace metacore::engine::editor_core {
std::vector<EditorAction> 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

View File

@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
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<EditorAction> build_default_actions() const;
};
} // namespace metacore::engine::editor_core

View File

@ -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

View File

@ -0,0 +1,24 @@
#pragma once
#include <filesystem>
#include <string>
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

View File

@ -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<ConsoleMessage>& 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

View File

@ -0,0 +1,50 @@
#pragma once
#include "MetaCore/engine/editor_core/EditorLayoutService.h"
#include <string>
#include <vector>
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<ConsoleMessage>& 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<ConsoleMessage> console_messages_;
PanelState panel_state_{};
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,22 @@
#include "MetaCore/engine/editor_core/InspectorService.h"
namespace metacore::engine::editor_core {
std::optional<std::reference_wrapper<const metacore::engine::scene::SceneObject>> 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

View File

@ -0,0 +1,18 @@
#pragma once
#include "MetaCore/engine/editor_core/SelectionService.h"
#include "MetaCore/engine/scene/Scene.h"
#include <optional>
namespace metacore::engine::editor_core {
class InspectorService {
public:
std::optional<std::reference_wrapper<const metacore::engine::scene::SceneObject>> current_selection(
const metacore::engine::scene::Scene& scene,
const SelectionService& selection_service
) const;
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,106 @@
#include "MetaCore/engine/editor_core/PanelViewModels.h"
#include <algorithm>
namespace metacore::engine::editor_core {
std::vector<HierarchyItem> PanelViewModels::build_hierarchy(
const metacore::engine::scene::Scene& scene,
const SelectionService& selection_service
) const {
std::vector<HierarchyItem> 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<ProjectAssetItem> PanelViewModels::build_project_assets(
const metacore::engine::assets::AssetDatabase& asset_database
) const {
std::vector<ProjectAssetItem> 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<ConsoleMessage>& PanelViewModels::console_messages(const EditorSession& session) const {
return session.console_messages();
}
} // namespace metacore::engine::editor_core

View File

@ -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 <string>
#include <vector>
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<InspectorField> fields;
};
class PanelViewModels {
public:
[[nodiscard]] std::vector<HierarchyItem> build_hierarchy(
const metacore::engine::scene::Scene& scene,
const SelectionService& selection_service
) const;
[[nodiscard]] std::vector<ProjectAssetItem> 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<ConsoleMessage>& console_messages(
const EditorSession& session
) const;
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,160 @@
#include "MetaCore/engine/editor_core/SceneCommands.h"
#include <utility>
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

View File

@ -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

View File

@ -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<metacore::engine::scene::EntityId>& SelectionService::selection() const {
return selected_id_;
}
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,20 @@
#pragma once
#include "MetaCore/engine/scene/Components.h"
#include <optional>
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<metacore::engine::scene::EntityId>& selection() const;
private:
std::optional<metacore::engine::scene::EntityId> selected_id_;
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,63 @@
#include "MetaCore/engine/editor_core/ShellComposition.h"
#include <map>
namespace metacore::engine::editor_core {
ShellComposition ShellCompositionBuilder::build(
const std::vector<EditorAction>& 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<DockRegion, std::vector<DockPanel>> 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

View File

@ -0,0 +1,40 @@
#pragma once
#include "MetaCore/engine/editor_core/EditorActions.h"
#include "MetaCore/engine/editor_core/WorkspaceLayout.h"
#include <string>
#include <vector>
namespace metacore::engine::editor_core {
struct MenuEntry {
std::string menu_name;
std::vector<EditorAction> actions;
};
struct ToolbarGroup {
std::string name;
std::vector<EditorAction> actions;
};
struct PanelSection {
DockRegion region = DockRegion::CenterViewport;
std::vector<DockPanel> panels;
};
struct ShellComposition {
std::vector<MenuEntry> menus;
std::vector<ToolbarGroup> toolbar_groups;
std::vector<PanelSection> sections;
};
class ShellCompositionBuilder {
public:
[[nodiscard]] ShellComposition build(
const std::vector<EditorAction>& actions,
const WorkspaceLayout& workspace
) const;
};
} // namespace metacore::engine::editor_core

View File

@ -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<float, 3> 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<float, 3>& TransformGizmoService::snap_values() const {
return snap_values_;
}
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,37 @@
#pragma once
#include <array>
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<float, 3> snap_values);
[[nodiscard]] Operation operation() const;
[[nodiscard]] Space space() const;
[[nodiscard]] bool snap_enabled() const;
[[nodiscard]] const std::array<float, 3>& snap_values() const;
private:
Operation operation_ = Operation::Translate;
Space space_ = Space::Local;
bool snap_enabled_ = false;
std::array<float, 3> snap_values_{1.0F, 1.0F, 1.0F};
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,25 @@
#include "MetaCore/engine/editor_core/ViewportService.h"
namespace metacore::engine::editor_core {
void ViewportService::set_camera_position(std::array<float, 3> position) {
camera_position_ = position;
}
const std::array<float, 3>& 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<metacore::engine::scene::EntityId>& ViewportService::focus_target() const {
return focus_target_;
}
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,24 @@
#pragma once
#include "MetaCore/engine/scene/Components.h"
#include <array>
#include <optional>
namespace metacore::engine::editor_core {
class ViewportService {
public:
void set_camera_position(std::array<float, 3> position);
[[nodiscard]] const std::array<float, 3>& camera_position() const;
void focus_selection(const metacore::engine::scene::EntityId& id);
void clear_focus_target();
[[nodiscard]] const std::optional<metacore::engine::scene::EntityId>& focus_target() const;
private:
std::array<float, 3> camera_position_{0.0F, -10.0F, 3.0F};
std::optional<metacore::engine::scene::EntityId> focus_target_;
};
} // namespace metacore::engine::editor_core

View File

@ -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

View File

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <vector>
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<DockPanel> panels;
};
class WorkspaceLayoutFactory {
public:
[[nodiscard]] WorkspaceLayout build_default_layout() const;
};
} // namespace metacore::engine::editor_core

View File

@ -0,0 +1,60 @@
#pragma once
#include <array>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace metacore::engine::render {
struct ShaderVariantKey {
std::map<std::string, std::string> defines;
};
struct MaterialDescriptor {
std::array<float, 4> base_color{1.0F, 1.0F, 1.0F, 1.0F};
std::array<float, 4> 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<float, 3> camera_world_position{0.0F, 0.0F, 0.0F};
float exposure = 1.0F;
};
struct LightUniforms {
struct LightEntry {
std::array<float, 3> color{1.0F, 1.0F, 1.0F};
std::array<float, 3> position{0.0F, 0.0F, 0.0F};
float intensity = 1.0F;
};
std::vector<LightEntry> 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

View File

@ -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<std::string, std::filesystem::path> 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

View File

@ -0,0 +1,19 @@
#pragma once
#include <filesystem>
#include <map>
namespace metacore::engine::render {
class SimplePbrShaderLibrary {
public:
explicit SimplePbrShaderLibrary(std::filesystem::path root);
[[nodiscard]] const std::filesystem::path& root() const;
[[nodiscard]] std::map<std::string, std::filesystem::path> shader_files() const;
private:
std::filesystem::path root_;
};
} // namespace metacore::engine::render

View File

@ -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<RuntimeUiDocument> RmlUiBridge::collect_documents(const metacore::engine::scene::Scene& scene) const {
std::vector<RuntimeUiDocument> 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

View File

@ -0,0 +1,32 @@
#pragma once
#include "MetaCore/engine/scene/Scene.h"
#include <filesystem>
#include <string>
#include <vector>
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<RuntimeUiDocument> 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

View File

@ -0,0 +1,78 @@
#pragma once
#include <array>
#include <string>
namespace metacore::engine::scene {
using EntityId = std::string;
struct TransformComponent {
std::array<float, 3> position{0.0F, 0.0F, 0.0F};
std::array<float, 3> rotation{0.0F, 0.0F, 0.0F};
std::array<float, 3> 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<float, 3> 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

View File

@ -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",
"<rml>\n"
" <head>\n"
" <link type=\"text/rcss\" href=\"main_menu.rcss\" />\n"
" </head>\n"
" <body>\n"
" <div class=\"root-panel\">\n"
" <h1>MetaCore</h1>\n"
" <p>Runtime UI scaffold</p>\n"
" </div>\n"
" </body>\n"
"</rml>\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

View File

@ -0,0 +1,23 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
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<std::string> 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

View File

@ -0,0 +1,96 @@
#include "MetaCore/engine/scene/Scene.h"
#include "MetaCore/foundation/Guid.h"
#include <algorithm>
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<std::reference_wrapper<SceneObject>> 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<std::reference_wrapper<const SceneObject>> 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<EntityId> 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

View File

@ -0,0 +1,32 @@
#pragma once
#include "MetaCore/engine/scene/Components.h"
#include <functional>
#include <optional>
#include <string>
#include <vector>
namespace metacore::engine::scene {
struct Scene {
std::string name = "Main";
std::vector<SceneObject> 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<std::reference_wrapper<SceneObject>> find_object(Scene& scene, const EntityId& id) const;
std::optional<std::reference_wrapper<const SceneObject>> 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

View File

@ -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<float, 3>& 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<float, 3> read_array3(const JsonValue& value, const std::array<float, 3>& fallback) {
if (!value.is_array() || value.as_array().size() != 3) {
return fallback;
}
return {
static_cast<float>(value.as_array()[0].as_number(fallback[0])),
static_cast<float>(value.as_array()[1].as_number(fallback[1])),
static_cast<float>(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<float>(light["intensity"].as_number(object.light.intensity));
object.light.range = static_cast<float>(light["range"].as_number(object.light.range));
object.light.spot_angle_degrees = static_cast<float>(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<float>(camera["fov_degrees"].as_number(object.camera.fov_degrees));
object.camera.near_clip = static_cast<float>(camera["near_clip"].as_number(object.camera.near_clip));
object.camera.far_clip = static_cast<float>(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

View File

@ -0,0 +1,15 @@
#pragma once
#include "MetaCore/engine/scene/Scene.h"
#include <filesystem>
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

View File

@ -0,0 +1,44 @@
#include "MetaCore/foundation/FileSystem.h"
#include <fstream>
#include <sstream>
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<std::string> 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

View File

@ -0,0 +1,13 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
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<std::string> read_text_file(const std::filesystem::path& path);
} // namespace metacore::foundation

View File

@ -0,0 +1,30 @@
#include "MetaCore/foundation/Guid.h"
#include <atomic>
#include <chrono>
#include <iomanip>
#include <random>
#include <sstream>
namespace metacore::foundation {
std::string generate_guid() {
static std::mt19937_64 rng(std::random_device{}());
static std::uniform_int_distribution<unsigned long long> dist;
static std::atomic<unsigned long long> counter{0};
const auto now = static_cast<unsigned long long>(
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

View File

@ -0,0 +1,9 @@
#pragma once
#include <string>
namespace metacore::foundation {
std::string generate_guid();
} // namespace metacore::foundation

View File

@ -0,0 +1,339 @@
#include "MetaCore/foundation/Json.h"
#include <cctype>
#include <iomanip>
#include <sstream>
#include <stdexcept>
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<unsigned char>(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<unsigned char>(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<std::size_t>(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<double>(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<std::nullptr_t>(value_); }
bool JsonValue::is_bool() const { return std::holds_alternative<bool>(value_); }
bool JsonValue::is_number() const { return std::holds_alternative<double>(value_); }
bool JsonValue::is_string() const { return std::holds_alternative<std::string>(value_); }
bool JsonValue::is_array() const { return std::holds_alternative<array_type>(value_); }
bool JsonValue::is_object() const { return std::holds_alternative<object_type>(value_); }
bool JsonValue::as_bool(const bool fallback) const {
return is_bool() ? std::get<bool>(value_) : fallback;
}
double JsonValue::as_number(const double fallback) const {
return is_number() ? std::get<double>(value_) : fallback;
}
int JsonValue::as_int(const int fallback) const {
return static_cast<int>(as_number(static_cast<double>(fallback)));
}
const std::string& JsonValue::as_string() const {
static const std::string empty;
return is_string() ? std::get<std::string>(value_) : empty;
}
const JsonValue::array_type& JsonValue::as_array() const {
static const array_type empty;
return is_array() ? std::get<array_type>(value_) : empty;
}
const JsonValue::object_type& JsonValue::as_object() const {
static const object_type empty;
return is_object() ? std::get<object_type>(value_) : empty;
}
JsonValue& JsonValue::operator[](const std::string& key) {
if (!is_object()) {
value_ = object_type{};
}
return std::get<object_type>(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<object_type>(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<object_type>(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

View File

@ -0,0 +1,52 @@
#pragma once
#include <map>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace metacore::foundation {
class JsonValue {
public:
using object_type = std::map<std::string, JsonValue>;
using array_type = std::vector<JsonValue>;
using value_type = std::variant<std::nullptr_t, bool, double, std::string, array_type, object_type>;
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

View File

@ -0,0 +1,30 @@
#include "MetaCore/foundation/Log.h"
#include <iostream>
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

16
MetaCore/foundation/Log.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <string_view>
namespace metacore::foundation {
enum class LogLevel {
Debug,
Info,
Warning,
Error,
};
void log(LogLevel level, std::string_view message);
} // namespace metacore::foundation

View File

@ -0,0 +1,161 @@
#include "MetaCore/platform/panda/PandaPlatformContext.h"
#include "MetaCore/foundation/Log.h"
#if METACORE_HAS_PANDA3D
#include <pandaFramework.h>
#include <windowProperties.h>
#endif
#include <filesystem>
#include <iostream>
#include <windows.h>
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<NativeState>();
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

View File

@ -0,0 +1,41 @@
#pragma once
#include <memory>
#include <filesystem>
#include <string>
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<NativeState> native_state_;
};
} // namespace metacore::platform::panda

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,683 @@
#include "MetaEditor/DearImGuiEditorShell.h"
#include "MetaCore/foundation/FileSystem.h"
#if METACORE_HAS_IMGUI
#include <imgui.h>
#include <imgui_internal.h>
#endif
#if METACORE_HAS_IMGUIZMO
#include <ImGuizmo.h>
#endif
#include <cmath>
#include <cstdlib>
#include <string_view>
#include <sstream>
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<float, 16> 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<float, 16> make_view_matrix(const std::array<float, 3>& 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<float, 16> 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, &center_id);
ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24F, &right_id, &center_id);
ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26F, &bottom_id, &center_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<int>(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

View File

@ -0,0 +1,53 @@
#pragma once
#include "MetaEditor/IEditorShell.h"
#include <filesystem>
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

View File

@ -0,0 +1,152 @@
#include "MetaEditor/EditorApplication.h"
#include "MetaEditor/DearImGuiEditorShell.h"
#include "MetaCore/foundation/Log.h"
#include <algorithm>
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

View File

@ -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 <filesystem>
#include <memory>
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<IEditorShell> editor_shell_{};
};
} // namespace metacore::editor

View File

@ -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

View File

@ -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 <array>
#include <string>
#include <vector>
namespace metacore::editor {
struct EditorFrameSnapshot {
struct ViewportSnapshot {
std::string selected_object_id;
std::string selected_object_name;
std::array<float, 3> camera_position{0.0F, -10.0F, 3.0F};
std::array<float, 3> object_position{0.0F, 0.0F, 0.0F};
std::array<float, 3> object_rotation{0.0F, 0.0F, 0.0F};
std::array<float, 3> object_scale{1.0F, 1.0F, 1.0F};
std::array<float, 3> 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<metacore::engine::editor_core::EditorAction> actions;
metacore::engine::editor_core::WorkspaceLayout workspace;
metacore::engine::editor_core::ShellComposition shell;
std::vector<metacore::engine::editor_core::HierarchyItem> hierarchy;
std::vector<metacore::engine::editor_core::ProjectAssetItem> assets;
metacore::engine::editor_core::InspectorViewModel inspector;
ViewportSnapshot viewport;
std::vector<metacore::engine::editor_core::ConsoleMessage> 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

43
MetaEditor/IEditorShell.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include "MetaEditor/EditorPresenter.h"
#include <array>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
namespace metacore::editor {
struct PendingShellEdits {
struct TransformEdit {
std::string entity_id;
std::array<float, 3> position{0.0F, 0.0F, 0.0F};
std::array<float, 3> rotation{0.0F, 0.0F, 0.0F};
std::array<float, 3> scale{1.0F, 1.0F, 1.0F};
};
std::optional<TransformEdit> 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<IEditorShell> create_default_editor_shell();
} // namespace metacore::editor

View File

@ -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 <cstdlib>
#include <sstream>
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<IEditorShell> 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<DearImGuiEditorShell>();
}
if (value == "null") {
return std::make_unique<NullEditorShell>();
}
}
#if defined(METACORE_HAS_IMGUI) && METACORE_HAS_IMGUI
return std::make_unique<DearImGuiEditorShell>();
#else
return std::make_unique<NullEditorShell>();
#endif
}
} // namespace metacore::editor

View File

@ -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

31
MetaEditor/main.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "MetaEditor/EditorApplication.h"
#include <cstdlib>
#include <iostream>
#include <filesystem>
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;
}

View File

@ -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

View File

@ -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 <filesystem>
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

View File

@ -0,0 +1,24 @@
#include "MetaPlayer/RuntimeUiPreviewSession.h"
#include <sstream>
namespace metacore::player {
void RuntimeUiPreviewSession::set_documents(std::vector<metacore::engine::runtime_ui::RuntimeUiDocument> documents) {
documents_ = std::move(documents);
}
const std::vector<metacore::engine::runtime_ui::RuntimeUiDocument>& 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

View File

@ -0,0 +1,20 @@
#pragma once
#include "MetaCore/engine/runtime_ui/RmlUiBridge.h"
#include <string>
#include <vector>
namespace metacore::player {
class RuntimeUiPreviewSession {
public:
void set_documents(std::vector<metacore::engine::runtime_ui::RuntimeUiDocument> documents);
[[nodiscard]] const std::vector<metacore::engine::runtime_ui::RuntimeUiDocument>& documents() const;
[[nodiscard]] std::string describe() const;
private:
std::vector<metacore::engine::runtime_ui::RuntimeUiDocument> documents_;
};
} // namespace metacore::player

31
MetaPlayer/main.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "MetaPlayer/PlayerApplication.h"
#include <cstdlib>
#include <iostream>
#include <filesystem>
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;
}

99
PLAN.md Normal file
View File

@ -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`
- 编辑器 UIDear ImGui docking + ImGuizmo
- 运行时 UIRmlUi
- 渲染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)

77
README.md Normal file
View File

@ -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.

27
docs/architecture.md Normal file
View File

@ -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.

96
docs/setup-windows.md Normal file
View File

@ -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.

View File

@ -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"

View File

@ -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

108
tests/MetaCoreTests.cpp Normal file
View File

@ -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 <cstdlib>
#include <filesystem>
#include <iostream>
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;
}

18
third_party/simplepbr-shaders/README.md vendored Normal file
View File

@ -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.

10
third_party/simplepbr-shaders/post.vert vendored Normal file
View File

@ -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;
}

View File

@ -0,0 +1,5 @@
#version 120
void main() {
gl_FragColor = vec4(1.0);
}

View File

@ -0,0 +1,8 @@
#version 120
uniform mat4 p3d_ModelViewProjectionMatrix;
attribute vec4 p3d_Vertex;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
#version 120
uniform samplerCube skybox_tex;
varying vec3 v_direction;
void main() {
gl_FragColor = textureCube(skybox_tex, normalize(v_direction));
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -0,0 +1,65 @@
#include <iostream>
#include <string>
#if METACORE_HAS_PANDA3D
#include <pandaFramework.h>
#include <windowProperties.h>
#include <windows.h>
#include <filesystem>
#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
}

25
vcpkg.json Normal file
View File

@ -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."
}
}
}