MetaCore/MetaEditor/DearImGuiEditorShell.cpp

684 lines
25 KiB
C++

#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