Files
XCEngine/editor/src/panels/SceneViewPanel.cpp

495 lines
20 KiB
C++
Raw Normal View History

2026-03-26 22:31:22 +08:00
#include "Actions/ActionRouting.h"
#include "Core/IEditorContext.h"
2026-03-29 16:18:13 +08:00
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "SceneViewPanel.h"
#include "Viewport/SceneViewportEditorOverlayData.h"
2026-04-03 17:04:34 +08:00
#include "Viewport/SceneViewportHudOverlay.h"
#include "Viewport/SceneViewportInteractionActions.h"
#include "Viewport/SceneViewportInteractionResolver.h"
#include "Viewport/SceneViewportMath.h"
#include "Viewport/SceneViewportNavigation.h"
2026-04-04 00:45:13 +08:00
#include "Viewport/SceneViewportTransformGizmoCoordinator.h"
2026-03-28 17:04:14 +08:00
#include "ViewportPanelContent.h"
#include "Platform/Win32Utf8.h"
2026-03-26 21:18:33 +08:00
#include "UI/UI.h"
2026-03-28 17:50:54 +08:00
#include <imgui.h>
#include <algorithm>
#include <filesystem>
#include <vector>
namespace XCEngine {
namespace Editor {
2026-03-29 16:18:13 +08:00
namespace {
struct SceneViewportToolOverlayResult {
bool hovered = false;
bool clicked = false;
SceneViewportToolMode clickedTool = SceneViewportToolMode::Move;
};
const char* GetSceneViewportPivotModeLabel(SceneViewportPivotMode mode) {
return mode == SceneViewportPivotMode::Pivot ? "Pivot" : "Center";
}
const char* GetSceneViewportTransformSpaceModeLabel(SceneViewportTransformSpaceMode mode) {
return mode == SceneViewportTransformSpaceMode::Global ? "Global" : "Local";
}
float GetSceneToolbarToggleWidth(const char* firstLabel, const char* secondLabel) {
constexpr float kHorizontalPadding = 10.0f;
constexpr float kMinWidth = 68.0f;
const float maxLabelWidth = (std::max)(
ImGui::CalcTextSize(firstLabel).x,
ImGui::CalcTextSize(secondLabel).x);
return (std::max)(kMinWidth, maxLabelWidth + kHorizontalPadding * 2.0f);
}
void RenderSceneViewportTopBar(
SceneViewportPivotMode& pivotMode,
SceneViewportTransformSpaceMode& transformSpaceMode) {
constexpr float kSceneToolbarHeight = 24.0f;
constexpr float kSceneToolbarPaddingY = 0.0f;
constexpr float kSceneToolbarButtonHeight = kSceneToolbarHeight - kSceneToolbarPaddingY * 2.0f;
constexpr ImVec2 kSceneToolbarButtonFramePadding(8.0f, 1.0f);
UI::PanelToolbarScope toolbar(
"SceneToolbar",
kSceneToolbarHeight,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
true,
ImVec2(UI::ToolbarPadding().x, kSceneToolbarPaddingY),
ImVec2(0.0f, UI::ToolbarItemSpacing().y),
ImVec4(0.23f, 0.23f, 0.23f, 1.0f));
if (!toolbar.IsOpen()) {
return;
}
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, kSceneToolbarButtonFramePadding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
if (UI::ToolbarButton(
GetSceneViewportPivotModeLabel(pivotMode),
true,
ImVec2(GetSceneToolbarToggleWidth("Pivot", "Center"), kSceneToolbarButtonHeight))) {
pivotMode = pivotMode == SceneViewportPivotMode::Pivot
? SceneViewportPivotMode::Center
: SceneViewportPivotMode::Pivot;
}
ImGui::SameLine(0.0f, 0.0f);
if (UI::ToolbarButton(
GetSceneViewportTransformSpaceModeLabel(transformSpaceMode),
true,
ImVec2(GetSceneToolbarToggleWidth("Global", "Local"), kSceneToolbarButtonHeight))) {
transformSpaceMode = transformSpaceMode == SceneViewportTransformSpaceMode::Global
? SceneViewportTransformSpaceMode::Local
: SceneViewportTransformSpaceMode::Global;
}
ImGui::PopStyleVar(2);
}
const char* GetSceneViewportToolTooltip(SceneViewportToolMode toolMode) {
switch (toolMode) {
case SceneViewportToolMode::ViewMove:
return "View Move";
case SceneViewportToolMode::Move:
return "Move";
case SceneViewportToolMode::Rotate:
return "Rotate";
case SceneViewportToolMode::Scale:
return "Scale";
case SceneViewportToolMode::Transform:
return "Transform";
default:
return "";
}
}
const char* GetSceneViewportToolIconBaseName(SceneViewportToolMode toolMode) {
switch (toolMode) {
case SceneViewportToolMode::ViewMove:
return "view_move_tool";
case SceneViewportToolMode::Move:
return "move_tool";
case SceneViewportToolMode::Rotate:
return "rotate_tool";
case SceneViewportToolMode::Scale:
return "scale_tool";
case SceneViewportToolMode::Transform:
return "transform_tool";
default:
return "";
}
}
std::string BuildSceneViewportIconPath(const char* iconBaseName, bool active = false) {
const std::filesystem::path exeDir(
XCEngine::Editor::Platform::Utf8ToWide(XCEngine::Editor::Platform::GetExecutableDirectoryUtf8()));
std::filesystem::path iconPath =
exeDir / L".." / L".." / L"resources" / L"Icons" /
std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(iconBaseName));
if (active) {
iconPath += L"_on";
}
iconPath += L".png";
return XCEngine::Editor::Platform::WideToUtf8(iconPath.lexically_normal().wstring());
}
const std::string& GetSceneViewportToolIconPath(SceneViewportToolMode toolMode, bool active) {
static std::string cachedPaths[5][2] = {};
const size_t toolIndex = static_cast<size_t>(toolMode);
const size_t stateIndex = active ? 1u : 0u;
std::string& cachedPath = cachedPaths[toolIndex][stateIndex];
if (!cachedPath.empty()) {
return cachedPath;
}
cachedPath = BuildSceneViewportIconPath(GetSceneViewportToolIconBaseName(toolMode), active);
return cachedPath;
}
SceneViewportToolOverlayResult RenderSceneViewportToolOverlay(
const ViewportPanelContentResult& content,
SceneViewportToolMode activeTool) {
SceneViewportToolOverlayResult result = {};
if (!content.hasViewportArea) {
return result;
}
constexpr float kButtonExtent = 30.0f;
constexpr float kButtonSpacing = 6.0f;
constexpr float kPanelPadding = 6.0f;
constexpr float kViewportInset = 10.0f;
constexpr float kIconInset = 4.0f;
constexpr size_t kToolCount = 5;
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (drawList == nullptr) {
return result;
}
const ImVec2 panelMin(
content.itemMin.x + kViewportInset,
content.itemMin.y + kViewportInset);
const ImVec2 panelMax(
panelMin.x + kPanelPadding * 2.0f + kButtonExtent,
panelMin.y + kPanelPadding * 2.0f + kToolCount * kButtonExtent + (kToolCount - 1) * kButtonSpacing);
drawList->AddRectFilled(panelMin, panelMax, IM_COL32(24, 26, 29, 220), 7.0f);
drawList->AddRect(panelMin, panelMax, IM_COL32(255, 255, 255, 28), 7.0f, 0, 1.0f);
const SceneViewportToolMode toolModes[kToolCount] = {
SceneViewportToolMode::ViewMove,
SceneViewportToolMode::Move,
SceneViewportToolMode::Rotate,
SceneViewportToolMode::Scale,
SceneViewportToolMode::Transform
};
for (size_t index = 0; index < kToolCount; ++index) {
const SceneViewportToolMode toolMode = toolModes[index];
const bool active = toolMode == activeTool;
const ImVec2 buttonMin(
panelMin.x + kPanelPadding,
panelMin.y + kPanelPadding + index * (kButtonExtent + kButtonSpacing));
const ImVec2 buttonMax(buttonMin.x + kButtonExtent, buttonMin.y + kButtonExtent);
ImGui::SetCursorScreenPos(buttonMin);
const std::string buttonId = std::string("##SceneToolButton") + std::to_string(static_cast<int>(toolMode));
const bool clicked = ImGui::InvisibleButton(
buttonId.c_str(),
ImVec2(kButtonExtent, kButtonExtent),
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_PressedOnClick |
ImGuiButtonFlags_AllowOverlap);
const bool hovered = ImGui::IsItemHovered();
const bool held = ImGui::IsItemActive();
result.hovered = result.hovered || hovered;
result.clicked = result.clicked || clicked;
if (clicked) {
result.clickedTool = toolMode;
}
const ImU32 backgroundColor = ImGui::GetColorU32(
held ? UI::ToolbarButtonActiveColor()
: hovered ? UI::ToolbarButtonHoveredColor(active)
: UI::ToolbarButtonColor(active));
drawList->AddRectFilled(buttonMin, buttonMax, backgroundColor, 5.0f);
drawList->AddRect(buttonMin, buttonMax, IM_COL32(255, 255, 255, active ? 48 : 24), 5.0f, 0, 1.0f);
const ImVec2 iconMin(buttonMin.x + kIconInset, buttonMin.y + kIconInset);
const ImVec2 iconMax(buttonMax.x - kIconInset, buttonMax.y - kIconInset);
if (!UI::DrawTextureAssetPreview(
drawList,
iconMin,
iconMax,
GetSceneViewportToolIconPath(toolMode, active))) {
drawList->AddText(
ImVec2(buttonMin.x + 8.0f, buttonMin.y + 7.0f),
IM_COL32(230, 230, 230, 255),
GetSceneViewportToolTooltip(toolMode));
}
if (hovered) {
ImGui::SetTooltip("%s", GetSceneViewportToolTooltip(toolMode));
}
}
return result;
}
2026-03-29 16:18:13 +08:00
} // namespace
SceneViewPanel::SceneViewPanel() : Panel("Scene") {}
void SceneViewPanel::Render() {
2026-03-29 16:18:13 +08:00
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
UI::PanelWindowScope panel(m_name.c_str());
2026-03-29 16:18:13 +08:00
ImGui::PopStyleVar();
2026-03-26 22:31:22 +08:00
if (!panel.IsOpen()) {
return;
}
RenderSceneViewportTopBar(m_pivotMode, m_transformSpaceMode);
const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) {
const ImGuiIO& io = ImGui::GetIO();
2026-03-29 16:18:13 +08:00
const bool hasInteractiveViewport = content.hasViewportArea && content.frame.hasTexture;
const SceneViewportToolOverlayResult toolOverlay = RenderSceneViewportToolOverlay(content, m_toolMode);
const bool viewportContentHovered = content.hovered && !toolOverlay.hovered;
if (toolOverlay.clicked) {
CancelSceneViewportTransformGizmoFrame(*m_context, m_moveGizmo, m_rotateGizmo, m_scaleGizmo);
m_toolMode = toolOverlay.clickedTool;
}
SceneViewportToolShortcutRequest toolShortcutRequest = {};
toolShortcutRequest.wantTextInput = io.WantTextInput;
toolShortcutRequest.lookDragging = m_navigationState.lookDragging;
toolShortcutRequest.panDragging = m_navigationState.panDragging;
toolShortcutRequest.pressedViewMove = ImGui::IsKeyPressed(ImGuiKey_Q, false);
toolShortcutRequest.pressedMove = ImGui::IsKeyPressed(ImGuiKey_W, false);
toolShortcutRequest.pressedRotate = ImGui::IsKeyPressed(ImGuiKey_E, false);
toolShortcutRequest.pressedScale = ImGui::IsKeyPressed(ImGuiKey_R, false);
const SceneViewportToolShortcutAction toolShortcutAction =
BuildSceneViewportToolShortcutAction(toolShortcutRequest);
if (toolShortcutAction.HasAction()) {
if (toolShortcutAction.cancelMoveGizmo && m_moveGizmo.IsActive()) {
m_moveGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (toolShortcutAction.cancelRotateGizmo && m_rotateGizmo.IsActive()) {
m_rotateGizmo.CancelDrag(&m_context->GetUndoManager());
}
if (toolShortcutAction.cancelScaleGizmo && m_scaleGizmo.IsActive()) {
m_scaleGizmo.CancelDrag(&m_context->GetUndoManager());
}
m_toolMode = toolShortcutAction.targetTool;
2026-03-31 23:45:08 +08:00
}
const bool usingViewMoveTool = m_toolMode == SceneViewportToolMode::ViewMove;
const bool usingTransformTool = m_toolMode == SceneViewportToolMode::Transform;
const bool showingMoveGizmo = m_toolMode == SceneViewportToolMode::Move || usingTransformTool;
const bool showingRotateGizmo = m_toolMode == SceneViewportToolMode::Rotate || usingTransformTool;
const bool showingScaleGizmo = m_toolMode == SceneViewportToolMode::Scale || usingTransformTool;
const bool useCenterPivot = m_pivotMode == SceneViewportPivotMode::Center;
const bool localSpace = m_transformSpaceMode == SceneViewportTransformSpaceMode::Local;
const SceneViewportTransformGizmoFrameOptions gizmoFrameOptions =
BuildSceneViewportTransformGizmoFrameOptions(
useCenterPivot,
localSpace,
usingTransformTool,
showingMoveGizmo,
showingRotateGizmo,
showingScaleGizmo);
const Math::Vector2 viewportSize(content.availableSize.x, content.availableSize.y);
const Math::Vector2 localMousePosition(
io.MousePos.x - content.itemMin.x,
io.MousePos.y - content.itemMin.y);
2026-03-29 16:18:13 +08:00
SceneViewportOverlayData overlay = {};
SceneViewportTransformGizmoFrameUpdate interactionGizmoFrame = {};
SceneViewportTransformGizmoFrameState gizmoFrameState = {};
SceneViewportOverlayFrameData emptySceneOverlayFrameData = {};
2026-03-29 16:18:13 +08:00
if (hasInteractiveViewport) {
overlay = viewportHostService->GetSceneViewOverlayData();
interactionGizmoFrame = RefreshAndSubmitSceneViewportTransformGizmoFrame(
*viewportHostService,
BuildSceneViewportTransformGizmoRefreshRequest(
*m_context,
overlay,
viewportSize,
localMousePosition,
gizmoFrameOptions),
m_moveGizmo,
m_rotateGizmo,
m_scaleGizmo);
gizmoFrameState = interactionGizmoFrame.frameState;
2026-03-31 23:45:08 +08:00
} else {
CancelSceneViewportTransformGizmoFrame(
*m_context,
m_moveGizmo,
m_rotateGizmo,
m_scaleGizmo);
2026-03-29 16:18:13 +08:00
}
2026-04-04 00:45:13 +08:00
const SceneViewportTransformGizmoOverlaySubmission interactionGizmoSubmission =
hasInteractiveViewport
? interactionGizmoFrame.overlaySubmission
2026-04-04 00:45:13 +08:00
: SceneViewportTransformGizmoOverlaySubmission{};
const SceneViewportOverlayFrameData& interactionOverlayFrameData =
hasInteractiveViewport
? viewportHostService->GetSceneViewEditorOverlayFrameData(*m_context)
: emptySceneOverlayFrameData;
2026-04-04 00:45:13 +08:00
const SceneViewportActiveGizmoKind activeGizmoKind = interactionGizmoSubmission.activeGizmoKind;
const bool gizmoActive = interactionGizmoSubmission.GizmoActive();
2026-04-03 17:04:34 +08:00
const SceneViewportHudOverlayData interactionHudOverlay =
BuildSceneViewportHudOverlayData(overlay);
SceneViewportInteractionResult hoveredInteraction = {};
const bool canResolveViewportInteraction = CanResolveSceneViewportInteraction(
hasInteractiveViewport,
viewportContentHovered,
usingViewMoveTool,
m_navigationState,
gizmoActive);
if (canResolveViewportInteraction) {
SceneViewportInteractionResolveRequest interactionRequest = {};
interactionRequest.overlayFrameData = &interactionOverlayFrameData;
interactionRequest.viewportSize = viewportSize;
interactionRequest.localMousePosition = localMousePosition;
interactionRequest.hudOverlay = &interactionHudOverlay;
interactionRequest.viewportMin = content.itemMin;
interactionRequest.viewportMax = content.itemMax;
interactionRequest.absoluteMousePosition = io.MousePos;
hoveredInteraction = ResolveSceneViewportInteraction(interactionRequest);
}
ApplySceneViewportHoveredHandleState(
BuildSceneViewportHoveredHandleState(hoveredInteraction),
gizmoActive,
showingMoveGizmo,
m_moveGizmo,
showingRotateGizmo,
m_rotateGizmo,
showingScaleGizmo,
m_scaleGizmo);
const SceneViewportInteractionActions interactionActions =
BuildSceneViewportInteractionActions(
hoveredInteraction,
hasInteractiveViewport,
content.clickedLeft,
canResolveViewportInteraction);
SceneViewportNavigationRequest navigationRequest = {};
navigationRequest.state = m_navigationState;
navigationRequest.hasInteractiveViewport = hasInteractiveViewport;
navigationRequest.viewportHovered = viewportContentHovered;
navigationRequest.usingViewMoveTool = usingViewMoveTool;
navigationRequest.gizmoActive = gizmoActive;
navigationRequest.clickedLeft = content.clickedLeft;
navigationRequest.clickedRight = content.clickedRight;
navigationRequest.clickedMiddle = content.clickedMiddle;
navigationRequest.leftMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
navigationRequest.rightMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
navigationRequest.middleMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
const SceneViewportNavigationUpdate navigationUpdate =
UpdateSceneViewportNavigationState(navigationRequest);
m_navigationState = navigationUpdate.state;
if (toolOverlay.clicked || interactionActions.HasClickAction() || navigationUpdate.beginLookDrag ||
navigationUpdate.beginPanDrag) {
ImGui::SetWindowFocus();
}
2026-04-04 00:45:13 +08:00
ExecuteSceneViewportTransformGizmoLifecycleCommand(
BuildBeginSceneViewportTransformGizmoLifecycleCommand(interactionActions),
m_context->GetUndoManager(),
gizmoFrameState,
m_moveGizmo,
m_rotateGizmo,
m_scaleGizmo);
2026-03-29 16:18:13 +08:00
DispatchSceneViewportInteractionActions(
interactionActions,
*m_context,
*viewportHostService,
content.availableSize,
localMousePosition);
2026-04-04 00:45:13 +08:00
ExecuteSceneViewportTransformGizmoLifecycleCommand(
BuildFrameSceneViewportTransformGizmoLifecycleCommand(
activeGizmoKind,
ImGui::IsMouseDown(ImGuiMouseButton_Left)),
m_context->GetUndoManager(),
gizmoFrameState,
m_moveGizmo,
m_rotateGizmo,
m_scaleGizmo);
2026-03-29 16:18:13 +08:00
const SceneViewportCaptureFlags captureFlags = BuildSceneViewportCaptureFlags(
SceneViewportCaptureRequest{
m_navigationState,
m_moveGizmo.IsActive(),
m_rotateGizmo.IsActive(),
m_scaleGizmo.IsActive()});
if (captureFlags.captureMouse) {
ImGui::SetNextFrameWantCaptureMouse(true);
}
if (captureFlags.captureKeyboard) {
ImGui::SetNextFrameWantCaptureKeyboard(true);
}
SceneViewportInputBuildRequest inputRequest = {};
inputRequest.state = m_navigationState;
inputRequest.viewportSize = content.availableSize;
inputRequest.mouseDelta = io.MouseDelta;
inputRequest.mouseWheel = io.MouseWheel;
inputRequest.deltaTime = io.DeltaTime;
inputRequest.viewportHovered = viewportContentHovered;
inputRequest.viewportFocused = content.focused;
inputRequest.wantTextInput = io.WantTextInput;
inputRequest.fastMove = io.KeyShift;
inputRequest.focusSelectionKeyPressed = ImGui::IsKeyPressed(ImGuiKey_F, false);
inputRequest.moveForwardKeyDown = ImGui::IsKeyDown(ImGuiKey_W);
inputRequest.moveBackwardKeyDown = ImGui::IsKeyDown(ImGuiKey_S);
inputRequest.moveRightKeyDown = ImGui::IsKeyDown(ImGuiKey_D);
inputRequest.moveLeftKeyDown = ImGui::IsKeyDown(ImGuiKey_A);
inputRequest.moveUpKeyDown = ImGui::IsKeyDown(ImGuiKey_E);
inputRequest.moveDownKeyDown = ImGui::IsKeyDown(ImGuiKey_Q);
const SceneViewportInput input = BuildSceneViewportInput(inputRequest);
viewportHostService->UpdateSceneViewInput(*m_context, input);
2026-03-28 17:50:54 +08:00
if (content.hasViewportArea && content.frame.hasTexture) {
2026-03-29 16:18:13 +08:00
overlay = viewportHostService->GetSceneViewOverlayData();
RefreshAndSubmitSceneViewportTransformGizmoFrame(
*viewportHostService,
BuildSceneViewportTransformGizmoRefreshRequest(
*m_context,
overlay,
viewportSize,
localMousePosition,
gizmoFrameOptions),
m_moveGizmo,
m_rotateGizmo,
m_scaleGizmo);
2026-03-31 23:45:08 +08:00
2026-04-03 17:04:34 +08:00
DrawSceneViewportHudOverlay(
2026-03-28 17:50:54 +08:00
ImGui::GetWindowDrawList(),
2026-04-03 17:04:34 +08:00
BuildSceneViewportHudOverlayData(overlay),
2026-03-28 17:50:54 +08:00
content.itemMin,
2026-04-03 17:04:34 +08:00
content.itemMax);
2026-03-28 17:50:54 +08:00
}
}
2026-03-26 22:31:22 +08:00
Actions::ObserveInactiveActionRoute(*m_context);
}
}
}