diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 91daed5e..681961c4 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -53,6 +53,7 @@ set(XCUI_EDITOR_FIELD_SOURCES src/Fields/PropertyGridInteractionEdit.cpp src/Fields/PropertyGridInteractionHelpers.cpp src/Fields/PropertyGridInteractionPopup.cpp + src/Fields/PropertyGridInteractionVector.cpp src/Fields/PropertyGridRendering.cpp src/Fields/UIEditorTextField.cpp src/Fields/UIEditorTextFieldInteraction.cpp @@ -101,6 +102,7 @@ set(XCUI_EDITOR_PANEL_SOURCES set(XCUI_EDITOR_SHELL_SOURCES src/Shell/UIEditorShellAsset.cpp + src/Shell/UIEditorShellCapturePolicy.cpp src/Shell/UIEditorShellCompose.cpp src/Shell/UIEditorShellInteraction.cpp src/Shell/ShellInteractionRequest.cpp @@ -269,22 +271,38 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) app/Features/Hierarchy/HierarchyPanel.cpp app/Features/Hierarchy/HierarchyPanelInternal.cpp app/Features/Inspector/InspectorPanel.cpp + app/Features/Inspector/InspectorPresentationModel.cpp + app/Features/Inspector/InspectorSubject.cpp + app/Features/Inspector/Components/InspectorComponentEditorRegistry.cpp + app/Features/Inspector/Components/TransformInspectorComponentEditor.cpp app/Features/Project/ProjectPanel.cpp app/Features/Project/ProjectPanelInternal.cpp app/Features/Project/ProjectBrowserModel.cpp app/Features/Project/ProjectBrowserModelAssets.cpp app/Features/Project/ProjectBrowserModelFolders.cpp app/Features/Project/ProjectBrowserModelInternal.cpp + app/Features/Scene/LegacySceneViewportGizmo.cpp + app/Features/Scene/SceneEditCommandRoute.cpp + app/Features/Scene/SceneViewportToolOverlay.cpp + app/Features/Scene/SceneViewportController.cpp + ../editor/src/Viewport/SceneViewportMoveGizmo.cpp + ../editor/src/Viewport/SceneViewportRotateGizmo.cpp + ../editor/src/Viewport/SceneViewportScaleGizmo.cpp + ../editor/src/Viewport/SceneViewportPicker.cpp ) set(XCUI_EDITOR_APP_RENDERING_SOURCES app/Rendering/Assets/BuiltInIcons.cpp + app/Rendering/Viewport/Passes/SceneViewportGridPass.cpp + app/Rendering/Viewport/Passes/SceneViewportSelectionOutlinePass.cpp + app/Rendering/Viewport/SceneViewportRenderPassBundle.cpp app/Rendering/Viewport/ViewportHostService.cpp app/Rendering/Viewport/ViewportRenderTargets.cpp app/Rendering/Viewport/ViewportRenderTargetInternal.cpp ) set(XCUI_EDITOR_APP_SUPPORT_SOURCES + app/Project/EditorProjectRuntime.cpp app/Scene/EditorSceneRuntime.cpp app/Internal/EmbeddedPngLoader.cpp app/Scene/EditorSceneBridge.cpp @@ -319,7 +337,9 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP) target_include_directories(XCUIEditorAppLib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/app ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/editor/src ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/engine/src ) target_compile_definitions(XCUIEditorAppLib PRIVATE diff --git a/new_editor/app/Composition/EditorShellRuntime.cpp b/new_editor/app/Composition/EditorShellRuntime.cpp index 18b3bce6..a245a72c 100644 --- a/new_editor/app/Composition/EditorShellRuntime.cpp +++ b/new_editor/app/Composition/EditorShellRuntime.cpp @@ -12,7 +12,9 @@ namespace XCEngine::UI::Editor::App { void EditorShellRuntime::Initialize( const std::filesystem::path& repoRoot, Host::NativeRenderer& renderer) { + m_nativeRenderer = &renderer; m_builtInIcons.Initialize(renderer); + m_sceneViewportController.Initialize(repoRoot, renderer); m_hierarchyPanel.SetBuiltInIcons(&m_builtInIcons); m_projectPanel.SetBuiltInIcons(&m_builtInIcons); m_projectPanel.SetTextMeasurer(&renderer); @@ -37,8 +39,15 @@ void EditorShellRuntime::Shutdown() { m_shellInteractionState = {}; m_splitterDragCorrectionState = {}; m_traceEntries.clear(); + m_sceneEditCommandRoute = {}; + if (m_nativeRenderer != nullptr) { + m_sceneViewportController.Shutdown(*m_nativeRenderer); + m_builtInIcons.Shutdown(); + m_nativeRenderer = nullptr; + } else { + m_builtInIcons.Shutdown(); + } m_viewportHostService.Shutdown(); - m_builtInIcons.Shutdown(); } void EditorShellRuntime::ResetInteractionState() { @@ -46,6 +55,7 @@ void EditorShellRuntime::ResetInteractionState() { m_shellInteractionState = {}; m_splitterDragCorrectionState = {}; m_traceEntries.clear(); + m_sceneViewportController.ResetInteractionState(); m_hierarchyPanel.ResetInteractionState(); m_projectPanel.ResetInteractionState(); } diff --git a/new_editor/app/Composition/EditorShellRuntime.h b/new_editor/app/Composition/EditorShellRuntime.h index 727f3025..95f34f0b 100644 --- a/new_editor/app/Composition/EditorShellRuntime.h +++ b/new_editor/app/Composition/EditorShellRuntime.h @@ -2,10 +2,12 @@ #include "Composition/EditorShellVariant.h" #include "Features/Console/ConsolePanel.h" -#include "Rendering/Assets/BuiltInIcons.h" #include "Features/Hierarchy/HierarchyPanel.h" #include "Features/Inspector/InspectorPanel.h" #include "Features/Project/ProjectPanel.h" +#include "Features/Scene/SceneEditCommandRoute.h" +#include "Features/Scene/SceneViewportController.h" +#include "Rendering/Assets/BuiltInIcons.h" #include "Rendering/Viewport/ViewportHostService.h" #include "Composition/WorkspaceEventSync.h" @@ -85,10 +87,13 @@ public: private: ViewportHostService m_viewportHostService = {}; BuiltInIcons m_builtInIcons = {}; + Host::NativeRenderer* m_nativeRenderer = nullptr; ConsolePanel m_consolePanel = {}; HierarchyPanel m_hierarchyPanel = {}; InspectorPanel m_inspectorPanel = {}; ProjectPanel m_projectPanel = {}; + SceneEditCommandRoute m_sceneEditCommandRoute = {}; + SceneViewportController m_sceneViewportController = {}; UIEditorShellInteractionState m_shellInteractionState = {}; UIEditorShellInteractionFrame m_shellFrame = {}; std::vector m_traceEntries = {}; diff --git a/new_editor/app/Composition/EditorShellRuntimeRendering.cpp b/new_editor/app/Composition/EditorShellRuntimeRendering.cpp index ddc22931..5804578b 100644 --- a/new_editor/app/Composition/EditorShellRuntimeRendering.cpp +++ b/new_editor/app/Composition/EditorShellRuntimeRendering.cpp @@ -75,6 +75,7 @@ void EditorShellRuntime::Append(UIDrawList& drawList) const { m_hierarchyPanel.Append(drawList); m_inspectorPanel.Append(drawList); m_projectPanel.Append(drawList); + m_sceneViewportController.Append(drawList); AppendUIEditorShellComposeOverlay( drawList, m_shellFrame.shellFrame, diff --git a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp b/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp index d0ad3078..a3388e40 100644 --- a/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp +++ b/new_editor/app/Composition/EditorShellRuntimeUpdate.cpp @@ -123,7 +123,16 @@ void EditorShellRuntime::Update( m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout; m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime()); - context.BindEditCommandRoutes(&m_hierarchyPanel, &m_projectPanel); + m_sceneEditCommandRoute.BindSceneRuntime(&context.GetSceneRuntime()); + // Keep the previous render request available for readback-based picking during + // this update, then refresh it again after camera/navigation state changes. + m_viewportHostService.SetSceneViewportRenderRequest( + context.GetSceneRuntime().BuildSceneViewportRenderRequest()); + context.BindEditCommandRoutes( + &m_hierarchyPanel, + &m_projectPanel, + &m_sceneEditCommandRoute, + &m_inspectorPanel); context.SyncSessionFromWorkspace(workspaceController); UIEditorShellInteractionDefinition definition = context.BuildShellDefinition(workspaceController, captureText, shellVariant); @@ -168,11 +177,19 @@ void EditorShellRuntime::Update( FilterHostedContentInputEventsForShellOwnership( inputEvents, shellOwnsHostedContentPointerStream); + m_sceneViewportController.Update( + context.GetSceneRuntime(), + m_viewportHostService, + m_shellInteractionState.workspaceInteractionState.composeState, + m_shellFrame.workspaceInteractionFrame.composeFrame); + m_viewportHostService.SetSceneViewportRenderRequest( + context.GetSceneRuntime().BuildSceneViewportRenderRequest()); ApplyViewportFramesToShellFrame(m_shellFrame, m_viewportHostService); context.SyncSessionFromWorkspace(workspaceController); context.UpdateStatusFromShellResult(workspaceController, m_shellFrame.result); const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; + m_projectPanel.SetProjectRuntime(&context.GetProjectRuntime()); m_hierarchyPanel.Update( m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, hostedContentEvents, @@ -187,7 +204,10 @@ void EditorShellRuntime::Update( m_inspectorPanel.Update( context.GetSession(), context.GetSceneRuntime(), - m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); + m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame, + hostedContentEvents, + !m_shellFrame.result.workspaceInputSuppressed, + activePanelId == kInspectorPanelId); m_consolePanel.Update( context.GetSession(), m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame); diff --git a/new_editor/app/Features/Scene/LegacySceneViewportGizmo.cpp b/new_editor/app/Features/Scene/LegacySceneViewportGizmo.cpp new file mode 100644 index 00000000..ef00fb7d --- /dev/null +++ b/new_editor/app/Features/Scene/LegacySceneViewportGizmo.cpp @@ -0,0 +1,636 @@ +#include "Features/Scene/LegacySceneViewportGizmo.h" + +#include "Scene/EditorSceneRuntime.h" +#include "Scene/SceneToolState.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::Components::CameraComponent; +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::MeshFilterComponent; +using ::XCEngine::Components::TransformComponent; +using ::XCEngine::Editor::BuildSceneViewportTransformGizmoOverlayFrameData; +using ::XCEngine::Editor::IUndoManager; +using ::XCEngine::Editor::SceneViewportMoveGizmo; +using ::XCEngine::Editor::SceneViewportMoveGizmoContext; +using ::XCEngine::Editor::SceneViewportOverlayData; +using ::XCEngine::Editor::SceneViewportOverlayFrameData; +using ::XCEngine::Editor::SceneViewportRotateGizmo; +using ::XCEngine::Editor::SceneViewportRotateGizmoContext; +using ::XCEngine::Editor::SceneViewportScaleGizmo; +using ::XCEngine::Editor::SceneViewportScaleGizmoContext; +using ::XCEngine::Editor::SceneViewportTransformGizmoHandleBuildInputs; +using ::XCEngine::Editor::UndoStateSnapshot; +using ::XCEngine::Math::Color; +using ::XCEngine::Math::Quaternion; +using ::XCEngine::Math::Vector2; +using ::XCEngine::Math::Vector3; +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +enum class ActiveLegacyGizmoKind : std::uint8_t { + None = 0, + Move, + Rotate, + Scale +}; + +struct LegacySelectionState { + GameObject* primaryObject = nullptr; + std::vector selectedObjects = {}; + Vector3 pivotWorldPosition = Vector3::Zero(); + Quaternion primaryWorldRotation = Quaternion::Identity(); +}; + +UIColor ToUIColor(const Color& color) { + return UIColor(color.r, color.g, color.b, color.a); +} + +UIPoint ToScreenPoint(const Vector2& point, const UIRect& viewportRect) { + return UIPoint(viewportRect.x + point.x, viewportRect.y + point.y); +} + +Vector2 ToLocalPoint(const UIRect& viewportRect, const UIPoint& point) { + return Vector2(point.x - viewportRect.x, point.y - viewportRect.y); +} + +Quaternion ComputeStableWorldRotation(const GameObject* gameObject) { + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + return Quaternion::Identity(); + } + + return gameObject->GetTransform()->GetRotation().Normalized(); +} + +Vector3 ResolvePivotWorldPosition(const GameObject* gameObject) { + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + return Vector3::Zero(); + } + + return gameObject->GetTransform()->GetPosition(); +} + +Vector3 ResolveCenterWorldPosition(const GameObject* gameObject) { + if (gameObject == nullptr || gameObject->GetTransform() == nullptr) { + return Vector3::Zero(); + } + + if (const MeshFilterComponent* meshFilter = + gameObject->GetComponent(); + meshFilter != nullptr) { + if (::XCEngine::Resources::Mesh* mesh = meshFilter->GetMesh(); + mesh != nullptr && mesh->IsValid()) { + return gameObject->GetTransform()->TransformPoint(mesh->GetBounds().center); + } + } + + return gameObject->GetTransform()->GetPosition(); +} + +LegacySelectionState BuildSelectionState( + EditorSceneRuntime& sceneRuntime, + bool useCenterPivot) { + LegacySelectionState state = {}; + const auto selectedId = sceneRuntime.GetSelectedGameObjectId(); + if (!selectedId.has_value()) { + return state; + } + + GameObject* selectedObject = + sceneRuntime.GetActiveScene() != nullptr + ? sceneRuntime.GetActiveScene()->FindByID(selectedId.value()) + : nullptr; + if (selectedObject == nullptr || selectedObject->GetTransform() == nullptr) { + return state; + } + + state.primaryObject = selectedObject; + state.selectedObjects.push_back(selectedObject); + state.primaryWorldRotation = ComputeStableWorldRotation(selectedObject); + state.pivotWorldPosition = useCenterPivot + ? ResolveCenterWorldPosition(selectedObject) + : ResolvePivotWorldPosition(selectedObject); + return state; +} + +SceneViewportOverlayData BuildOverlayData(const EditorSceneRuntime& sceneRuntime) { + SceneViewportOverlayData overlay = {}; + const CameraComponent* camera = sceneRuntime.GetSceneViewCamera(); + if (camera == nullptr || camera->GetGameObject() == nullptr) { + return overlay; + } + + const TransformComponent* transform = camera->GetGameObject()->GetTransform(); + if (transform == nullptr) { + return overlay; + } + + overlay.valid = true; + overlay.cameraPosition = transform->GetPosition(); + overlay.cameraForward = transform->GetForward(); + overlay.cameraRight = transform->GetRight(); + overlay.cameraUp = transform->GetUp(); + overlay.verticalFovDegrees = camera->GetFieldOfView(); + overlay.nearClipPlane = camera->GetNearClipPlane(); + overlay.farClipPlane = camera->GetFarClipPlane(); + overlay.orbitDistance = sceneRuntime.GetSceneViewOrbitDistance(); + return overlay; +} + +SceneViewportMoveGizmoContext BuildMoveContext( + const LegacySelectionState& selection, + const SceneViewportOverlayData& overlay, + const UIRect& viewportRect, + const Vector2& localPointer, + bool localSpace) { + SceneViewportMoveGizmoContext context = {}; + context.overlay = overlay; + context.viewportSize = Vector2(viewportRect.width, viewportRect.height); + context.mousePosition = localPointer; + context.selectedObject = selection.primaryObject; + context.selectedObjects = selection.selectedObjects; + context.pivotWorldPosition = selection.pivotWorldPosition; + context.axisOrientation = localSpace + ? selection.primaryWorldRotation + : Quaternion::Identity(); + return context; +} + +SceneViewportRotateGizmoContext BuildRotateContext( + const LegacySelectionState& selection, + const SceneViewportOverlayData& overlay, + const UIRect& viewportRect, + const Vector2& localPointer, + bool localSpace, + bool useCenterPivot) { + SceneViewportRotateGizmoContext context = {}; + context.overlay = overlay; + context.viewportSize = Vector2(viewportRect.width, viewportRect.height); + context.mousePosition = localPointer; + context.selectedObject = selection.primaryObject; + context.selectedObjects = selection.selectedObjects; + context.pivotWorldPosition = selection.pivotWorldPosition; + context.axisOrientation = localSpace + ? selection.primaryWorldRotation + : Quaternion::Identity(); + context.localSpace = localSpace; + context.rotateAroundSharedPivot = useCenterPivot; + return context; +} + +SceneViewportScaleGizmoContext BuildScaleContext( + const LegacySelectionState& selection, + const SceneViewportOverlayData& overlay, + const UIRect& viewportRect, + const Vector2& localPointer, + bool localSpace) { + SceneViewportScaleGizmoContext context = {}; + context.overlay = overlay; + context.viewportSize = Vector2(viewportRect.width, viewportRect.height); + context.mousePosition = localPointer; + context.selectedObject = selection.primaryObject; + context.pivotWorldPosition = selection.pivotWorldPosition; + context.axisOrientation = localSpace + ? selection.primaryWorldRotation + : Quaternion::Identity(); + return context; +} + +ActiveLegacyGizmoKind ResolveActiveGizmoKind( + const SceneViewportMoveGizmo& moveGizmo, + const SceneViewportRotateGizmo& rotateGizmo, + const SceneViewportScaleGizmo& scaleGizmo) { + if (moveGizmo.IsActive()) { + return ActiveLegacyGizmoKind::Move; + } + if (rotateGizmo.IsActive()) { + return ActiveLegacyGizmoKind::Rotate; + } + if (scaleGizmo.IsActive()) { + return ActiveLegacyGizmoKind::Scale; + } + + return ActiveLegacyGizmoKind::None; +} + +class SceneGizmoUndoBridge final : public IUndoManager { +public: + void Bind(EditorSceneRuntime& sceneRuntime) { + m_sceneRuntime = &sceneRuntime; + } + + void ClearHistory() override { + m_pendingLabel.clear(); + m_beforeSnapshot = {}; + } + + bool CanUndo() const override { + return m_sceneRuntime != nullptr && m_sceneRuntime->CanUndoTransformEdit(); + } + + bool CanRedo() const override { + return m_sceneRuntime != nullptr && m_sceneRuntime->CanRedoTransformEdit(); + } + + const std::string& GetUndoLabel() const override { + return m_emptyLabel; + } + + const std::string& GetRedoLabel() const override { + return m_emptyLabel; + } + + void Undo() override { + if (m_sceneRuntime != nullptr) { + m_sceneRuntime->UndoTransformEdit(); + } + } + + void Redo() override { + if (m_sceneRuntime != nullptr) { + m_sceneRuntime->RedoTransformEdit(); + } + } + + UndoStateSnapshot CaptureCurrentState() const override { + return {}; + } + + void PushCommand( + const std::string&, + UndoStateSnapshot, + UndoStateSnapshot) override { + } + + void BeginInteractiveChange(const std::string& label) override { + if (m_sceneRuntime == nullptr || HasPendingInteractiveChange()) { + return; + } + + SceneTransformSnapshot snapshot = {}; + if (!m_sceneRuntime->CaptureSelectedTransformSnapshot(snapshot)) { + return; + } + + m_pendingLabel = label; + m_beforeSnapshot = snapshot; + } + + bool HasPendingInteractiveChange() const override { + return m_beforeSnapshot.IsValid(); + } + + void FinalizeInteractiveChange() override { + if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) { + return; + } + + SceneTransformSnapshot afterSnapshot = {}; + m_sceneRuntime->CaptureSelectedTransformSnapshot(afterSnapshot); + m_sceneRuntime->RecordTransformEdit(m_beforeSnapshot, afterSnapshot); + m_pendingLabel.clear(); + m_beforeSnapshot = {}; + } + + void CancelInteractiveChange() override { + if (m_sceneRuntime == nullptr || !HasPendingInteractiveChange()) { + return; + } + + m_sceneRuntime->ApplyTransformSnapshot(m_beforeSnapshot); + m_pendingLabel.clear(); + m_beforeSnapshot = {}; + } + +private: + EditorSceneRuntime* m_sceneRuntime = nullptr; + std::string m_pendingLabel = {}; + std::string m_emptyLabel = {}; + SceneTransformSnapshot m_beforeSnapshot = {}; +}; + +} // namespace + +struct LegacySceneViewportGizmo::State { + SceneGizmoUndoBridge undoBridge = {}; + SceneViewportMoveGizmo moveGizmo = {}; + SceneViewportRotateGizmo rotateGizmo = {}; + SceneViewportScaleGizmo scaleGizmo = {}; + SceneViewportMoveGizmoContext moveContext = {}; + SceneViewportRotateGizmoContext rotateContext = {}; + SceneViewportScaleGizmoContext scaleContext = {}; + LegacySceneViewportGizmoFrame frame = {}; +}; + +LegacySceneViewportGizmo::LegacySceneViewportGizmo() + : m_state(std::make_unique()) { +} + +LegacySceneViewportGizmo::~LegacySceneViewportGizmo() = default; + +LegacySceneViewportGizmo::LegacySceneViewportGizmo(LegacySceneViewportGizmo&&) noexcept = + default; + +LegacySceneViewportGizmo& LegacySceneViewportGizmo::operator=( + LegacySceneViewportGizmo&&) noexcept = default; + +void LegacySceneViewportGizmo::Refresh( + EditorSceneRuntime& sceneRuntime, + const UIRect& viewportRect, + const UIPoint& pointerScreen, + bool hoverEnabled) { + State& state = *m_state; + state.undoBridge.Bind(sceneRuntime); + state.frame = {}; + state.frame.clipRect = viewportRect; + + if (viewportRect.width <= 1.0f || viewportRect.height <= 1.0f) { + return; + } + + const SceneViewportOverlayData overlay = BuildOverlayData(sceneRuntime); + if (!overlay.valid) { + CancelDrag(sceneRuntime); + return; + } + + const bool useCenterPivot = + sceneRuntime.GetToolPivotMode() == SceneToolPivotMode::Center; + const bool localSpace = + sceneRuntime.GetToolSpaceMode() == SceneToolSpaceMode::Local; + const LegacySelectionState selection = + BuildSelectionState(sceneRuntime, useCenterPivot); + if (selection.primaryObject == nullptr) { + CancelDrag(sceneRuntime); + return; + } + + const SceneToolMode toolMode = sceneRuntime.GetToolMode(); + if (toolMode == SceneToolMode::View) { + CancelDrag(sceneRuntime); + return; + } + + const ActiveLegacyGizmoKind activeKind = ResolveActiveGizmoKind( + state.moveGizmo, + state.rotateGizmo, + state.scaleGizmo); + Vector2 localPointer = ToLocalPoint(viewportRect, pointerScreen); + if (!hoverEnabled && activeKind == ActiveLegacyGizmoKind::None) { + localPointer = Vector2(-1.0f, -1.0f); + } + + SceneViewportTransformGizmoHandleBuildInputs inputs = {}; + switch (toolMode) { + case SceneToolMode::Translate: { + if (activeKind == ActiveLegacyGizmoKind::Rotate) { + state.rotateGizmo.CancelDrag(&state.undoBridge); + } else if (activeKind == ActiveLegacyGizmoKind::Scale) { + state.scaleGizmo.CancelDrag(&state.undoBridge); + } + + state.moveContext = BuildMoveContext( + selection, + overlay, + viewportRect, + localPointer, + localSpace); + if (state.moveGizmo.IsActive() && + state.moveContext.selectedObject != nullptr && + state.moveContext.selectedObject->GetID() != + state.moveGizmo.GetActiveEntityId()) { + state.moveGizmo.CancelDrag(&state.undoBridge); + } + state.moveGizmo.Update(state.moveContext); + inputs.moveGizmo = &state.moveGizmo.GetDrawData(); + inputs.moveEntityId = selection.primaryObject->GetID(); + break; + } + + case SceneToolMode::Rotate: { + if (activeKind == ActiveLegacyGizmoKind::Move) { + state.moveGizmo.CancelDrag(&state.undoBridge); + } else if (activeKind == ActiveLegacyGizmoKind::Scale) { + state.scaleGizmo.CancelDrag(&state.undoBridge); + } + + state.rotateContext = BuildRotateContext( + selection, + overlay, + viewportRect, + localPointer, + localSpace, + useCenterPivot); + if (state.rotateGizmo.IsActive() && + state.rotateContext.selectedObject != nullptr && + state.rotateContext.selectedObject->GetID() != + state.rotateGizmo.GetActiveEntityId()) { + state.rotateGizmo.CancelDrag(&state.undoBridge); + } + state.rotateGizmo.Update(state.rotateContext); + inputs.rotateGizmo = &state.rotateGizmo.GetDrawData(); + inputs.rotateEntityId = selection.primaryObject->GetID(); + break; + } + + case SceneToolMode::Scale: { + if (activeKind == ActiveLegacyGizmoKind::Move) { + state.moveGizmo.CancelDrag(&state.undoBridge); + } else if (activeKind == ActiveLegacyGizmoKind::Rotate) { + state.rotateGizmo.CancelDrag(&state.undoBridge); + } + + state.scaleContext = BuildScaleContext( + selection, + overlay, + viewportRect, + localPointer, + localSpace); + if (state.scaleGizmo.IsActive() && + state.scaleContext.selectedObject != nullptr && + state.scaleContext.selectedObject->GetID() != + state.scaleGizmo.GetActiveEntityId()) { + state.scaleGizmo.CancelDrag(&state.undoBridge); + } + state.scaleGizmo.Update(state.scaleContext); + inputs.scaleGizmo = &state.scaleGizmo.GetDrawData(); + inputs.scaleEntityId = selection.primaryObject->GetID(); + break; + } + + case SceneToolMode::View: + default: + break; + } + + const SceneViewportOverlayFrameData overlayFrame = + BuildSceneViewportTransformGizmoOverlayFrameData(overlay, inputs); + if (overlayFrame.screenTriangles.empty()) { + return; + } + + state.frame.visible = true; + state.frame.triangles.reserve(overlayFrame.screenTriangles.size()); + for (const auto& triangle : overlayFrame.screenTriangles) { + LegacySceneViewportTriangle triangleFrame = {}; + triangleFrame.a = + ToScreenPoint(triangle.vertices[0].screenPosition, viewportRect); + triangleFrame.b = + ToScreenPoint(triangle.vertices[1].screenPosition, viewportRect); + triangleFrame.c = + ToScreenPoint(triangle.vertices[2].screenPosition, viewportRect); + triangleFrame.color = ToUIColor(triangle.vertices[0].color); + state.frame.triangles.push_back(std::move(triangleFrame)); + } +} + +bool LegacySceneViewportGizmo::TryBeginDrag(EditorSceneRuntime& sceneRuntime) { + State& state = *m_state; + state.undoBridge.Bind(sceneRuntime); + + switch (sceneRuntime.GetToolMode()) { + case SceneToolMode::Translate: + return state.moveGizmo.TryBeginDrag(state.moveContext, state.undoBridge); + case SceneToolMode::Rotate: + return state.rotateGizmo.TryBeginDrag(state.rotateContext, state.undoBridge); + case SceneToolMode::Scale: + return state.scaleGizmo.TryBeginDrag(state.scaleContext, state.undoBridge); + case SceneToolMode::View: + default: + return false; + } +} + +bool LegacySceneViewportGizmo::UpdateDrag(EditorSceneRuntime& sceneRuntime) { + State& state = *m_state; + state.undoBridge.Bind(sceneRuntime); + + switch (ResolveActiveGizmoKind( + state.moveGizmo, + state.rotateGizmo, + state.scaleGizmo)) { + case ActiveLegacyGizmoKind::Move: + state.moveGizmo.UpdateDrag(state.moveContext); + return true; + case ActiveLegacyGizmoKind::Rotate: + state.rotateGizmo.UpdateDrag(state.rotateContext); + return true; + case ActiveLegacyGizmoKind::Scale: + state.scaleGizmo.UpdateDrag(state.scaleContext); + return true; + case ActiveLegacyGizmoKind::None: + default: + return false; + } +} + +bool LegacySceneViewportGizmo::EndDrag(EditorSceneRuntime& sceneRuntime) { + State& state = *m_state; + state.undoBridge.Bind(sceneRuntime); + + switch (ResolveActiveGizmoKind( + state.moveGizmo, + state.rotateGizmo, + state.scaleGizmo)) { + case ActiveLegacyGizmoKind::Move: + state.moveGizmo.EndDrag(state.undoBridge); + return true; + case ActiveLegacyGizmoKind::Rotate: + state.rotateGizmo.EndDrag(state.undoBridge); + return true; + case ActiveLegacyGizmoKind::Scale: + state.scaleGizmo.EndDrag(state.undoBridge); + return true; + case ActiveLegacyGizmoKind::None: + default: + return false; + } +} + +void LegacySceneViewportGizmo::CancelDrag(EditorSceneRuntime& sceneRuntime) { + State& state = *m_state; + state.undoBridge.Bind(sceneRuntime); + + if (state.moveGizmo.IsActive()) { + state.moveGizmo.CancelDrag(&state.undoBridge); + } + if (state.rotateGizmo.IsActive()) { + state.rotateGizmo.CancelDrag(&state.undoBridge); + } + if (state.scaleGizmo.IsActive()) { + state.scaleGizmo.CancelDrag(&state.undoBridge); + } +} + +void LegacySceneViewportGizmo::ResetVisualState() { + if (m_state == nullptr) { + return; + } + + m_state->frame = {}; +} + +bool LegacySceneViewportGizmo::IsActive() const { + if (m_state == nullptr) { + return false; + } + + return ResolveActiveGizmoKind( + m_state->moveGizmo, + m_state->rotateGizmo, + m_state->scaleGizmo) != ActiveLegacyGizmoKind::None; +} + +bool LegacySceneViewportGizmo::IsHoveringHandle() const { + if (m_state == nullptr) { + return false; + } + + return m_state->moveGizmo.IsHoveringHandle() || + m_state->rotateGizmo.IsHoveringHandle() || + m_state->scaleGizmo.IsHoveringHandle(); +} + +const LegacySceneViewportGizmoFrame& LegacySceneViewportGizmo::GetFrame() const { + return m_state->frame; +} + +void AppendLegacySceneViewportGizmo( + ::XCEngine::UI::UIDrawList& drawList, + const LegacySceneViewportGizmoFrame& frame) { + if (!frame.visible || frame.triangles.empty()) { + return; + } + + drawList.PushClipRect(frame.clipRect); + for (const LegacySceneViewportTriangle& triangle : frame.triangles) { + drawList.AddFilledTriangle( + triangle.a, + triangle.b, + triangle.c, + triangle.color); + } + drawList.PopClipRect(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/LegacySceneViewportGizmo.h b/new_editor/app/Features/Scene/LegacySceneViewportGizmo.h new file mode 100644 index 00000000..e63040f3 --- /dev/null +++ b/new_editor/app/Features/Scene/LegacySceneViewportGizmo.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorSceneRuntime; + +struct LegacySceneViewportTriangle { + ::XCEngine::UI::UIPoint a = {}; + ::XCEngine::UI::UIPoint b = {}; + ::XCEngine::UI::UIPoint c = {}; + ::XCEngine::UI::UIColor color = {}; +}; + +struct LegacySceneViewportGizmoFrame { + bool visible = false; + ::XCEngine::UI::UIRect clipRect = {}; + std::vector triangles = {}; +}; + +class LegacySceneViewportGizmo { +public: + LegacySceneViewportGizmo(); + ~LegacySceneViewportGizmo(); + + LegacySceneViewportGizmo(const LegacySceneViewportGizmo&) = delete; + LegacySceneViewportGizmo& operator=(const LegacySceneViewportGizmo&) = delete; + LegacySceneViewportGizmo(LegacySceneViewportGizmo&&) noexcept; + LegacySceneViewportGizmo& operator=(LegacySceneViewportGizmo&&) noexcept; + + void Refresh( + EditorSceneRuntime& sceneRuntime, + const ::XCEngine::UI::UIRect& viewportRect, + const ::XCEngine::UI::UIPoint& pointerScreen, + bool hoverEnabled); + bool TryBeginDrag(EditorSceneRuntime& sceneRuntime); + bool UpdateDrag(EditorSceneRuntime& sceneRuntime); + bool EndDrag(EditorSceneRuntime& sceneRuntime); + void CancelDrag(EditorSceneRuntime& sceneRuntime); + void ResetVisualState(); + + bool IsActive() const; + bool IsHoveringHandle() const; + const LegacySceneViewportGizmoFrame& GetFrame() const; + +private: + struct State; + + std::unique_ptr m_state = {}; +}; + +void AppendLegacySceneViewportGizmo( + ::XCEngine::UI::UIDrawList& drawList, + const LegacySceneViewportGizmoFrame& frame); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneEditCommandRoute.cpp b/new_editor/app/Features/Scene/SceneEditCommandRoute.cpp new file mode 100644 index 00000000..a3e4971c --- /dev/null +++ b/new_editor/app/Features/Scene/SceneEditCommandRoute.cpp @@ -0,0 +1,148 @@ +#include "Features/Scene/SceneEditCommandRoute.h" + +#include "Scene/EditorSceneRuntime.h" + +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +UIEditorHostCommandEvaluationResult BuildEvaluationResult( + bool executable, + std::string message) { + UIEditorHostCommandEvaluationResult result = {}; + result.executable = executable; + result.message = std::move(message); + return result; +} + +UIEditorHostCommandDispatchResult BuildDispatchResult( + bool commandExecuted, + std::string message) { + UIEditorHostCommandDispatchResult result = {}; + result.commandExecuted = commandExecuted; + result.message = std::move(message); + return result; +} + +} // namespace + +void SceneEditCommandRoute::BindSceneRuntime(EditorSceneRuntime* sceneRuntime) { + m_sceneRuntime = sceneRuntime; +} + +UIEditorHostCommandEvaluationResult SceneEditCommandRoute::EvaluateEditCommand( + std::string_view commandId) const { + if (m_sceneRuntime == nullptr) { + return BuildEvaluationResult(false, "Scene runtime is unavailable."); + } + + if (commandId == "edit.undo") { + return m_sceneRuntime->CanUndoTransformEdit() + ? BuildEvaluationResult(true, "Undo the last scene transform edit.") + : BuildEvaluationResult(false, "Scene transform history is empty."); + } + + if (commandId == "edit.redo") { + return m_sceneRuntime->CanRedoTransformEdit() + ? BuildEvaluationResult(true, "Redo the last scene transform edit.") + : BuildEvaluationResult(false, "Scene redo history is empty."); + } + + if (!m_sceneRuntime->HasSceneSelection()) { + return BuildEvaluationResult(false, "Select a scene object first."); + } + + const std::string selectedName = + m_sceneRuntime->GetSelectedDisplayName().empty() + ? std::string("GameObject") + : m_sceneRuntime->GetSelectedDisplayName(); + + if (commandId == "edit.delete") { + return BuildEvaluationResult( + true, + "Delete scene object '" + selectedName + "'."); + } + + if (commandId == "edit.duplicate") { + return BuildEvaluationResult( + true, + "Duplicate scene object '" + selectedName + "'."); + } + + if (commandId == "edit.rename") { + return BuildEvaluationResult( + false, + "Scene viewport does not expose inline rename yet."); + } + + if (commandId == "edit.cut" || + commandId == "edit.copy" || + commandId == "edit.paste") { + return BuildEvaluationResult( + false, + "Scene clipboard transfer has no bound owner in the current shell."); + } + + return BuildEvaluationResult(false, "Scene does not expose this edit command."); +} + +UIEditorHostCommandDispatchResult SceneEditCommandRoute::DispatchEditCommand( + std::string_view commandId) { + const UIEditorHostCommandEvaluationResult evaluation = + EvaluateEditCommand(commandId); + if (!evaluation.executable) { + return BuildDispatchResult(false, evaluation.message); + } + + if (m_sceneRuntime == nullptr) { + return BuildDispatchResult(false, "Scene runtime is unavailable."); + } + + if (commandId == "edit.undo") { + return m_sceneRuntime->UndoTransformEdit() + ? BuildDispatchResult(true, "Undid the last scene transform edit.") + : BuildDispatchResult(false, "Scene transform history is empty."); + } + + if (commandId == "edit.redo") { + return m_sceneRuntime->RedoTransformEdit() + ? BuildDispatchResult(true, "Redid the last scene transform edit.") + : BuildDispatchResult(false, "Scene redo history is empty."); + } + + const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId(); + const std::string selectedName = + m_sceneRuntime->GetSelectedDisplayName().empty() + ? std::string("GameObject") + : m_sceneRuntime->GetSelectedDisplayName(); + + if (selectedItemId.empty()) { + return BuildDispatchResult(false, "Select a scene object first."); + } + + if (commandId == "edit.delete") { + return m_sceneRuntime->DeleteGameObject(selectedItemId) + ? BuildDispatchResult( + true, + "Deleted scene object '" + selectedName + "'.") + : BuildDispatchResult( + false, + "Failed to delete the selected scene object."); + } + + if (commandId == "edit.duplicate") { + return !m_sceneRuntime->DuplicateGameObject(selectedItemId).empty() + ? BuildDispatchResult( + true, + "Duplicated scene object '" + selectedName + "'.") + : BuildDispatchResult( + false, + "Failed to duplicate the selected scene object."); + } + + return BuildDispatchResult(false, "Scene does not expose this edit command."); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneEditCommandRoute.h b/new_editor/app/Features/Scene/SceneEditCommandRoute.h new file mode 100644 index 00000000..37239faa --- /dev/null +++ b/new_editor/app/Features/Scene/SceneEditCommandRoute.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace XCEngine::UI::Editor::App { + +class EditorSceneRuntime; + +class SceneEditCommandRoute final : public EditorEditCommandRoute { +public: + void BindSceneRuntime(EditorSceneRuntime* sceneRuntime); + + UIEditorHostCommandEvaluationResult EvaluateEditCommand( + std::string_view commandId) const override; + UIEditorHostCommandDispatchResult DispatchEditCommand( + std::string_view commandId) override; + +private: + EditorSceneRuntime* m_sceneRuntime = nullptr; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportController.cpp b/new_editor/app/Features/Scene/SceneViewportController.cpp new file mode 100644 index 00000000..507a7a68 --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportController.cpp @@ -0,0 +1,487 @@ +#include "Features/Scene/SceneViewportController.h" + +#include "Rendering/Viewport/ViewportHostService.h" +#include "Scene/EditorSceneRuntime.h" + +#include + +#include +#include +#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; +using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot; +using ::XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTarget; +using ::XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind; + +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 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; +} + +bool ApplySceneToolbarCommand( + std::string_view itemId, + EditorSceneRuntime& sceneRuntime) { + if (itemId == "scene.pivot.pivot") { + sceneRuntime.SetToolPivotMode(SceneToolPivotMode::Pivot); + return true; + } + if (itemId == "scene.pivot.center") { + sceneRuntime.SetToolPivotMode(SceneToolPivotMode::Center); + return true; + } + if (itemId == "scene.space.world") { + sceneRuntime.SetToolSpaceMode(SceneToolSpaceMode::World); + return true; + } + if (itemId == "scene.space.local") { + sceneRuntime.SetToolSpaceMode(SceneToolSpaceMode::Local); + 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::NativeRenderer& renderer) { + m_toolOverlay.Initialize(repoRoot, renderer); + ResetInteractionState(); +} + +void SceneViewportController::Shutdown(Host::NativeRenderer& renderer) { + m_toolOverlay.Shutdown(renderer); + ResetInteractionState(); +} + +void SceneViewportController::ResetInteractionState() { + ResetFrameState(); + m_toolOverlay.ResetFrame(); + m_legacyGizmo.ResetVisualState(); +} + +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.ClearToolbarInteraction(); + 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 auto& toolItems = viewportFrame->viewportShellModel.spec.toolItems; + 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 (inputFrame.focusLost) { + m_navigationState = {}; + m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + if (m_legacyGizmo.IsActive()) { + m_legacyGizmo.CancelDrag(sceneRuntime); + } + sceneRuntime.ClearToolbarInteraction(); + } + + std::size_t hoveredToolbarIndex = kSceneToolInvalidToolbarIndex; + if (!toolItems.empty() && inputState.hasPointerPosition) { + const UIEditorViewportSlotHitTarget toolbarHit = + HitTestUIEditorViewportSlot(slotLayout, pointerScreen); + if (toolbarHit.kind == UIEditorViewportSlotHitTargetKind::ToolItem && + toolbarHit.index < toolItems.size() && + toolItems[toolbarHit.index].enabled) { + hoveredToolbarIndex = toolbarHit.index; + } + } + + if (m_legacyGizmo.IsActive()) { + sceneRuntime.ClearToolbarInteraction(); + } else { + sceneRuntime.SetToolbarHoveredIndex(hoveredToolbarIndex); + if (inputFrame.changedPointerButton == UIPointerButton::Left && + leftMouseDown && + hoveredToolbarIndex != kSceneToolInvalidToolbarIndex) { + sceneRuntime.SetToolbarActiveIndex(hoveredToolbarIndex); + sceneRuntime.SetToolInteractionLock(SceneToolInteractionLock::Toolbar); + } else if (inputFrame.changedPointerButton == UIPointerButton::Left && + !leftMouseDown) { + const std::size_t activeToolbarIndex = + sceneRuntime.GetToolState().toolbarActiveIndex; + const bool applyCommand = + activeToolbarIndex != kSceneToolInvalidToolbarIndex && + activeToolbarIndex == hoveredToolbarIndex && + activeToolbarIndex < toolItems.size() && + toolItems[activeToolbarIndex].enabled; + sceneRuntime.ClearToolbarInteraction(); + if (applyCommand) { + ApplySceneToolbarCommand( + toolItems[activeToolbarIndex].itemId, + sceneRuntime); + } + } else if (!leftMouseDown && + sceneRuntime.GetToolState().interactionLock == + SceneToolInteractionLock::Toolbar) { + sceneRuntime.ClearToolbarInteraction(); + } + } + + const bool toolbarInteractionActive = + sceneRuntime.GetToolState().interactionLock == + SceneToolInteractionLock::Toolbar; + + 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; + + if (inputFrame.changedPointerButton == UIPointerButton::Left && + leftMouseDown && + hoveredToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex && + !toolbarInteractionActive && + !m_legacyGizmo.IsActive()) { + m_activeToolOverlayIndex = hoveredToolOverlayIndex; + } else if (inputFrame.changedPointerButton == UIPointerButton::Left && + !leftMouseDown) { + if (m_activeToolOverlayIndex != kSceneViewportToolOverlayInvalidIndex && + m_activeToolOverlayIndex == hoveredToolOverlayIndex && + m_activeToolOverlayIndex < m_toolOverlay.GetFrame().buttons.size()) { + ApplySceneToolMode( + m_toolOverlay.GetFrame().buttons[m_activeToolOverlayIndex].mode, + sceneRuntime, + m_legacyGizmo); + } + m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + } else 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; + + if (!toolbarInteractionActive && + !toolOverlayInteractionActive && + !m_legacyGizmo.IsActive()) { + if (inputFrame.pointerPressedInside && + inputFrame.changedPointerButton == UIPointerButton::Right && + !pointerOverToolOverlay) { + m_navigationState.lookDragging = true; + m_navigationState.panDragging = false; + } + if (inputFrame.pointerPressedInside && + inputFrame.changedPointerButton == UIPointerButton::Middle && + !pointerOverToolOverlay) { + m_navigationState.panDragging = true; + m_navigationState.lookDragging = false; + } + } + + if (m_navigationState.lookDragging && !rightMouseDown) { + m_navigationState.lookDragging = false; + } + if (m_navigationState.panDragging && !middleMouseDown) { + m_navigationState.panDragging = false; + } + + const bool viewportHoverEligible = + inputState.hasPointerPosition && + ContainsPoint(slotLayout.inputRect, pointerScreen) && + !pointerOverToolOverlay && + !toolbarInteractionActive && + !toolOverlayInteractionActive && + !m_navigationState.lookDragging && + !m_navigationState.panDragging; + + if (!m_legacyGizmo.IsActive() && + !toolbarInteractionActive && + !toolOverlayInteractionActive && + inputFrame.focused) { + 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 (inputFrame.changedPointerButton == UIPointerButton::Left && + !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; + } + + const bool shouldStartTransformDrag = + inputFrame.pointerPressedInside && + inputFrame.changedPointerButton == UIPointerButton::Left && + viewportHoverEligible && + m_legacyGizmo.IsHoveringHandle(); + if (shouldStartTransformDrag && + m_legacyGizmo.TryBeginDrag(sceneRuntime)) { + m_navigationState = {}; + m_legacyGizmo.Refresh( + sceneRuntime, + slotLayout.inputRect, + pointerScreen, + viewportHoverEligible); + return; + } + + const bool shouldPickSelection = + inputFrame.pointerPressedInside && + inputFrame.changedPointerButton == UIPointerButton::Left && + viewportHoverEligible && + !m_legacyGizmo.IsHoveringHandle(); + if (shouldPickSelection) { + const ViewportObjectIdPickResult pickResult = + viewportHostService.PickSceneViewportObject( + viewportFrame->viewportShellFrame.requestedViewportSize, + inputFrame.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 (toolbarInteractionActive || 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 (inputFrame.hovered && !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 diff --git a/new_editor/app/Features/Scene/SceneViewportController.h b/new_editor/app/Features/Scene/SceneViewportController.h new file mode 100644 index 00000000..cbb1c99a --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportController.h @@ -0,0 +1,61 @@ +#pragma once + +#include "Features/Scene/LegacySceneViewportGizmo.h" +#include "Features/Scene/SceneViewportToolOverlay.h" + +#include + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +class EditorSceneRuntime; +class ViewportHostService; + +} // namespace XCEngine::UI::Editor::App + +namespace XCEngine::UI::Editor::Host { + +class NativeRenderer; + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::App { + +class SceneViewportController { +public: + void Initialize( + const std::filesystem::path& repoRoot, + Host::NativeRenderer& renderer); + void Shutdown(Host::NativeRenderer& renderer); + void ResetInteractionState(); + + void Update( + EditorSceneRuntime& sceneRuntime, + ViewportHostService& viewportHostService, + const UIEditorWorkspaceComposeState& composeState, + const UIEditorWorkspaceComposeFrame& composeFrame); + void Append(::XCEngine::UI::UIDrawList& drawList) const; + +private: + struct NavigationState { + bool lookDragging = false; + bool panDragging = false; + }; + + void ResetFrameState(); + float ConsumeDeltaTimeSeconds(); + + NavigationState m_navigationState = {}; + LegacySceneViewportGizmo m_legacyGizmo = {}; + SceneViewportToolOverlay m_toolOverlay = {}; + std::size_t m_hoveredToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + std::size_t m_activeToolOverlayIndex = kSceneViewportToolOverlayInvalidIndex; + std::chrono::steady_clock::time_point m_lastUpdateTime = {}; + bool m_hasLastUpdateTime = false; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp b/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp new file mode 100644 index 00000000..bd80059f --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportToolOverlay.cpp @@ -0,0 +1,221 @@ +#include "Features/Scene/SceneViewportToolOverlay.h" + +#include + +#include +#include + +namespace XCEngine::UI::Editor::App { + +namespace { + +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +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 float kPanelCornerRounding = 7.0f; +constexpr float kButtonCornerRounding = 5.0f; +constexpr float kFallbackFontSize = 11.0f; + +struct ToolTexturePath { + SceneToolMode mode = SceneToolMode::View; + const char* label = ""; + const char* inactiveFile = ""; + const char* activeFile = ""; +}; + +constexpr std::array kToolTexturePaths = {{ + { SceneToolMode::View, "View", "view_move_tool.png", "view_move_tool_on.png" }, + { SceneToolMode::Translate, "Move", "move_tool.png", "move_tool_on.png" }, + { SceneToolMode::Rotate, "Rotate", "rotate_tool.png", "rotate_tool_on.png" }, + { SceneToolMode::Scale, "Scale", "scale_tool.png", "scale_tool_on.png" } +}}; + +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; +} + +UIRect BuildButtonRect(const UIRect& panelRect, std::size_t index) { + return UIRect( + panelRect.x + kPanelPadding, + panelRect.y + kPanelPadding + static_cast(index) * (kButtonExtent + kButtonSpacing), + kButtonExtent, + kButtonExtent); +} + +} // namespace + +bool SceneViewportToolOverlay::Initialize( + const std::filesystem::path& repoRoot, + Host::NativeRenderer& renderer) { + Shutdown(renderer); + + const std::filesystem::path iconRoot = + (repoRoot / "editor" / "resources" / "Icons").lexically_normal(); + bool loadedAnyTexture = false; + for (std::size_t index = 0; index < kToolTexturePaths.size(); ++index) { + const ToolTexturePath& path = kToolTexturePaths[index]; + ToolTextureSet& textureSet = m_toolTextures[index]; + textureSet = {}; + textureSet.mode = path.mode; + textureSet.label = path.label; + + std::string error = {}; + loadedAnyTexture = + renderer.LoadTextureFromFile( + iconRoot / path.inactiveFile, + textureSet.inactiveTexture, + error) || loadedAnyTexture; + error.clear(); + loadedAnyTexture = + renderer.LoadTextureFromFile( + iconRoot / path.activeFile, + textureSet.activeTexture, + error) || loadedAnyTexture; + } + + return loadedAnyTexture; +} + +void SceneViewportToolOverlay::Shutdown(Host::NativeRenderer& renderer) { + for (ToolTextureSet& textureSet : m_toolTextures) { + renderer.ReleaseTexture(textureSet.inactiveTexture); + renderer.ReleaseTexture(textureSet.activeTexture); + textureSet = {}; + } + + ResetFrame(); +} + +void SceneViewportToolOverlay::ResetFrame() { + m_frame = {}; +} + +void SceneViewportToolOverlay::BuildFrame( + const UIRect& viewportRect, + SceneToolMode activeMode, + std::size_t hoveredIndex, + std::size_t pressedIndex) { + m_frame = {}; + if (viewportRect.width <= 1.0f || viewportRect.height <= 1.0f) { + return; + } + + m_frame.visible = true; + m_frame.clipRect = viewportRect; + m_frame.panelRect = UIRect( + viewportRect.x + kViewportInset, + viewportRect.y + kViewportInset, + kPanelPadding * 2.0f + kButtonExtent, + kPanelPadding * 2.0f + + static_cast(m_frame.buttons.size()) * kButtonExtent + + static_cast(m_frame.buttons.size() - 1u) * kButtonSpacing); + + for (std::size_t index = 0; index < m_frame.buttons.size(); ++index) { + const ToolTextureSet& textureSet = m_toolTextures[index]; + SceneViewportToolOverlayButtonFrame& button = m_frame.buttons[index]; + button = {}; + button.mode = textureSet.mode; + button.label = textureSet.label; + button.rect = BuildButtonRect(m_frame.panelRect, index); + button.active = textureSet.mode == activeMode; + button.hovered = index == hoveredIndex; + button.pressed = index == pressedIndex; + button.texture = button.active + ? textureSet.activeTexture + : textureSet.inactiveTexture; + } +} + +std::size_t SceneViewportToolOverlay::HitTest(const UIPoint& point) const { + if (!m_frame.visible || !Contains(point)) { + return kSceneViewportToolOverlayInvalidIndex; + } + + for (std::size_t index = 0; index < m_frame.buttons.size(); ++index) { + if (ContainsPoint(m_frame.buttons[index].rect, point)) { + return index; + } + } + + return kSceneViewportToolOverlayInvalidIndex; +} + +bool SceneViewportToolOverlay::Contains(const UIPoint& point) const { + return m_frame.visible && ContainsPoint(m_frame.panelRect, point); +} + +const SceneViewportToolOverlayFrame& SceneViewportToolOverlay::GetFrame() const { + return m_frame; +} + +void AppendSceneViewportToolOverlay( + UIDrawList& drawList, + const SceneViewportToolOverlayFrame& frame) { + if (!frame.visible) { + return; + } + + constexpr UIColor kPanelFill(24.0f / 255.0f, 26.0f / 255.0f, 29.0f / 255.0f, 220.0f / 255.0f); + constexpr UIColor kPanelOutline(1.0f, 1.0f, 1.0f, 28.0f / 255.0f); + constexpr UIColor kButtonIdle(44.0f / 255.0f, 47.0f / 255.0f, 54.0f / 255.0f, 230.0f / 255.0f); + constexpr UIColor kButtonHover(68.0f / 255.0f, 74.0f / 255.0f, 84.0f / 255.0f, 240.0f / 255.0f); + constexpr UIColor kButtonPressed(86.0f / 255.0f, 96.0f / 255.0f, 109.0f / 255.0f, 245.0f / 255.0f); + constexpr UIColor kButtonActive(78.0f / 255.0f, 102.0f / 255.0f, 126.0f / 255.0f, 245.0f / 255.0f); + constexpr UIColor kButtonOutline(1.0f, 1.0f, 1.0f, 24.0f / 255.0f); + constexpr UIColor kButtonActiveOutline(1.0f, 1.0f, 1.0f, 48.0f / 255.0f); + constexpr UIColor kFallbackText(230.0f / 255.0f, 230.0f / 255.0f, 230.0f / 255.0f, 1.0f); + + drawList.PushClipRect(frame.clipRect); + drawList.AddFilledRect(frame.panelRect, kPanelFill, kPanelCornerRounding); + drawList.AddRectOutline(frame.panelRect, kPanelOutline, 1.0f, kPanelCornerRounding); + + for (const SceneViewportToolOverlayButtonFrame& button : frame.buttons) { + UIColor fill = kButtonIdle; + if (button.active) { + fill = kButtonActive; + } + if (button.hovered) { + fill = button.active ? kButtonActive : kButtonHover; + } + if (button.pressed) { + fill = kButtonPressed; + } + + drawList.AddFilledRect(button.rect, fill, kButtonCornerRounding); + drawList.AddRectOutline( + button.rect, + button.active ? kButtonActiveOutline : kButtonOutline, + 1.0f, + kButtonCornerRounding); + + if (button.texture.IsValid()) { + const UIRect iconRect( + button.rect.x + kIconInset, + button.rect.y + kIconInset, + button.rect.width - kIconInset * 2.0f, + button.rect.height - kIconInset * 2.0f); + drawList.AddImage(iconRect, button.texture); + continue; + } + + drawList.AddText( + UIPoint(button.rect.x + 7.0f, button.rect.y + 9.0f), + button.label, + kFallbackText, + kFallbackFontSize); + } + + drawList.PopClipRect(); +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Features/Scene/SceneViewportToolOverlay.h b/new_editor/app/Features/Scene/SceneViewportToolOverlay.h new file mode 100644 index 00000000..ed72044d --- /dev/null +++ b/new_editor/app/Features/Scene/SceneViewportToolOverlay.h @@ -0,0 +1,75 @@ +#pragma once + +#include "Scene/SceneToolState.h" + +#include +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::Host { + +class NativeRenderer; + +} // namespace XCEngine::UI::Editor::Host + +namespace XCEngine::UI::Editor::App { + +inline constexpr std::size_t kSceneViewportToolOverlayInvalidIndex = + static_cast(-1); + +struct SceneViewportToolOverlayButtonFrame { + SceneToolMode mode = SceneToolMode::View; + ::XCEngine::UI::UIRect rect = {}; + ::XCEngine::UI::UITextureHandle texture = {}; + const char* label = ""; + bool active = false; + bool hovered = false; + bool pressed = false; +}; + +struct SceneViewportToolOverlayFrame { + bool visible = false; + ::XCEngine::UI::UIRect clipRect = {}; + ::XCEngine::UI::UIRect panelRect = {}; + std::array buttons = {}; +}; + +class SceneViewportToolOverlay { +public: + bool Initialize( + const std::filesystem::path& repoRoot, + Host::NativeRenderer& renderer); + void Shutdown(Host::NativeRenderer& renderer); + void ResetFrame(); + + void BuildFrame( + const ::XCEngine::UI::UIRect& viewportRect, + SceneToolMode activeMode, + std::size_t hoveredIndex, + std::size_t pressedIndex); + + std::size_t HitTest(const ::XCEngine::UI::UIPoint& point) const; + bool Contains(const ::XCEngine::UI::UIPoint& point) const; + + const SceneViewportToolOverlayFrame& GetFrame() const; + +private: + struct ToolTextureSet { + SceneToolMode mode = SceneToolMode::View; + const char* label = ""; + ::XCEngine::UI::UITextureHandle inactiveTexture = {}; + ::XCEngine::UI::UITextureHandle activeTexture = {}; + }; + + std::array m_toolTextures = {}; + SceneViewportToolOverlayFrame m_frame = {}; +}; + +void AppendSceneViewportToolOverlay( + ::XCEngine::UI::UIDrawList& drawList, + const SceneViewportToolOverlayFrame& frame); + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Scene/EditorSceneRuntime.cpp b/new_editor/app/Scene/EditorSceneRuntime.cpp index f99139ab..188b456a 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.cpp +++ b/new_editor/app/Scene/EditorSceneRuntime.cpp @@ -1,14 +1,29 @@ #include "Scene/EditorSceneRuntime.h" +#include "State/EditorSelectionStamp.h" + +#include +#include #include +#include #include +#include +#include +#include + namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::Components::GameObject; using ::XCEngine::Components::Scene; +using ::XCEngine::Components::TransformComponent; +using ::XCEngine::Components::Component; +using ::XCEngine::Math::Quaternion; +using ::XCEngine::Math::Vector3; + +constexpr char kComponentIdSeparator = '#'; std::string ResolveGameObjectDisplayName(const GameObject& gameObject) { return gameObject.GetName().empty() @@ -16,19 +31,164 @@ std::string ResolveGameObjectDisplayName(const GameObject& gameObject) { : gameObject.GetName(); } +bool NearlyEqual(float lhs, float rhs, float epsilon = 0.0001f) { + return std::abs(lhs - rhs) <= epsilon; +} + +bool NearlyEqual(const Vector3& lhs, const Vector3& rhs, float epsilon = 0.0001f) { + return NearlyEqual(lhs.x, rhs.x, epsilon) && + NearlyEqual(lhs.y, rhs.y, epsilon) && + NearlyEqual(lhs.z, rhs.z, epsilon); +} + +bool NearlyEqual(const Quaternion& lhs, const Quaternion& rhs, float epsilon = 0.0001f) { + return std::abs(lhs.Dot(rhs)) >= 1.0f - epsilon; +} + +bool TransformSnapshotsMatch( + const SceneTransformSnapshot& lhs, + const SceneTransformSnapshot& rhs) { + return lhs.IsValid() && + rhs.IsValid() && + lhs.targetId == rhs.targetId && + NearlyEqual(lhs.position, rhs.position) && + NearlyEqual(lhs.rotation, rhs.rotation) && + NearlyEqual(lhs.scale, rhs.scale); +} + +std::string BuildEditorComponentId( + std::string_view typeName, + std::size_t ordinal) { + std::string componentId(typeName); + componentId.push_back(kComponentIdSeparator); + componentId += std::to_string(ordinal); + return componentId; +} + +bool ParseEditorComponentId( + std::string_view componentId, + std::string& outTypeName, + std::size_t& outOrdinal) { + const std::size_t separatorIndex = componentId.find(kComponentIdSeparator); + if (separatorIndex == std::string_view::npos || + separatorIndex == 0u || + separatorIndex + 1u >= componentId.size()) { + return false; + } + + outTypeName = std::string(componentId.substr(0u, separatorIndex)); + std::size_t ordinal = 0u; + const std::string_view ordinalText = + componentId.substr(separatorIndex + 1u); + const char* first = ordinalText.data(); + const char* last = ordinalText.data() + ordinalText.size(); + const std::from_chars_result result = + std::from_chars(first, last, ordinal); + if (result.ec != std::errc() || result.ptr != last) { + outTypeName.clear(); + return false; + } + + outOrdinal = ordinal; + return true; +} + +EditorSceneComponentDescriptor BuildComponentDescriptor( + const Component& component, + std::size_t ordinal) { + EditorSceneComponentDescriptor descriptor = {}; + descriptor.typeName = component.GetName(); + descriptor.componentId = + BuildEditorComponentId(descriptor.typeName, ordinal); + descriptor.component = &component; + descriptor.removable = + component.GetGameObject() != nullptr && + component.GetGameObject()->GetTransform() != &component; + return descriptor; +} + } // namespace +std::string_view GetSceneToolModeName(SceneToolMode mode) { + switch (mode) { + case SceneToolMode::View: + return "View"; + case SceneToolMode::Translate: + return "Translate"; + case SceneToolMode::Rotate: + return "Rotate"; + case SceneToolMode::Scale: + return "Scale"; + default: + return "Unknown"; + } +} + +std::string_view GetSceneToolSpaceModeName(SceneToolSpaceMode mode) { + switch (mode) { + case SceneToolSpaceMode::World: + return "World"; + case SceneToolSpaceMode::Local: + return "Local"; + default: + return "Unknown"; + } +} + +std::string_view GetSceneToolPivotModeName(SceneToolPivotMode mode) { + switch (mode) { + case SceneToolPivotMode::Pivot: + return "Pivot"; + case SceneToolPivotMode::Center: + return "Center"; + default: + return "Unknown"; + } +} + +std::string_view GetSceneToolHandleName(SceneToolHandle handle) { + switch (handle) { + case SceneToolHandle::AxisX: + return "AxisX"; + case SceneToolHandle::AxisY: + return "AxisY"; + case SceneToolHandle::AxisZ: + return "AxisZ"; + case SceneToolHandle::None: + default: + return "None"; + } +} + +std::string_view GetSceneToolInteractionLockName(SceneToolInteractionLock lock) { + switch (lock) { + case SceneToolInteractionLock::Toolbar: + return "Toolbar"; + case SceneToolInteractionLock::TransformDrag: + return "TransformDrag"; + case SceneToolInteractionLock::None: + default: + return "None"; + } +} + bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) { m_projectRoot = projectRoot; m_startupSceneResult = EnsureEditorStartupScene(projectRoot); + EnsureSceneViewCamera(); + ResetTransformEditHistory(); + m_toolState = {}; RefreshScene(); return m_startupSceneResult.ready; } void EditorSceneRuntime::RefreshScene() { - if (!HasValidSelection()) { + if (m_selectedGameObjectId.has_value() && !HasValidSelection()) { m_selectedGameObjectId.reset(); + m_selectionStamp = GenerateEditorSelectionStamp(); } + + ClearInvalidToolInteractionState(); } void EditorSceneRuntime::EnsureSceneSelection() { @@ -47,6 +207,56 @@ Scene* EditorSceneRuntime::GetActiveScene() const { return GetActiveEditorScene(); } +::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() { + return EnsureSceneViewCamera() ? m_sceneViewCamera.camera : nullptr; +} + +const ::XCEngine::Components::CameraComponent* EditorSceneRuntime::GetSceneViewCamera() const { + return m_sceneViewCamera.camera; +} + +float EditorSceneRuntime::GetSceneViewOrbitDistance() const { + return m_sceneViewCamera.controller.GetDistance(); +} + +SceneViewportRenderRequest EditorSceneRuntime::BuildSceneViewportRenderRequest() { + SceneViewportRenderRequest request = {}; + request.scene = GetActiveScene(); + request.camera = GetSceneViewCamera(); + request.orbitDistance = GetSceneViewOrbitDistance(); + if (const std::optional selectedId = GetSelectedGameObjectId(); + selectedId.has_value()) { + request.selectedObjectIds.push_back(selectedId.value()); + } + return request; +} + +void EditorSceneRuntime::ApplySceneViewportCameraInput( + const SceneViewportCameraInputState& input) { + if (!EnsureSceneViewCamera()) { + return; + } + + m_sceneViewCamera.controller.ApplyInput(input); + ApplySceneViewCameraController(); +} + +bool EditorSceneRuntime::FocusSceneSelection() { + const GameObject* selectedGameObject = GetSelectedGameObject(); + if (selectedGameObject == nullptr || !EnsureSceneViewCamera()) { + return false; + } + + const auto* transform = selectedGameObject->GetTransform(); + if (transform == nullptr) { + return false; + } + + m_sceneViewCamera.controller.Focus(transform->GetPosition()); + ApplySceneViewCameraController(); + return true; +} + bool EditorSceneRuntime::HasSceneSelection() const { return HasValidSelection(); } @@ -80,6 +290,35 @@ const GameObject* EditorSceneRuntime::GetSelectedGameObject() const { : nullptr; } +std::vector EditorSceneRuntime::GetSelectedComponents() const { + std::vector descriptors = {}; + const GameObject* gameObject = GetSelectedGameObject(); + if (gameObject == nullptr) { + return descriptors; + } + + const std::vector components = + gameObject->GetComponents(); + descriptors.reserve(components.size()); + std::unordered_map ordinalsByType = {}; + for (const Component* component : components) { + if (component == nullptr) { + continue; + } + + const std::string typeName = component->GetName(); + const std::size_t ordinal = ordinalsByType[typeName]; + descriptors.push_back(BuildComponentDescriptor(*component, ordinal)); + ordinalsByType[typeName] = ordinal + 1u; + } + + return descriptors; +} + +std::uint64_t EditorSceneRuntime::GetSelectionStamp() const { + return m_selectionStamp; +} + bool EditorSceneRuntime::SetSelection(std::string_view itemId) { const std::optional gameObjectId = ParseEditorGameObjectItemId(itemId); @@ -103,12 +342,41 @@ bool EditorSceneRuntime::SetSelection(GameObject::ID id) { const bool changed = !m_selectedGameObjectId.has_value() || m_selectedGameObjectId.value() != id; + if (changed) { + ResetToolInteractionState(); + m_selectionStamp = GenerateEditorSelectionStamp(); + } m_selectedGameObjectId = id; + ClearInvalidToolInteractionState(); return changed; } void EditorSceneRuntime::ClearSelection() { + ResetToolInteractionState(); m_selectedGameObjectId.reset(); + m_selectionStamp = GenerateEditorSelectionStamp(); +} + +bool EditorSceneRuntime::OpenSceneAsset(const std::filesystem::path& scenePath) { + if (!OpenEditorSceneAsset(scenePath)) { + return false; + } + + m_startupSceneResult.ready = true; + m_startupSceneResult.loadedFromDisk = true; + m_startupSceneResult.scenePath = scenePath; + if (Scene* activeScene = GetActiveScene(); + activeScene != nullptr) { + m_startupSceneResult.sceneName = activeScene->GetName(); + } else { + m_startupSceneResult.sceneName = scenePath.stem().string(); + } + + ResetTransformEditHistory(); + ResetToolInteractionState(); + RefreshScene(); + EnsureSceneSelection(); + return true; } GameObject* EditorSceneRuntime::FindGameObject(std::string_view itemId) const { @@ -124,6 +392,7 @@ bool EditorSceneRuntime::RenameGameObject( } bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { + ResetTransformEditHistory(); const bool deleted = DeleteEditorGameObject(itemId); RefreshScene(); EnsureSceneSelection(); @@ -131,6 +400,7 @@ bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) { } std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { + ResetTransformEditHistory(); const std::string duplicatedItemId = DuplicateEditorGameObject(itemId); if (!duplicatedItemId.empty()) { SetSelection(duplicatedItemId); @@ -143,6 +413,7 @@ std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) { bool EditorSceneRuntime::ReparentGameObject( std::string_view itemId, std::string_view parentItemId) { + ResetTransformEditHistory(); const bool reparented = ReparentEditorGameObject(itemId, parentItemId); RefreshScene(); @@ -150,19 +421,476 @@ bool EditorSceneRuntime::ReparentGameObject( } bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) { + ResetTransformEditHistory(); const bool moved = MoveEditorGameObjectToRoot(itemId); RefreshScene(); return moved; } +bool EditorSceneRuntime::CanRemoveSelectedComponent( + std::string_view componentId) const { + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + return descriptor.IsValid() && descriptor.removable; +} + +bool EditorSceneRuntime::RemoveSelectedComponent(std::string_view componentId) { + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + if (!descriptor.IsValid() || !descriptor.removable) { + return false; + } + + const std::optional selectedId = GetSelectedGameObjectId(); + Scene* scene = GetActiveScene(); + if (!selectedId.has_value() || scene == nullptr) { + return false; + } + + GameObject* gameObject = scene->FindByID(selectedId.value()); + Component* component = + const_cast(descriptor.component); + if (gameObject == nullptr || component == nullptr) { + return false; + } + + ResetTransformEditHistory(); + const bool removed = gameObject->RemoveComponent(component); + RefreshScene(); + return removed; +} + +bool EditorSceneRuntime::SetSelectedTransformLocalPosition( + std::string_view componentId, + const Vector3& position) { + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + auto* transform = + const_cast( + dynamic_cast(descriptor.component)); + if (transform == nullptr) { + return false; + } + + SceneTransformSnapshot beforeSnapshot = {}; + if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + return false; + } + + transform->SetLocalPosition(position); + + SceneTransformSnapshot afterSnapshot = {}; + CaptureSelectedTransformSnapshot(afterSnapshot); + RecordTransformEdit(beforeSnapshot, afterSnapshot); + return true; +} + +bool EditorSceneRuntime::SetSelectedTransformLocalEulerAngles( + std::string_view componentId, + const Vector3& eulerAngles) { + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + auto* transform = + const_cast( + dynamic_cast(descriptor.component)); + if (transform == nullptr) { + return false; + } + + SceneTransformSnapshot beforeSnapshot = {}; + if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + return false; + } + + transform->SetLocalEulerAngles(eulerAngles); + + SceneTransformSnapshot afterSnapshot = {}; + CaptureSelectedTransformSnapshot(afterSnapshot); + RecordTransformEdit(beforeSnapshot, afterSnapshot); + return true; +} + +bool EditorSceneRuntime::SetSelectedTransformLocalScale( + std::string_view componentId, + const Vector3& scale) { + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + auto* transform = + const_cast( + dynamic_cast(descriptor.component)); + if (transform == nullptr) { + return false; + } + + SceneTransformSnapshot beforeSnapshot = {}; + if (!CaptureSelectedTransformSnapshot(beforeSnapshot)) { + return false; + } + + transform->SetLocalScale(scale); + + SceneTransformSnapshot afterSnapshot = {}; + CaptureSelectedTransformSnapshot(afterSnapshot); + RecordTransformEdit(beforeSnapshot, afterSnapshot); + return true; +} + +bool EditorSceneRuntime::ApplySelectedComponentMutation( + std::string_view componentId, + const std::function& mutation) { + if (!mutation) { + return false; + } + + const EditorSceneComponentDescriptor descriptor = + ResolveSelectedComponentDescriptor(componentId); + Component* component = const_cast(descriptor.component); + if (!descriptor.IsValid() || component == nullptr) { + return false; + } + + return mutation(*component); +} + +const SceneToolState& EditorSceneRuntime::GetToolState() const { + return m_toolState; +} + +SceneToolMode EditorSceneRuntime::GetToolMode() const { + return m_toolState.mode; +} + +SceneToolSpaceMode EditorSceneRuntime::GetToolSpaceMode() const { + return m_toolState.spaceMode; +} + +SceneToolPivotMode EditorSceneRuntime::GetToolPivotMode() const { + return m_toolState.pivotMode; +} + +void EditorSceneRuntime::SetToolMode(SceneToolMode mode) { + if (m_toolState.mode == mode) { + return; + } + + ResetToolInteractionState(); + m_toolState.mode = mode; +} + +void EditorSceneRuntime::SetToolSpaceMode(SceneToolSpaceMode mode) { + if (m_toolState.spaceMode == mode) { + return; + } + + ResetToolInteractionState(); + m_toolState.spaceMode = mode; +} + +void EditorSceneRuntime::SetToolPivotMode(SceneToolPivotMode mode) { + if (m_toolState.pivotMode == mode) { + return; + } + + ResetToolInteractionState(); + m_toolState.pivotMode = mode; +} + +void EditorSceneRuntime::SetHoveredToolHandle(SceneToolHandle handle) { + if (m_toolState.dragState.active) { + return; + } + + m_toolState.hoveredHandle = handle; +} + +void EditorSceneRuntime::SetToolInteractionLock(SceneToolInteractionLock lock) { + m_toolState.interactionLock = lock; +} + +void EditorSceneRuntime::ClearToolInteractionLock() { + m_toolState.interactionLock = SceneToolInteractionLock::None; +} + +void EditorSceneRuntime::SetToolbarHoveredIndex(std::size_t index) { + m_toolState.toolbarHoveredIndex = index; +} + +void EditorSceneRuntime::SetToolbarActiveIndex(std::size_t index) { + m_toolState.toolbarActiveIndex = index; +} + +void EditorSceneRuntime::ClearToolbarInteraction() { + m_toolState.toolbarHoveredIndex = kSceneToolInvalidToolbarIndex; + m_toolState.toolbarActiveIndex = kSceneToolInvalidToolbarIndex; + if (m_toolState.interactionLock == SceneToolInteractionLock::Toolbar) { + ClearToolInteractionLock(); + } +} + +void EditorSceneRuntime::ResetToolInteractionState() { + CancelTransformToolDrag(); + ResetToolInteractionTransientState(); +} + +bool EditorSceneRuntime::CaptureSelectedTransformSnapshot( + SceneTransformSnapshot& outSnapshot) const { + const GameObject* selectedGameObject = GetSelectedGameObject(); + if (selectedGameObject == nullptr) { + outSnapshot = {}; + return false; + } + + const TransformComponent* transform = selectedGameObject->GetTransform(); + if (transform == nullptr) { + outSnapshot = {}; + return false; + } + + outSnapshot = {}; + outSnapshot.targetId = selectedGameObject->GetID(); + outSnapshot.position = transform->GetPosition(); + outSnapshot.rotation = transform->GetRotation(); + outSnapshot.scale = transform->GetScale(); + outSnapshot.valid = true; + return true; +} + +bool EditorSceneRuntime::ApplyTransformSnapshot( + const SceneTransformSnapshot& snapshot) { + if (!snapshot.IsValid()) { + return false; + } + + Scene* scene = GetActiveScene(); + if (scene == nullptr) { + return false; + } + + GameObject* gameObject = scene->FindByID(snapshot.targetId); + if (gameObject == nullptr) { + return false; + } + + TransformComponent* transform = gameObject->GetTransform(); + if (transform == nullptr) { + return false; + } + + transform->SetPosition(snapshot.position); + transform->SetRotation(snapshot.rotation); + transform->SetScale(snapshot.scale); + return true; +} + +bool EditorSceneRuntime::RecordTransformEdit( + const SceneTransformSnapshot& before, + const SceneTransformSnapshot& after) { + if (!before.IsValid() || + !after.IsValid() || + before.targetId != after.targetId || + TransformSnapshotsMatch(before, after)) { + return false; + } + + m_transformUndoStack.push_back({ before, after }); + m_transformRedoStack.clear(); + return true; +} + +bool EditorSceneRuntime::BeginTransformToolDrag( + SceneToolHandle handle, + const ::XCEngine::UI::UIPoint& startPointerPosition) { + if (handle == SceneToolHandle::None || + m_toolState.mode == SceneToolMode::View) { + return false; + } + + SceneTransformSnapshot snapshot = {}; + if (!CaptureSelectedTransformSnapshot(snapshot)) { + return false; + } + + CancelTransformToolDrag(); + m_toolState.dragState = {}; + m_toolState.dragState.active = true; + m_toolState.dragState.mode = m_toolState.mode; + m_toolState.dragState.handle = handle; + m_toolState.dragState.startPointerPosition = startPointerPosition; + m_toolState.dragState.initialTransform = snapshot; + m_toolState.hoveredHandle = handle; + m_toolState.activeHandle = handle; + SetToolInteractionLock(SceneToolInteractionLock::TransformDrag); + return true; +} + +bool EditorSceneRuntime::HasActiveTransformToolDrag() const { + return m_toolState.dragState.active; +} + +const SceneToolDragState* EditorSceneRuntime::GetActiveTransformToolDrag() const { + return m_toolState.dragState.active ? &m_toolState.dragState : nullptr; +} + +bool EditorSceneRuntime::ApplyTransformToolPreview( + const SceneTransformSnapshot& snapshot) { + if (!m_toolState.dragState.active || + !snapshot.IsValid() || + snapshot.targetId != m_toolState.dragState.initialTransform.targetId) { + return false; + } + + return ApplyTransformSnapshot(snapshot); +} + +bool EditorSceneRuntime::CommitTransformToolDrag() { + if (!m_toolState.dragState.active) { + return false; + } + + SceneTransformSnapshot afterSnapshot = {}; + const bool captured = CaptureSelectedTransformSnapshot(afterSnapshot); + const SceneTransformSnapshot beforeSnapshot = + m_toolState.dragState.initialTransform; + ResetToolInteractionTransientState(); + + if (!captured || !afterSnapshot.IsValid() || !beforeSnapshot.IsValid()) { + return false; + } + + return RecordTransformEdit(beforeSnapshot, afterSnapshot); +} + +void EditorSceneRuntime::CancelTransformToolDrag() { + if (!m_toolState.dragState.active) { + return; + } + + const SceneTransformSnapshot snapshot = m_toolState.dragState.initialTransform; + if (snapshot.IsValid()) { + ApplyTransformSnapshot(snapshot); + } + + ResetToolInteractionTransientState(); +} + +bool EditorSceneRuntime::CanUndoTransformEdit() const { + return !m_transformUndoStack.empty(); +} + +bool EditorSceneRuntime::CanRedoTransformEdit() const { + return !m_transformRedoStack.empty(); +} + +bool EditorSceneRuntime::UndoTransformEdit() { + if (m_transformUndoStack.empty()) { + return false; + } + + const TransformEditTransaction transaction = m_transformUndoStack.back(); + if (!ApplyTransformSnapshot(transaction.before)) { + return false; + } + + m_transformUndoStack.pop_back(); + m_transformRedoStack.push_back(transaction); + SetSelection(transaction.before.targetId); + return true; +} + +bool EditorSceneRuntime::RedoTransformEdit() { + if (m_transformRedoStack.empty()) { + return false; + } + + const TransformEditTransaction transaction = m_transformRedoStack.back(); + if (!ApplyTransformSnapshot(transaction.after)) { + return false; + } + + m_transformRedoStack.pop_back(); + m_transformUndoStack.push_back(transaction); + SetSelection(transaction.after.targetId); + return true; +} + +bool EditorSceneRuntime::EnsureSceneViewCamera() { + if (m_sceneViewCamera.gameObject != nullptr && + m_sceneViewCamera.camera != nullptr) { + return true; + } + + m_sceneViewCamera = {}; + m_sceneViewCamera.gameObject = + std::make_unique("EditorSceneCamera"); + m_sceneViewCamera.camera = + m_sceneViewCamera.gameObject->AddComponent< + ::XCEngine::Components::CameraComponent>(); + if (m_sceneViewCamera.camera == nullptr) { + m_sceneViewCamera.gameObject.reset(); + return false; + } + + m_sceneViewCamera.camera->SetPrimary(false); + m_sceneViewCamera.camera->SetProjectionType( + ::XCEngine::Components::CameraProjectionType::Perspective); + m_sceneViewCamera.camera->SetFieldOfView(60.0f); + m_sceneViewCamera.camera->SetNearClipPlane(0.03f); + m_sceneViewCamera.camera->SetFarClipPlane(2000.0f); + m_sceneViewCamera.controller.Reset(); + ApplySceneViewCameraController(); + return true; +} + +void EditorSceneRuntime::ApplySceneViewCameraController() { + if (m_sceneViewCamera.gameObject == nullptr) { + return; + } + + if (auto* transform = m_sceneViewCamera.gameObject->GetTransform(); + transform != nullptr) { + m_sceneViewCamera.controller.ApplyTo(*transform); + } +} + bool EditorSceneRuntime::HasValidSelection() const { return GetSelectedGameObject() != nullptr; } +EditorSceneComponentDescriptor EditorSceneRuntime::ResolveSelectedComponentDescriptor( + std::string_view componentId) const { + std::string typeName = {}; + std::size_t ordinal = 0u; + if (!ParseEditorComponentId(componentId, typeName, ordinal)) { + return {}; + } + + const GameObject* gameObject = GetSelectedGameObject(); + if (gameObject == nullptr) { + return {}; + } + + std::size_t currentOrdinal = 0u; + for (const Component* component : gameObject->GetComponents()) { + if (component == nullptr || component->GetName() != typeName) { + continue; + } + + if (currentOrdinal == ordinal) { + return BuildComponentDescriptor(*component, currentOrdinal); + } + + ++currentOrdinal; + } + + return {}; +} + bool EditorSceneRuntime::SelectFirstAvailableGameObject() { Scene* scene = GetActiveScene(); if (scene == nullptr) { - m_selectedGameObjectId.reset(); + if (m_selectedGameObjectId.has_value()) { + ClearSelection(); + } return false; } @@ -171,13 +899,45 @@ bool EditorSceneRuntime::SelectFirstAvailableGameObject() { continue; } - m_selectedGameObjectId = root->GetID(); - return true; + return SetSelection(root->GetID()); } - m_selectedGameObjectId.reset(); + if (m_selectedGameObjectId.has_value()) { + ClearSelection(); + } return false; } -} // namespace XCEngine::UI::Editor::App +void EditorSceneRuntime::ResetTransformEditHistory() { + m_transformUndoStack.clear(); + m_transformRedoStack.clear(); +} +void EditorSceneRuntime::ResetToolInteractionTransientState() { + m_toolState.hoveredHandle = SceneToolHandle::None; + m_toolState.activeHandle = SceneToolHandle::None; + ClearToolbarInteraction(); + ClearToolInteractionLock(); + m_toolState.dragState = {}; +} + +void EditorSceneRuntime::ClearInvalidToolInteractionState() { + if (!HasSceneSelection()) { + ResetToolInteractionTransientState(); + return; + } + + if (!m_toolState.dragState.active) { + return; + } + + const SceneTransformSnapshot snapshot = m_toolState.dragState.initialTransform; + Scene* scene = GetActiveScene(); + if (scene == nullptr || + !snapshot.IsValid() || + scene->FindByID(snapshot.targetId) == nullptr) { + ResetToolInteractionTransientState(); + } +} + +} // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Scene/EditorSceneRuntime.h b/new_editor/app/Scene/EditorSceneRuntime.h index 1325ec0a..980287e7 100644 --- a/new_editor/app/Scene/EditorSceneRuntime.h +++ b/new_editor/app/Scene/EditorSceneRuntime.h @@ -1,20 +1,61 @@ #pragma once #include "Scene/EditorSceneBridge.h" +#include "Scene/SceneViewportCameraController.h" +#include "Scene/SceneToolState.h" +#include + +#include +#include #include +#include #include #include +#include namespace XCEngine::Components { +class CameraComponent; +class Component; class GameObject; class Scene; } // namespace XCEngine::Components +namespace XCEngine::Math { + +struct Vector3; + +} // namespace XCEngine::Math + namespace XCEngine::UI::Editor::App { +struct SceneViewportRenderRequest { + ::XCEngine::Components::Scene* scene = nullptr; + ::XCEngine::Components::CameraComponent* camera = nullptr; + std::vector selectedObjectIds = {}; + float orbitDistance = 0.0f; + bool debugSelectionMask = false; + + bool IsValid() const { + return scene != nullptr && camera != nullptr; + } +}; + +struct EditorSceneComponentDescriptor { + std::string componentId = {}; + std::string typeName = {}; + const ::XCEngine::Components::Component* component = nullptr; + bool removable = false; + + bool IsValid() const { + return !componentId.empty() && + !typeName.empty() && + component != nullptr; + } +}; + class EditorSceneRuntime { public: bool Initialize(const std::filesystem::path& projectRoot); @@ -24,16 +65,25 @@ public: const EditorStartupSceneResult& GetStartupResult() const; ::XCEngine::Components::Scene* GetActiveScene() const; + ::XCEngine::Components::CameraComponent* GetSceneViewCamera(); + const ::XCEngine::Components::CameraComponent* GetSceneViewCamera() const; + float GetSceneViewOrbitDistance() const; + SceneViewportRenderRequest BuildSceneViewportRenderRequest(); + void ApplySceneViewportCameraInput(const SceneViewportCameraInputState& input); + bool FocusSceneSelection(); bool HasSceneSelection() const; std::optional<::XCEngine::Components::GameObject::ID> GetSelectedGameObjectId() const; std::string GetSelectedItemId() const; std::string GetSelectedDisplayName() const; const ::XCEngine::Components::GameObject* GetSelectedGameObject() const; + std::vector GetSelectedComponents() const; + std::uint64_t GetSelectionStamp() const; bool SetSelection(std::string_view itemId); bool SetSelection(::XCEngine::Components::GameObject::ID id); void ClearSelection(); + bool OpenSceneAsset(const std::filesystem::path& scenePath); ::XCEngine::Components::GameObject* FindGameObject(std::string_view itemId) const; bool RenameGameObject( @@ -45,15 +95,85 @@ public: std::string_view itemId, std::string_view parentItemId); bool MoveGameObjectToRoot(std::string_view itemId); + bool CanRemoveSelectedComponent(std::string_view componentId) const; + bool RemoveSelectedComponent(std::string_view componentId); + bool SetSelectedTransformLocalPosition( + std::string_view componentId, + const ::XCEngine::Math::Vector3& position); + bool SetSelectedTransformLocalEulerAngles( + std::string_view componentId, + const ::XCEngine::Math::Vector3& eulerAngles); + bool SetSelectedTransformLocalScale( + std::string_view componentId, + const ::XCEngine::Math::Vector3& scale); + bool ApplySelectedComponentMutation( + std::string_view componentId, + const std::function& mutation); + + const SceneToolState& GetToolState() const; + SceneToolMode GetToolMode() const; + SceneToolSpaceMode GetToolSpaceMode() const; + SceneToolPivotMode GetToolPivotMode() const; + void SetToolMode(SceneToolMode mode); + void SetToolSpaceMode(SceneToolSpaceMode mode); + void SetToolPivotMode(SceneToolPivotMode mode); + void SetHoveredToolHandle(SceneToolHandle handle); + void SetToolInteractionLock(SceneToolInteractionLock lock); + void ClearToolInteractionLock(); + void SetToolbarHoveredIndex(std::size_t index); + void SetToolbarActiveIndex(std::size_t index); + void ClearToolbarInteraction(); + void ResetToolInteractionState(); + + bool CaptureSelectedTransformSnapshot(SceneTransformSnapshot& outSnapshot) const; + bool ApplyTransformSnapshot(const SceneTransformSnapshot& snapshot); + bool RecordTransformEdit( + const SceneTransformSnapshot& before, + const SceneTransformSnapshot& after); + bool BeginTransformToolDrag( + SceneToolHandle handle, + const ::XCEngine::UI::UIPoint& startPointerPosition); + bool HasActiveTransformToolDrag() const; + const SceneToolDragState* GetActiveTransformToolDrag() const; + bool ApplyTransformToolPreview(const SceneTransformSnapshot& snapshot); + bool CommitTransformToolDrag(); + void CancelTransformToolDrag(); + + bool CanUndoTransformEdit() const; + bool CanRedoTransformEdit() const; + bool UndoTransformEdit(); + bool RedoTransformEdit(); private: + struct SceneViewCameraState { + std::unique_ptr<::XCEngine::Components::GameObject> gameObject = {}; + ::XCEngine::Components::CameraComponent* camera = nullptr; + SceneViewportCameraController controller = {}; + }; + + struct TransformEditTransaction { + SceneTransformSnapshot before = {}; + SceneTransformSnapshot after = {}; + }; + + bool EnsureSceneViewCamera(); + void ApplySceneViewCameraController(); bool HasValidSelection() const; + EditorSceneComponentDescriptor ResolveSelectedComponentDescriptor( + std::string_view componentId) const; bool SelectFirstAvailableGameObject(); + void ResetTransformEditHistory(); + void ResetToolInteractionTransientState(); + void ClearInvalidToolInteractionState(); std::filesystem::path m_projectRoot = {}; EditorStartupSceneResult m_startupSceneResult = {}; std::optional<::XCEngine::Components::GameObject::ID> m_selectedGameObjectId = std::nullopt; + std::uint64_t m_selectionStamp = 0u; + SceneViewCameraState m_sceneViewCamera = {}; + SceneToolState m_toolState = {}; + std::vector m_transformUndoStack = {}; + std::vector m_transformRedoStack = {}; }; } // namespace XCEngine::UI::Editor::App - diff --git a/new_editor/app/State/EditorContext.cpp b/new_editor/app/State/EditorContext.cpp index 15c3906c..7e8c9c26 100644 --- a/new_editor/app/State/EditorContext.cpp +++ b/new_editor/app/State/EditorContext.cpp @@ -2,6 +2,9 @@ #include "Composition/EditorShellAssetBuilder.h" #include "Scene/EditorSceneRuntime.h" +#include "State/EditorSelectionStamp.h" + +#include #include #include @@ -11,6 +14,9 @@ namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager; +using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; +using ::XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using ::XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; std::string ComposeStatusText( std::string_view status, @@ -26,6 +32,64 @@ std::string ComposeStatusText( return std::string(status) + ": " + std::string(message); } +UIEditorWorkspacePanelPresentationModel* FindMutablePresentation( + std::vector& presentations, + std::string_view panelId) { + for (UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (presentation.panelId == panelId) { + return &presentation; + } + } + + return nullptr; +} + +UIEditorViewportSlotToolItem BuildSceneToolItem( + std::string itemId, + std::string label, + UIEditorViewportSlotToolSlot slot, + bool selected, + float desiredWidth) { + UIEditorViewportSlotToolItem item = {}; + item.itemId = std::move(itemId); + item.label = std::move(label); + item.slot = slot; + item.enabled = true; + item.selected = selected; + item.desiredWidth = desiredWidth; + return item; +} + +std::vector BuildSceneViewportTopBarItems( + const EditorSceneRuntime& sceneRuntime) { + return { + BuildSceneToolItem( + "scene.pivot.pivot", + "Pivot", + UIEditorViewportSlotToolSlot::Leading, + sceneRuntime.GetToolPivotMode() == SceneToolPivotMode::Pivot, + 52.0f), + BuildSceneToolItem( + "scene.pivot.center", + "Center", + UIEditorViewportSlotToolSlot::Leading, + sceneRuntime.GetToolPivotMode() == SceneToolPivotMode::Center, + 58.0f), + BuildSceneToolItem( + "scene.space.world", + "World", + UIEditorViewportSlotToolSlot::Leading, + sceneRuntime.GetToolSpaceMode() == SceneToolSpaceMode::World, + 58.0f), + BuildSceneToolItem( + "scene.space.local", + "Local", + UIEditorViewportSlotToolSlot::Leading, + sceneRuntime.GetToolSpaceMode() == SceneToolSpaceMode::Local, + 56.0f) + }; +} + } // namespace bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { @@ -38,6 +102,8 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) { m_session = {}; m_session.repoRoot = repoRoot; m_session.projectRoot = (repoRoot / "project").lexically_normal(); + m_projectRuntime = {}; + m_projectRuntime.Initialize(repoRoot); m_sceneRuntime = {}; m_sceneRuntime.Initialize(m_session.projectRoot); m_hostCommandBridge.BindSession(m_session); @@ -57,8 +123,14 @@ void EditorContext::AttachTextMeasurer( void EditorContext::BindEditCommandRoutes( EditorEditCommandRoute* hierarchyRoute, - EditorEditCommandRoute* projectRoute) { - m_hostCommandBridge.BindEditCommandRoutes(hierarchyRoute, projectRoute); + EditorEditCommandRoute* projectRoute, + EditorEditCommandRoute* sceneRoute, + EditorEditCommandRoute* inspectorRoute) { + m_hostCommandBridge.BindEditCommandRoutes( + hierarchyRoute, + projectRoute, + sceneRoute, + inspectorRoute); } void EditorContext::SetExitRequestHandler(std::function handler) { @@ -86,6 +158,14 @@ const EditorSession& EditorContext::GetSession() const { return m_session; } +EditorProjectRuntime& EditorContext::GetProjectRuntime() { + return m_projectRuntime; +} + +const EditorProjectRuntime& EditorContext::GetProjectRuntime() const { + return m_projectRuntime; +} + EditorSceneRuntime& EditorContext::GetSceneRuntime() { return m_sceneRuntime; } @@ -95,11 +175,17 @@ const EditorSceneRuntime& EditorContext::GetSceneRuntime() const { } void EditorContext::SetSelection(EditorSelectionState selection) { + selection.stamp = GenerateEditorSelectionStamp(); m_session.selection = std::move(selection); } void EditorContext::ClearSelection() { m_session.selection = {}; + m_session.selection.stamp = GenerateEditorSelectionStamp(); +} + +void EditorContext::SyncSessionFromProjectRuntime() { + m_session.selection = m_projectRuntime.GetSelection(); } UIEditorWorkspaceController EditorContext::BuildWorkspaceController() const { @@ -117,12 +203,27 @@ UIEditorShellInteractionDefinition EditorContext::BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, std::string_view captureText, EditorShellVariant variant) const { - return BuildEditorApplicationShellInteractionDefinition( + UIEditorShellInteractionDefinition definition = + BuildEditorApplicationShellInteractionDefinition( m_shellAsset, workspaceController, ComposeStatusText(m_lastStatus, m_lastMessage), captureText, variant); + + if (UIEditorWorkspacePanelPresentationModel* scenePresentation = + FindMutablePresentation(definition.workspacePresentations, kScenePanelId); + scenePresentation != nullptr) { + scenePresentation->viewportShellModel.spec.chrome.showTopBar = true; + scenePresentation->viewportShellModel.spec.toolItems = + BuildSceneViewportTopBarItems(m_sceneRuntime); + scenePresentation->viewportShellModel.spec.visualState.hoveredToolIndex = + m_sceneRuntime.GetToolState().toolbarHoveredIndex; + scenePresentation->viewportShellModel.spec.visualState.activeToolIndex = + m_sceneRuntime.GetToolState().toolbarActiveIndex; + } + + return definition; } std::string EditorContext::DescribeWorkspaceState( diff --git a/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp new file mode 100644 index 00000000..7f160da2 --- /dev/null +++ b/tests/UI/Editor/unit/test_scene_viewport_runtime.cpp @@ -0,0 +1,650 @@ +#include "Scene/EditorSceneRuntime.h" +#include "Features/Scene/SceneViewportController.h" +#include "Features/Inspector/InspectorSubject.h" +#include "Rendering/Viewport/ViewportHostService.h" +#include "State/EditorSelectionStamp.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::App { +namespace { + +using ::XCEngine::Components::GameObject; +using ::XCEngine::Components::Scene; +using ::XCEngine::Components::SceneManager; +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Editor::UIEditorViewportInputBridgeState; +using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame; +using ::XCEngine::UI::Editor::UIEditorWorkspaceComposeState; +using ::XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationState; +using ::XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame; +using ::XCEngine::UI::Editor::UpdateUIEditorViewportInputBridge; + +class ScopedSceneManagerReset final { +public: + ScopedSceneManagerReset() { + Reset(); + } + + ~ScopedSceneManagerReset() { + Reset(); + ::XCEngine::Resources::ResourceManager::Get().Shutdown(); + } + +private: + static void Reset() { + SceneManager& manager = SceneManager::Get(); + const auto scenes = manager.GetAllScenes(); + for (Scene* scene : scenes) { + manager.UnloadScene(scene); + } + } +}; + +class TemporaryProjectRoot final { +public: + TemporaryProjectRoot() { + const auto uniqueSuffix = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_root = + std::filesystem::temp_directory_path() / + ("xcui_scene_viewport_runtime_" + std::to_string(uniqueSuffix)); + } + + ~TemporaryProjectRoot() { + std::error_code errorCode = {}; + std::filesystem::remove_all(m_root, errorCode); + } + + const std::filesystem::path& Root() const { + return m_root; + } + + std::filesystem::path MainScenePath() const { + return m_root / "Assets" / "Scenes" / "Main.xc"; + } + +private: + std::filesystem::path m_root = {}; +}; + +void SaveMainScene(const TemporaryProjectRoot& projectRoot, const Math::Vector3& targetPosition) { + const std::filesystem::path scenePath = projectRoot.MainScenePath(); + std::filesystem::create_directories(scenePath.parent_path()); + + Scene scene("Main"); + GameObject* target = scene.CreateGameObject("Target"); + ASSERT_NE(target, nullptr); + ASSERT_NE(target->GetTransform(), nullptr); + target->GetTransform()->SetPosition(targetPosition); + scene.Save(scenePath.string()); +} + +UIInputEvent MakePointerEvent( + UIInputEventType type, + float x, + float y, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +UIInputEvent MakeWheelEvent(float x, float y, float wheelDelta) { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerWheel; + event.position = UIPoint(x, y); + event.wheelDelta = wheelDelta; + return event; +} + +UIEditorWorkspaceComposeState BuildSceneComposeState( + const UIEditorViewportInputBridgeState& inputBridgeState) { + UIEditorWorkspaceComposeState composeState = {}; + UIEditorWorkspacePanelPresentationState panelState = {}; + panelState.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId); + panelState.viewportShellState.inputBridgeState = inputBridgeState; + composeState.panelStates.push_back(std::move(panelState)); + return composeState; +} + +UIEditorWorkspaceComposeFrame BuildSceneComposeFrame( + const ::XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame& inputFrame, + const UIRect& inputRect, + const UISize& requestedViewportSize) { + UIEditorWorkspaceComposeFrame composeFrame = {}; + UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; + viewportFrame.panelId = std::string(::XCEngine::UI::Editor::App::kScenePanelId); + viewportFrame.viewportShellFrame.inputFrame = inputFrame; + viewportFrame.viewportShellFrame.requestedViewportSize = requestedViewportSize; + viewportFrame.viewportShellFrame.slotLayout.inputRect = inputRect; + composeFrame.viewportFrames.push_back(std::move(viewportFrame)); + return composeFrame; +} + +const EditorSceneComponentDescriptor* FindComponentDescriptor( + const std::vector& descriptors, + std::string_view typeName) { + for (const EditorSceneComponentDescriptor& descriptor : descriptors) { + if (descriptor.typeName == typeName) { + return &descriptor; + } + } + + return nullptr; +} + +TEST(SceneViewportRuntimeTests, ApplySceneViewportCameraInputUpdatesCameraTransform) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + auto* camera = runtime.GetSceneViewCamera(); + ASSERT_NE(camera, nullptr); + auto* transform = camera->GetGameObject()->GetTransform(); + ASSERT_NE(transform, nullptr); + + const Math::Vector3 before = transform->GetPosition(); + + SceneViewportCameraInputState input = {}; + input.viewportHeight = 720.0f; + input.zoomDelta = 1.0f; + runtime.ApplySceneViewportCameraInput(input); + + const Math::Vector3 after = transform->GetPosition(); + EXPECT_NE(before.x, after.x); + EXPECT_NE(before.y, after.y); + EXPECT_NE(before.z, after.z); +} + +TEST(SceneViewportRuntimeTests, FocusSceneSelectionRepositionsCameraAroundSelectedObject) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(12.0f, 3.0f, -8.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID())); + + auto* camera = runtime.GetSceneViewCamera(); + ASSERT_NE(camera, nullptr); + auto* transform = camera->GetGameObject()->GetTransform(); + ASSERT_NE(transform, nullptr); + const Math::Vector3 before = transform->GetPosition(); + + ASSERT_TRUE(runtime.FocusSceneSelection()); + + const Math::Vector3 after = transform->GetPosition(); + EXPECT_NE(before.x, after.x); + EXPECT_NE(before.y, after.y); + EXPECT_NE(before.z, after.z); +} + +TEST(SceneViewportRuntimeTests, BuildSceneViewportRenderRequestIncludesSelectedObjectId) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID())); + + const SceneViewportRenderRequest request = + runtime.BuildSceneViewportRenderRequest(); + ASSERT_TRUE(request.IsValid()); + ASSERT_EQ(request.selectedObjectIds.size(), 1u); + EXPECT_EQ(request.selectedObjectIds.front(), target->GetID()); + EXPECT_GT(request.orbitDistance, 0.0f); + EXPECT_FALSE(request.debugSelectionMask); +} + +TEST(SceneViewportRuntimeTests, SelectedComponentsExposeTransformAndAttachedCameraDescriptors) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_NE(target->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID())); + + const std::vector descriptors = + runtime.GetSelectedComponents(); + ASSERT_EQ(descriptors.size(), 2u); + + const auto* transformDescriptor = + FindComponentDescriptor(descriptors, "Transform"); + const auto* cameraDescriptor = + FindComponentDescriptor(descriptors, "Camera"); + ASSERT_NE(transformDescriptor, nullptr); + ASSERT_NE(cameraDescriptor, nullptr); + EXPECT_EQ(transformDescriptor->componentId, "Transform#0"); + EXPECT_FALSE(transformDescriptor->removable); + EXPECT_EQ(cameraDescriptor->componentId, "Camera#0"); + EXPECT_TRUE(cameraDescriptor->removable); +} + +TEST(SceneViewportRuntimeTests, RemoveSelectedComponentDropsRemovableDescriptorButKeepsTransform) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* target = scene->Find("Target"); + ASSERT_NE(target, nullptr); + ASSERT_NE(target->AddComponent<::XCEngine::Components::CameraComponent>(), nullptr); + ASSERT_TRUE(runtime.SetSelection(target->GetID())); + + EXPECT_FALSE(runtime.CanRemoveSelectedComponent("Transform#0")); + EXPECT_TRUE(runtime.CanRemoveSelectedComponent("Camera#0")); + EXPECT_FALSE(runtime.RemoveSelectedComponent("Transform#0")); + ASSERT_TRUE(runtime.RemoveSelectedComponent("Camera#0")); + + const std::vector descriptors = + runtime.GetSelectedComponents(); + ASSERT_EQ(descriptors.size(), 1u); + EXPECT_EQ(descriptors[0].typeName, "Transform"); + EXPECT_EQ(descriptors[0].componentId, "Transform#0"); +} + +TEST(SceneViewportRuntimeTests, TransformSetterApisWriteLocalValuesOnSelectedTransform) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + runtime.EnsureSceneSelection(); + + auto* target = const_cast(runtime.GetSelectedGameObject()); + ASSERT_NE(target, nullptr); + auto* transform = target->GetTransform(); + ASSERT_NE(transform, nullptr); + + ASSERT_TRUE(runtime.SetSelectedTransformLocalPosition( + "Transform#0", + Math::Vector3(8.0f, 9.0f, 10.0f))); + ASSERT_TRUE(runtime.SetSelectedTransformLocalEulerAngles( + "Transform#0", + Math::Vector3(15.0f, 25.0f, 35.0f))); + ASSERT_TRUE(runtime.SetSelectedTransformLocalScale( + "Transform#0", + Math::Vector3(2.0f, 3.0f, 4.0f))); + + const Math::Vector3 position = transform->GetLocalPosition(); + const Math::Quaternion rotation = transform->GetLocalRotation(); + const Math::Vector3 scale = transform->GetLocalScale(); + const Math::Quaternion expectedRotation = + Math::Quaternion::FromEulerAngles( + Math::Vector3(15.0f, 25.0f, 35.0f) * Math::DEG_TO_RAD); + EXPECT_FLOAT_EQ(position.x, 8.0f); + EXPECT_FLOAT_EQ(position.y, 9.0f); + EXPECT_FLOAT_EQ(position.z, 10.0f); + EXPECT_GT(std::abs(rotation.Dot(expectedRotation)), 0.9999f); + EXPECT_FLOAT_EQ(scale.x, 2.0f); + EXPECT_FLOAT_EQ(scale.y, 3.0f); + EXPECT_FLOAT_EQ(scale.z, 4.0f); + EXPECT_TRUE(runtime.CanUndoTransformEdit()); +} + +TEST(SceneViewportRuntimeTests, SelectionStampAdvancesOnSceneSelectionChanges) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + runtime.EnsureSceneSelection(); + + const std::uint64_t initialStamp = runtime.GetSelectionStamp(); + EXPECT_GT(initialStamp, 0u); + + Scene* scene = runtime.GetActiveScene(); + ASSERT_NE(scene, nullptr); + GameObject* secondary = scene->CreateGameObject("Secondary"); + ASSERT_NE(secondary, nullptr); + ASSERT_TRUE(runtime.SetSelection(secondary->GetID())); + const std::uint64_t selectedStamp = runtime.GetSelectionStamp(); + EXPECT_GT(selectedStamp, initialStamp); + + runtime.ClearSelection(); + const std::uint64_t clearedStamp = runtime.GetSelectionStamp(); + EXPECT_GT(clearedStamp, selectedStamp); + + runtime.ClearSelection(); + EXPECT_GT(runtime.GetSelectionStamp(), clearedStamp); +} + +TEST(SceneViewportRuntimeTests, InspectorSelectionResolverFollowsLatestSelectionDomain) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(4.0f, 5.0f, 6.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + runtime.EnsureSceneSelection(); + ASSERT_TRUE(runtime.HasSceneSelection()); + EXPECT_EQ( + ResolveInspectorSelectionSource(EditorSession{}, runtime), + InspectorSelectionSource::Scene); + const InspectorSubject sceneSubject = + BuildInspectorSubject(EditorSession{}, runtime); + EXPECT_EQ(sceneSubject.kind, InspectorSubjectKind::SceneObject); + EXPECT_EQ(sceneSubject.source, InspectorSelectionSource::Scene); + EXPECT_EQ(sceneSubject.sceneObject.displayName, "Target"); + + EditorSession session = {}; + session.selection.kind = EditorSelectionKind::ProjectItem; + session.selection.itemId = "asset:scene"; + session.selection.displayName = "Main"; + session.selection.absolutePath = projectRoot.MainScenePath(); + session.selection.stamp = GenerateEditorSelectionStamp(); + EXPECT_EQ( + ResolveInspectorSelectionSource(session, runtime), + InspectorSelectionSource::Project); + const InspectorSubject projectSubject = + BuildInspectorSubject(session, runtime); + EXPECT_EQ(projectSubject.kind, InspectorSubjectKind::ProjectAsset); + EXPECT_EQ(projectSubject.source, InspectorSelectionSource::Project); + EXPECT_EQ(projectSubject.projectAsset.selection.itemId, "asset:scene"); + + runtime.ClearSelection(); + EXPECT_EQ( + ResolveInspectorSelectionSource(session, runtime), + InspectorSelectionSource::None); + EXPECT_EQ( + BuildInspectorSubject(session, runtime).kind, + InspectorSubjectKind::None); + + runtime.EnsureSceneSelection(); + EXPECT_EQ( + ResolveInspectorSelectionSource(session, runtime), + InspectorSelectionSource::Scene); + + session.selection = {}; + session.selection.stamp = GenerateEditorSelectionStamp(); + EXPECT_EQ( + ResolveInspectorSelectionSource(session, runtime), + InspectorSelectionSource::None); +} + +TEST(SceneViewportRuntimeTests, RightMouseDragRotatesSceneCameraThroughViewportController) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + auto* camera = runtime.GetSceneViewCamera(); + ASSERT_NE(camera, nullptr); + auto* transform = camera->GetGameObject()->GetTransform(); + ASSERT_NE(transform, nullptr); + const Math::Vector3 beforeForward = transform->GetForward(); + + SceneViewportController controller = {}; + ViewportHostService viewportHostService = {}; + UIEditorViewportInputBridgeState inputBridgeState = {}; + const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f); + const UISize viewportSize(640.0f, 360.0f); + + const auto pressFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + 220.0f, + 180.0f, + UIPointerButton::Right) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(pressFrame, inputRect, viewportSize)); + + const auto dragFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 280.0f, 220.0f) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(dragFrame, inputRect, viewportSize)); + + const Math::Vector3 afterForward = transform->GetForward(); + EXPECT_NE(beforeForward.x, afterForward.x); + EXPECT_NE(beforeForward.y, afterForward.y); + EXPECT_NE(beforeForward.z, afterForward.z); +} + +TEST(SceneViewportRuntimeTests, MoveRightInputMovesSceneCameraTowardPositiveCameraRight) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + auto* camera = runtime.GetSceneViewCamera(); + ASSERT_NE(camera, nullptr); + auto* transform = camera->GetGameObject()->GetTransform(); + ASSERT_NE(transform, nullptr); + const Math::Vector3 before = transform->GetPosition(); + + SceneViewportCameraInputState input = {}; + input.viewportHeight = 720.0f; + input.deltaTime = 1.0f; + input.moveRight = 1.0f; + runtime.ApplySceneViewportCameraInput(input); + + const Math::Vector3 after = transform->GetPosition(); + EXPECT_GT(after.x, before.x); +} + +TEST(SceneViewportRuntimeTests, MiddleMouseDragPansSceneCameraWithGrabSemantics) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + + auto* camera = runtime.GetSceneViewCamera(); + ASSERT_NE(camera, nullptr); + auto* transform = camera->GetGameObject()->GetTransform(); + ASSERT_NE(transform, nullptr); + const Math::Vector3 before = transform->GetPosition(); + + SceneViewportController controller = {}; + ViewportHostService viewportHostService = {}; + UIEditorViewportInputBridgeState inputBridgeState = {}; + const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f); + const UISize viewportSize(640.0f, 360.0f); + + const auto pressFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + 220.0f, + 180.0f, + UIPointerButton::Middle) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(pressFrame, inputRect, viewportSize)); + + const auto dragFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 280.0f, 180.0f) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(dragFrame, inputRect, viewportSize)); + + const Math::Vector3 after = transform->GetPosition(); + EXPECT_LT(after.x, before.x); +} + +TEST(SceneViewportRuntimeTests, MouseWheelUsesSingleNotchNormalizationForSceneZoom) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + const float beforeDistance = + runtime.BuildSceneViewportRenderRequest().orbitDistance; + + SceneViewportController controller = {}; + ViewportHostService viewportHostService = {}; + UIEditorViewportInputBridgeState inputBridgeState = {}; + const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f); + const UISize viewportSize(640.0f, 360.0f); + + const auto hoverFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(hoverFrame, inputRect, viewportSize)); + + const auto wheelFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakeWheelEvent(220.0f, 180.0f, 120.0f) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(wheelFrame, inputRect, viewportSize)); + + const float afterDistance = + runtime.BuildSceneViewportRenderRequest().orbitDistance; + EXPECT_LT(afterDistance, beforeDistance); + EXPECT_GT(afterDistance, 4.0f); +} + +TEST(SceneViewportRuntimeTests, ToolShortcutSwitchesFocusedSceneViewportIntoTranslateMode) { + ScopedSceneManagerReset reset = {}; + TemporaryProjectRoot projectRoot = {}; + SaveMainScene(projectRoot, Math::Vector3(0.0f, 0.0f, 0.0f)); + + EditorSceneRuntime runtime = {}; + ASSERT_TRUE(runtime.Initialize(projectRoot.Root())); + runtime.SetToolMode(SceneToolMode::View); + EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::View); + + SceneViewportController controller = {}; + ViewportHostService viewportHostService = {}; + UIEditorViewportInputBridgeState inputBridgeState = {}; + const UIRect inputRect(100.0f, 80.0f, 640.0f, 360.0f); + const UISize viewportSize(640.0f, 360.0f); + + const auto focusFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + MakePointerEvent(UIInputEventType::PointerMove, 220.0f, 180.0f), + MakePointerEvent( + UIInputEventType::PointerButtonDown, + 220.0f, + 180.0f, + UIPointerButton::Left), + MakePointerEvent( + UIInputEventType::PointerButtonUp, + 220.0f, + 180.0f, + UIPointerButton::Left) + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(focusFrame, inputRect, viewportSize)); + + const auto shortcutFrame = UpdateUIEditorViewportInputBridge( + inputBridgeState, + inputRect, + { + UIInputEvent { + .type = UIInputEventType::KeyDown, + .position = UIPoint(220.0f, 180.0f), + .keyCode = static_cast(KeyCode::W) + } + }); + controller.Update( + runtime, + viewportHostService, + BuildSceneComposeState(inputBridgeState), + BuildSceneComposeFrame(shortcutFrame, inputRect, viewportSize)); + + EXPECT_EQ(runtime.GetToolMode(), SceneToolMode::Translate); +} + +} // namespace +} // namespace XCEngine::UI::Editor::App