#include "Features/Scene/SceneViewportController.h" #include "Rendering/Viewport/ViewportHostService.h" #include "Scene/EditorSceneRuntime.h" #include "State/EditorCommandFocusService.h" #include "Composition/EditorPanelIds.h" #include #include #include #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; constexpr float kWheelDeltaPerStep = 120.0f; bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } bool IsKeyDown( const UIEditorViewportInputBridgeState& state, KeyCode keyCode) { return IsUIEditorViewportInputBridgeKeyDown( state, static_cast(keyCode)); } bool WasKeyPressed( const UIEditorViewportInputBridgeFrame& frame, KeyCode keyCode) { const std::int32_t expected = static_cast(keyCode); return std::find( frame.pressedKeyCodes.begin(), frame.pressedKeyCodes.end(), expected) != frame.pressedKeyCodes.end(); } bool HasCameraInput(const SceneViewportCameraInputState& input) { return input.lookDeltaX != 0.0f || input.lookDeltaY != 0.0f || input.panDeltaX != 0.0f || input.panDeltaY != 0.0f || input.zoomDelta != 0.0f || input.flySpeedDelta != 0.0f || input.moveForward != 0.0f || input.moveRight != 0.0f || input.moveUp != 0.0f; } float NormalizeWheelDelta(float wheelDelta) { return wheelDelta / kWheelDeltaPerStep; } bool IsViewMoveTool(SceneToolMode mode) { return mode == SceneToolMode::View; } bool IsPanDragButtonDown( const UIEditorViewportInputBridgeState& state, UIPointerButton button) { return button != UIPointerButton::None && IsUIEditorViewportInputBridgePointerButtonDown(state, button); } bool TryResolveToolModeShortcut( const UIEditorViewportInputBridgeFrame& inputFrame, SceneToolMode& outMode) { if (inputFrame.modifiers.control || inputFrame.modifiers.alt || inputFrame.modifiers.super) { return false; } if (WasKeyPressed(inputFrame, KeyCode::Q)) { outMode = SceneToolMode::View; return true; } if (WasKeyPressed(inputFrame, KeyCode::W)) { outMode = SceneToolMode::Translate; return true; } if (WasKeyPressed(inputFrame, KeyCode::E)) { outMode = SceneToolMode::Rotate; return true; } if (WasKeyPressed(inputFrame, KeyCode::R)) { outMode = SceneToolMode::Scale; return true; } return false; } void ApplySceneToolMode( SceneToolMode mode, EditorSceneRuntime& sceneRuntime, LegacySceneViewportGizmo& legacyGizmo) { if (sceneRuntime.GetToolMode() == mode) { return; } legacyGizmo.CancelDrag(sceneRuntime); sceneRuntime.SetToolMode(mode); } } // namespace void SceneViewportController::Initialize( const std::filesystem::path& repoRoot, Host::TextureHost& renderer) { m_toolOverlay.Initialize(repoRoot, renderer); ResetInteractionState(); } void SceneViewportController::Shutdown(Host::TextureHost& renderer) { m_toolOverlay.Shutdown(renderer); ResetInteractionState(); } void SceneViewportController::ResetInteractionState() { ResetFrameState(); m_toolOverlay.ResetFrame(); m_legacyGizmo.ResetVisualState(); } void SceneViewportController::SetCommandFocusService( EditorCommandFocusService* commandFocusService) { m_commandFocusService = commandFocusService; } void SceneViewportController::Update( EditorSceneRuntime& sceneRuntime, ViewportHostService& viewportHostService, const UIEditorWorkspaceComposeState& composeState, const UIEditorWorkspaceComposeFrame& composeFrame) { const UIEditorWorkspaceViewportComposeFrame* viewportFrame = FindUIEditorWorkspaceViewportPresentationFrame(composeFrame, kScenePanelId); const UIEditorWorkspacePanelPresentationState* panelState = FindUIEditorWorkspacePanelPresentationState(composeState, kScenePanelId); if (viewportFrame == nullptr || panelState == nullptr) { if (m_legacyGizmo.IsActive()) { m_legacyGizmo.CancelDrag(sceneRuntime); } sceneRuntime.SetHoveredToolHandle(SceneToolHandle::None); ResetInteractionState(); return; } const auto& inputFrame = viewportFrame->viewportShellFrame.inputFrame; const auto& inputState = panelState->viewportShellState.inputBridgeState; const auto& slotLayout = viewportFrame->viewportShellFrame.slotLayout; const bool leftMouseDown = IsUIEditorViewportInputBridgePointerButtonDown( inputState, UIPointerButton::Left); const bool rightMouseDown = IsUIEditorViewportInputBridgePointerButtonDown( inputState, UIPointerButton::Right); const bool middleMouseDown = IsUIEditorViewportInputBridgePointerButtonDown( inputState, UIPointerButton::Middle); const UIPoint pointerScreen = inputState.hasPointerPosition ? inputState.lastScreenPointerPosition : inputFrame.screenPointerPosition; if (m_commandFocusService != nullptr && (inputFrame.focused || std::any_of( inputFrame.pointerButtonTransitions.begin(), inputFrame.pointerButtonTransitions.end(), [&](const auto& transition) { return transition.pressed && ContainsPoint(slotLayout.inputRect, transition.screenPosition); }))) { m_commandFocusService->ClaimFocus(EditorActionRoute::Scene); } if (inputFrame.focusLost) { m_navigationState = {}; m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; if (m_legacyGizmo.IsActive()) { m_legacyGizmo.CancelDrag(sceneRuntime); } } m_toolOverlay.BuildFrame( slotLayout.inputRect, sceneRuntime.GetToolMode(), kSceneViewportToolOverlayInvalidIndex, kSceneViewportToolOverlayInvalidIndex); const bool pointerOverToolOverlay = inputState.hasPointerPosition && m_toolOverlay.Contains(pointerScreen); const std::size_t hoveredToolOverlayIndex = inputState.hasPointerPosition ? m_toolOverlay.HitTest(pointerScreen) : kSceneViewportToolOverlayInvalidIndex; for (const auto& transition : inputFrame.pointerButtonTransitions) { if (transition.button != UIPointerButton::Left) { continue; } const std::size_t transitionHoveredToolOverlayIndex = m_toolOverlay.HitTest(transition.screenPosition); if (transition.pressed && transitionHoveredToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex && !m_legacyGizmo.IsActive()) { m_activeToolOverlayIndex = transitionHoveredToolOverlayIndex; if (m_activeToolOverlayIndex < m_toolOverlay.GetFrame().buttons.size()) { ApplySceneToolMode( m_toolOverlay.GetFrame().buttons[m_activeToolOverlayIndex].mode, sceneRuntime, m_legacyGizmo); } continue; } m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; } if (!leftMouseDown) { m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; } m_hoveredToolOverlayIndex = hoveredToolOverlayIndex; m_toolOverlay.BuildFrame( slotLayout.inputRect, sceneRuntime.GetToolMode(), m_hoveredToolOverlayIndex, m_activeToolOverlayIndex); const bool toolOverlayInteractionActive = m_activeToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex; const bool usingViewMoveTool = IsViewMoveTool(sceneRuntime.GetToolMode()); if (!m_legacyGizmo.IsActive()) { for (const auto& transition : inputFrame.pointerButtonTransitions) { if (!transition.pressed || !ContainsPoint(slotLayout.inputRect, transition.screenPosition) || m_toolOverlay.Contains(transition.screenPosition)) { continue; } if (usingViewMoveTool && transition.button == UIPointerButton::Left) { m_navigationState.panDragging = true; m_navigationState.lookDragging = false; m_navigationState.panDragButton = UIPointerButton::Left; } else if (transition.button == UIPointerButton::Right) { m_navigationState.lookDragging = true; m_navigationState.panDragging = false; m_navigationState.panDragButton = UIPointerButton::None; } else if (transition.button == UIPointerButton::Middle) { m_navigationState.panDragging = true; m_navigationState.lookDragging = false; m_navigationState.panDragButton = UIPointerButton::Middle; } } } if (m_navigationState.lookDragging && !rightMouseDown) { m_navigationState.lookDragging = false; } if (m_navigationState.panDragging && !IsPanDragButtonDown(inputState, m_navigationState.panDragButton)) { m_navigationState.panDragging = false; m_navigationState.panDragButton = UIPointerButton::None; } const bool viewportHoverEligible = inputState.hasPointerPosition && ContainsPoint(slotLayout.inputRect, pointerScreen) && !pointerOverToolOverlay && !toolOverlayInteractionActive && !m_navigationState.lookDragging && !m_navigationState.panDragging; if (!m_legacyGizmo.IsActive() && !toolOverlayInteractionActive && inputFrame.focused && !m_navigationState.lookDragging && !m_navigationState.panDragging) { SceneToolMode shortcutMode = SceneToolMode::View; if (TryResolveToolModeShortcut(inputFrame, shortcutMode)) { ApplySceneToolMode(shortcutMode, sceneRuntime, m_legacyGizmo); } } if (inputFrame.focused && !m_legacyGizmo.IsActive() && WasKeyPressed(inputFrame, KeyCode::F)) { sceneRuntime.FocusSceneSelection(); } m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); sceneRuntime.SetHoveredToolHandle(SceneToolHandle::None); if (m_legacyGizmo.IsActive()) { if (WasKeyPressed(inputFrame, KeyCode::Escape)) { m_legacyGizmo.CancelDrag(sceneRuntime); m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); return; } if (!leftMouseDown) { m_legacyGizmo.EndDrag(sceneRuntime); m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); return; } m_legacyGizmo.UpdateDrag(sceneRuntime); m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); return; } if (!usingViewMoveTool) { for (const auto& transition : inputFrame.pointerButtonTransitions) { if (!transition.pressed || transition.button != UIPointerButton::Left || !ContainsPoint(slotLayout.inputRect, transition.screenPosition) || m_toolOverlay.Contains(transition.screenPosition)) { continue; } m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, transition.screenPosition, true); if (m_legacyGizmo.IsHoveringHandle() && m_legacyGizmo.TryBeginDrag(sceneRuntime)) { m_navigationState = {}; m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); return; } const ViewportObjectIdPickResult pickResult = viewportHostService.PickSceneViewportObject( viewportFrame->viewportShellFrame.requestedViewportSize, transition.localPointerPosition); if (pickResult.status == ViewportObjectIdPickStatus::Success) { if (pickResult.entityId != 0u) { sceneRuntime.SetSelection(pickResult.entityId); } else { sceneRuntime.ClearSelection(); } } m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); } } if (toolOverlayInteractionActive) { return; } SceneViewportCameraInputState input = {}; input.deltaTime = ConsumeDeltaTimeSeconds(); input.viewportHeight = viewportFrame->viewportShellFrame.requestedViewportSize.height; if (m_navigationState.lookDragging) { input.lookDeltaX = inputFrame.pointerDelta.x; input.lookDeltaY = inputFrame.pointerDelta.y; input.flySpeedDelta = NormalizeWheelDelta(inputFrame.wheelDelta); input.fastMove = inputFrame.modifiers.shift; input.moveForward = (IsKeyDown(inputState, KeyCode::W) ? 1.0f : 0.0f) - (IsKeyDown(inputState, KeyCode::S) ? 1.0f : 0.0f); input.moveRight = (IsKeyDown(inputState, KeyCode::D) ? 1.0f : 0.0f) - (IsKeyDown(inputState, KeyCode::A) ? 1.0f : 0.0f); input.moveUp = (IsKeyDown(inputState, KeyCode::E) ? 1.0f : 0.0f) - (IsKeyDown(inputState, KeyCode::Q) ? 1.0f : 0.0f); } else if (m_navigationState.panDragging) { input.panDeltaX = inputFrame.pointerDelta.x; input.panDeltaY = inputFrame.pointerDelta.y; } else if (ContainsPoint(slotLayout.inputRect, pointerScreen) && !pointerOverToolOverlay) { input.zoomDelta = NormalizeWheelDelta(inputFrame.wheelDelta); } if (HasCameraInput(input)) { sceneRuntime.ApplySceneViewportCameraInput(input); m_legacyGizmo.Refresh( sceneRuntime, slotLayout.inputRect, pointerScreen, viewportHoverEligible); } } void SceneViewportController::Append(::XCEngine::UI::UIDrawList& drawList) const { AppendLegacySceneViewportGizmo(drawList, m_legacyGizmo.GetFrame()); AppendSceneViewportToolOverlay(drawList, m_toolOverlay.GetFrame()); } void SceneViewportController::ResetFrameState() { m_navigationState = {}; m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; m_lastUpdateTime = {}; m_hasLastUpdateTime = false; } float SceneViewportController::ConsumeDeltaTimeSeconds() { const auto now = std::chrono::steady_clock::now(); if (!m_hasLastUpdateTime) { m_lastUpdateTime = now; m_hasLastUpdateTime = true; return 0.0f; } const float deltaTime = std::chrono::duration(now - m_lastUpdateTime).count(); m_lastUpdateTime = now; return std::clamp(deltaTime, 0.0f, 0.1f); } } // namespace XCEngine::UI::Editor::App