#nullable enable using System.Diagnostics; using ImGuiNET; using T3.Core.Operator; using T3.Core.Operator.Interfaces; using T3.Core.Operator.Slots; using T3.Core.Resource; using T3.Core.Utils; using T3.Core.Utils.Geometry; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows; using T3.Editor.UiModel; using T3.Editor.UiModel.Commands; using T3.Editor.UiModel.Commands.Graph; using Color = T3.Core.DataTypes.Vector.Color; using GraphicsMath = T3.Core.Utils.Geometry.GraphicsMath; using Plane = System.Numerics.Plane; using Quaternion = System.Numerics.Quaternion; using Vector2 = System.Numerics.Vector2; using Vector4 = System.Numerics.Vector4; using Ray = T3.Core.Utils.Geometry.Ray; // ReSharper disable RedundantNameQualifier namespace T3.Editor.Gui.Interaction.TransformGizmos; /** * Handles the interaction with 3d-gizmos for operators selected in the graph. */ [HelpUiID("Gizmo")] internal static class TransformGizmoHandling { #region Setup and callback public static bool IsDragging => _draggedGizmoPart != GizmoParts.None; public static void RegisterSelectedTransformable(SymbolUi.Child node, ITransformable transformable) { if (_selectedTransformables.Contains(transformable)) return; transformable.TransformCallback = TransformCallback; _selectedTransformables.Add(transformable); } public static void ClearDeselectedTransformableNode(ITransformable transformable) { if (_selectedTransformables.Contains(transformable)) { Log.Warning("trying to deselect an unregistered transformable?"); return; } transformable.TransformCallback = null; _selectedTransformables.Remove(transformable); } public static void ClearSelectedTransformables() { foreach (var selectedTransformable in _selectedTransformables) { selectedTransformable.TransformCallback = null; } _selectedTransformables.Clear(); } /// /// We need the foreground draw list at the moment when the output texture is drawn to the to output view... /// public static void SetDrawList(ImDrawListPtr drawList) { _drawList = drawList; _isDrawListValid = true; if (_dragNotStopped) { if (!ImGui.IsMouseDown(ImGuiMouseButton.Left)) { Log.Warning("Gizmo Drag not correctly stopped?"); _draggedGizmoPart = GizmoParts.None; } _dragNotStopped = false; } else if (_draggedGizmoPart != GizmoParts.None && !ImGui.IsMouseDown(ImGuiMouseButton.Left)) { _dragNotStopped = true; } } // /// /// In certain situations the release is not correctly handled during the update. /// In SetDrawList /// private static bool _dragNotStopped; public static void RestoreDrawList() { _isDrawListValid = false; } private static Vector3 _selectedCenter; public static Vector3 GetLatestSelectionCenter() { if (_selectedTransformables.Count == 0) return Vector3.Zero; return _selectedCenter; } /// /// Called from operators during update call /// public static void TransformCallback(Instance instance, EvaluationContext context) { if (!_isDrawListValid) { Log.Warning("can't draw gizmo without initialized draw list"); return; } if (instance is not ITransformable tmp) return; _instance = instance; _transformable = tmp; if (!_selectedTransformables.Contains(_transformable)) { Log.Warning("transform-callback from non-selected node?" + _transformable); return; } if (context.ShowGizmos == GizmoVisibility.Off) { return; } _objectToWorld = context.ObjectToWorld; _objectToClipSpace = context.ObjectToWorld * context.WorldToCamera * context.CameraToClipSpace; PrepareCalculations(context); if (_canvas == null) return; var gizmoScale = CalcGizmoScale(context, _localToObject, _viewport.Width, _viewport.Height, 45f, UserSettings.Config.GizmoSize); _centerPadding = 0.2f * gizmoScale / _canvas.Scale.X; _gizmoLength = 2f * gizmoScale / _canvas.Scale.Y; _planeGizmoSize = 0.5f * gizmoScale / _canvas.Scale.X; //var lineThickness = 2; // Handle different gizmo modes switch (context.TransformGizmoMode) { case TransformGizmoModes.Move: if (_transformable.TranslationInput != null) { HandlePositionGizmos(); } break; case TransformGizmoModes.Scale: if (_transformable.ScaleInput != null) { HandleScaleGizmos(); } break; case TransformGizmoModes.Rotate: if (_transformable.RotationInput != null) { HandleRotationGizmos(context); } break; } } // Extract position gizmo handling into separate method private static bool HandlePositionGizmos() { var isHoveringSomething = HandleDragOnAxis(Vector3.UnitX, Color.Red, GizmoParts.PositionXAxis); isHoveringSomething |= HandleDragOnAxis(Vector3.UnitY, Color.Green, GizmoParts.PositionYAxis); isHoveringSomething |= HandleDragOnAxis(Vector3.UnitZ, Color.Blue, GizmoParts.PositionZAxis); isHoveringSomething |= HandleDragOnPlane(Vector3.UnitX, Vector3.UnitY, Color.Blue, GizmoParts.PositionOnXyPlane); isHoveringSomething |= HandleDragOnPlane(Vector3.UnitX, Vector3.UnitZ, Color.Green, GizmoParts.PositionOnXzPlane); isHoveringSomething |= HandleDragOnPlane(Vector3.UnitY, Vector3.UnitZ, Color.Red, GizmoParts.PositionOnYzPlane); isHoveringSomething |= HandleDragInScreenSpace(); return isHoveringSomething; } private static void PrepareCalculations(EvaluationContext context) { Debug.Assert(_transformable != null); // Terminology of the matrices: // objectToClipSpace means in this context the transform without application of the ITransformable values. These are // named 'local'. So localToObject is the matrix of applying the ITransformable values and localToClipSpace to transform // points from the local system (including trans/rot of ITransformable) to the projected space. Scale is ignored for // local here as the local values are only used for drawing, and therefore we don't want to draw anything scaled by this values. _mousePosInScreen = ImGui.GetIO().MousePos; var translation = TryGetVectorFromInput(_transformable.TranslationInput); var center = Vector3.TransformNormal(translation, _objectToWorld); _selectedCenter = center; _localToObject = GraphicsMath.CreateTransformationMatrix( scalingCenter: Vector3.Zero, scalingRotation: Quaternion.Identity, scaling: Vector3.One, rotationCenter: Vector3.Zero, rotation: Quaternion.Identity, // no rotation here translation: translation); _localToClipSpace = _localToObject * _objectToClipSpace; // Camera looks along negative Z in view space // Camera looks along negative Z in world space var viewDirWorld = -Vector3.UnitZ; // Bring to object space _viewDirLocal = Vector3.TransformNormal(viewDirWorld, _initialObjectToLocal); _originInClipSpace = GraphicsMath.TransformCoordinate(translation, _objectToClipSpace); // Compute gizmo center in world space _initialOriginWorld = Vector3.Transform(_initialOrigin, _objectToWorld); // Don't draw gizmo behind camera (view plane) _renderGizmo = Math.Abs(_originInClipSpace.Z) <= 1 && Math.Abs(_originInClipSpace.X) <= 2 && Math.Abs(_originInClipSpace.Y) <= 2; var viewports = ResourceManager.Device.ImmediateContext.Rasterizer.GetViewports(); _viewport = viewports[0]; var originInViewport = new Vector2(_viewport.Width * (_originInClipSpace.X * 0.5f + 0.5f), _viewport.Height * (1.0f - (_originInClipSpace.Y * 0.5f + 0.5f))); _canvas = ImageOutputCanvas.Current; if (_canvas == null) return; var originInCanvas = _canvas.TransformDirection(originInViewport); _topLeftOnScreen = _canvas.TransformPosition(System.Numerics.Vector2.Zero); _originInScreen = _topLeftOnScreen + originInCanvas; Matrix4x4.Invert(_localToObject, out _initialObjectToLocal); } #endregion #region scale ---------------------------------------------------------------- // New method to handle scale gizmos private static bool HandleScaleGizmos() { var isHoveringSomething = HandleAndDrawScaleDragOnAxis(Vector3.UnitX, Color.Red, GizmoParts.ScaleXAxis); isHoveringSomething |= HandleAndDrawScaleDragOnAxis(Vector3.UnitY, Color.Green, GizmoParts.ScaleYAxis); isHoveringSomething |= HandleAndDrawScaleDragOnAxis(Vector3.UnitZ, Color.Blue, GizmoParts.ScaleZAxis); isHoveringSomething |= HandleUniformScale(); return isHoveringSomething; } private static bool HandleAndDrawScaleDragOnAxis(Vector3 gizmoAxis, Color color, GizmoParts mode) { var axisStartInScreen = LocalPosToScreenPos(gizmoAxis * _centerPadding); var deltaScaleForAxis = (_draggedGizmoPart == GizmoParts.ScaleUniform || mode == _draggedGizmoPart) ? _deltaScaleFactor : 1; var axisEndInScreen = LocalPosToScreenPos(gizmoAxis * _gizmoLength * deltaScaleForAxis); // Draw scale handle as a cube at the end of the axis var handleSize = 12f; var handleRect = new Vector2(handleSize, handleSize); var handleCenter = axisEndInScreen; var handleMin = handleCenter - handleRect * 0.5f; var handleMax = handleCenter + handleRect * 0.5f; var isHovering = false; if (!IsDragging) { // Check if mouse is over the handle isHovering = (_mousePosInScreen.X >= handleMin.X && _mousePosInScreen.X <= handleMax.X && _mousePosInScreen.Y >= handleMin.Y && _mousePosInScreen.Y <= handleMax.Y); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartScaleDragging(mode); } } else if (_draggedGizmoPart == mode && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; UpdateScaleDragging(gizmoAxis); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompleteScaleDragging(); } } if (_renderGizmo) { // Draw axis line _drawList.AddLine(axisStartInScreen, axisEndInScreen, color, 2 * (isHovering ? 3 : 1)); // Draw scale handle (cube) var handleColor = color; handleColor.Rgba.W = isHovering ? 1.0f : 0.8f; _drawList.AddRectFilled(handleMin, handleMax, handleColor); _drawList.AddRect(handleMin, handleMax, Color.White, 0, ImDrawFlags.None, 1); } return isHovering; } // New method to handle uniform scaling private static bool HandleUniformScale() { const float handleSize = 12f; var handleRect = new Vector2(handleSize, handleSize); var handleCenter = _originInScreen; var handleMin = handleCenter - handleRect * 0.5f; var handleMax = handleCenter + handleRect * 0.5f; var isHovering = false; if (!IsDragging) { isHovering = (_mousePosInScreen.X >= handleMin.X && _mousePosInScreen.X <= handleMax.X && _mousePosInScreen.Y >= handleMin.Y && _mousePosInScreen.Y <= handleMax.Y); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartScaleDragging(GizmoParts.ScaleUniform); } } else if (_draggedGizmoPart == GizmoParts.ScaleUniform && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; UpdateUniformScaleDragging(); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompleteScaleDragging(); } } if (_renderGizmo) { var color = UiColors.StatusAnimated; color.Rgba.W = isHovering ? 1.0f : 0.6f; _drawList.AddRectFilled(handleMin, handleMax, color); _drawList.AddRect(handleMin, handleMax, Color.White, 0, ImDrawFlags.None, 2); } return isHovering; } // New method to start scale dragging private static void StartScaleDragging(GizmoParts mode) { Debug.Assert(_instance?.Parent != null); Debug.Assert(_transformable != null); _draggedGizmoPart = mode; _inputCommandInFlight = new ChangeInputValueCommand(_instance.Parent.Symbol, _instance.SymbolChildId, _transformable.ScaleInput.Input, _transformable.ScaleInput.Input.Value); _draggedTransformable = _transformable; _dragInteractionWindowId = ImGui.GetID(""); _initialOffsetToOrigin = _mousePosInScreen - _originInScreen; _initialScale = TryGetVectorFromInput(_transformable.ScaleInput, 1f); var rayInObject = GetPickRayInObject(_mousePosInScreen); var rayInLocal = rayInObject; rayInLocal.Direction = Vector3.TransformNormal(rayInObject.Direction, _initialObjectToLocal); rayInLocal.Origin = Vector3.Transform(rayInObject.Origin, _initialObjectToLocal); _plane = GetPlaneForDragMode(mode, rayInObject.Direction, _initialOrigin); if (!_plane.Intersects(rayInLocal, out _initialIntersectionPoint)) { _plane.D = -_plane.D; if (!_plane.Intersects(rayInLocal, out _initialIntersectionPoint)) { _plane.Normal = Vector3.Zero; Log.Debug($"Couldn't intersect pick ray with gizmo axis plane."); } } } // New method to update scale dragging private static void UpdateScaleDragging(Vector3 axis) { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); var rayInObject = GetPickRayInObject(_mousePosInScreen); var rayInLocal = rayInObject; rayInLocal.Direction = Vector3.TransformNormal(rayInObject.Direction, _initialObjectToLocal); rayInLocal.Origin = Vector3.Transform(rayInObject.Origin, _initialObjectToLocal); if (_plane.Normal != Vector3.Zero && _plane.Intersects(rayInLocal, out Vector3 intersectionPoint)) { var deltaInLocal = intersectionPoint - _initialIntersectionPoint; var scaleDelta = Vector3.Dot(deltaInLocal, axis); var newScale = _initialScale + axis * scaleDelta; _deltaScaleFactor = 1 + scaleDelta; // Prevent negative or zero scaling //newScale.X = Math.Max(0.001f, newScale.X); //newScale.Y = Math.Max(0.001f, newScale.Y); //newScale.Z = Math.Max(0.001f, newScale.Z); TrySetVector3ToInput(_transformable.ScaleInput, newScale); _inputCommandInFlight.AssignNewValue(_transformable.ScaleInput.Input.Value); } } /** Uniform */ private static float _deltaScaleFactor = 1; // New method to update uniform scale dragging private static void UpdateUniformScaleDragging() { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); var mouseDelta = _mousePosInScreen - (_originInScreen + _initialOffsetToOrigin); var scaleDelta = 1 + mouseDelta.X * 0.01f; // Use X movement for uniform scaling _deltaScaleFactor = scaleDelta; //var newScale = _initialScale + Vector3.One * scaleDelta; var newScale = _initialScale * scaleDelta; TrySetVector3ToInput(_transformable.ScaleInput, newScale); _inputCommandInFlight.AssignNewValue(_transformable.ScaleInput.Input.Value); } // New method to complete scale dragging private static void CompleteScaleDragging() { Debug.Assert(_inputCommandInFlight != null); UndoRedoStack.Add(_inputCommandInFlight); _inputCommandInFlight = null; _draggedGizmoPart = GizmoParts.None; _draggedTransformable = null; _dragInteractionWindowId = 0; _deltaScaleFactor = 1; _dragNotStopped = false; } #endregion #region translate ---------------------------------------------------- // Returns true if hovered or active private static bool HandleDragOnAxis(Vector3 gizmoAxis, Color color, GizmoParts mode) { var axisStartInScreen = LocalPosToScreenPos(gizmoAxis * _centerPadding); var axisEndInScreen = LocalPosToScreenPos(gizmoAxis * _gizmoLength); var isHovering = false; if (!IsDragging) { isHovering = IsPointOnLine(_mousePosInScreen, axisStartInScreen, axisEndInScreen); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartPositionDragging(mode, true); } } else if (_draggedGizmoPart == mode && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; var rayInObject = GetPickRayInObject(_mousePosInScreen); var rayInLocal = rayInObject; rayInLocal.Direction = Vector3.TransformNormal((rayInObject.Direction), _initialObjectToLocal); rayInLocal.Origin = Vector3.Transform((rayInObject.Origin), _initialObjectToLocal); Vector3 newOrigin = _initialOrigin; // (GetNewOffsetInObject() - _initialOrigin) * (gizmoAxis1 + gizmoAxis2) + _initialOrigin; if (_plane.Normal != Vector3.Zero && _plane.Intersects(rayInLocal, out Vector3 intersectionPoint)) { Vector3 offsetInLocal = (intersectionPoint - _initialIntersectionPoint) * gizmoAxis; var offsetInObject = Vector4.Transform(new Vector4(offsetInLocal, 1), _localToObject); newOrigin = new Vector3(offsetInObject.X, offsetInObject.Y, offsetInObject.Z) / offsetInObject.W; } UpdatePositionDragging(newOrigin); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompletePositionDragging(); } } if (_renderGizmo) _drawList.AddLine(axisStartInScreen, axisEndInScreen, color, 2 * (isHovering ? 3 : 1)); return isHovering; } // Returns true if hovered or active private static bool HandleDragOnPlane(Vector3 gizmoAxis1, Vector3 gizmoAxis2, Color color, GizmoParts mode) { var origin = (gizmoAxis1 + gizmoAxis2) * _centerPadding; Vector2[] pointsOnScreen = { LocalPosToScreenPos(origin), LocalPosToScreenPos(origin + gizmoAxis1 * _planeGizmoSize), LocalPosToScreenPos(origin + (gizmoAxis1 + gizmoAxis2) * _planeGizmoSize), LocalPosToScreenPos(origin + gizmoAxis2 * _planeGizmoSize), }; var isHovering = false; if (!IsDragging) { isHovering = IsPointInQuad(_mousePosInScreen, pointsOnScreen); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartPositionDragging(mode, true); } } else if (_draggedGizmoPart == mode && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; var rayInObject = GetPickRayInObject(_mousePosInScreen); var rayInLocal = rayInObject; rayInLocal.Direction = (Vector3.TransformNormal((rayInObject.Direction), _initialObjectToLocal)); rayInLocal.Origin = (Vector3.Transform((rayInObject.Origin), _initialObjectToLocal)); Vector3 newOrigin = _initialOrigin; // (GetNewOffsetInObject() - _initialOrigin) * (gizmoAxis1 + gizmoAxis2) + _initialOrigin; if (_plane.Normal != Vector3.Zero && _plane.Intersects(rayInLocal, out Vector3 intersectionPoint)) { Vector3 offsetInLocal = (intersectionPoint - _initialIntersectionPoint) * (gizmoAxis1 + gizmoAxis2); var offsetInObject = GraphicsMath.TransformCoordinate(offsetInLocal, _localToObject); newOrigin = offsetInObject; } UpdatePositionDragging(newOrigin); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompletePositionDragging(); } } if (_renderGizmo) { var color2 = color; color2.Rgba.W = isHovering ? 0.4f : 0.2f; _drawList.AddConvexPolyFilled(ref pointsOnScreen[0], 4, color2); } return isHovering; } private static bool HandleDragInScreenSpace() { const float gizmoSize = 4; var screenSquaredMin = _originInScreen - new Vector2(gizmoSize, gizmoSize); var screenSquaredMax = _originInScreen + new Vector2(gizmoSize, gizmoSize); var isHovering = false; if (_draggedGizmoPart == GizmoParts.None) { isHovering = (_mousePosInScreen.X > screenSquaredMin.X && _mousePosInScreen.X < screenSquaredMax.X && _mousePosInScreen.Y > screenSquaredMin.Y && _mousePosInScreen.Y < screenSquaredMax.Y); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartPositionDragging(GizmoParts.PositionInScreenPlane, true); } } else if (_draggedGizmoPart == GizmoParts.PositionInScreenPlane && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompletePositionDragging(); } else { UpdatePositionDragging(GetNewOffsetInObject()); // not internal plane udpate needed here since it's not used at all } } if (_renderGizmo) { var color2 = UiColors.StatusAnimated; color2.Rgba.W = isHovering ? 0.8f : 0.3f; _drawList.AddRectFilled(screenSquaredMin, screenSquaredMax, color2); } return isHovering; } private static void StartPositionDragging(GizmoParts mode, bool createUndo) { Debug.Assert(_instance?.Parent != null); Debug.Assert(_transformable != null); _draggedGizmoPart = mode; if (createUndo) { _inputCommandInFlight = new ChangeInputValueCommand(_instance.Parent.Symbol, _instance.SymbolChildId, _transformable.TranslationInput.Input, _transformable.TranslationInput.Input.Value); } _draggedTransformable = _transformable; _dragInteractionWindowId = ImGui.GetID(""); _initialOffsetToOrigin = _mousePosInScreen - _originInScreen; _initialOrigin = _localToObject.Translation; var rayInObject = GetPickRayInObject(_mousePosInScreen); var rayInLocal = rayInObject; rayInLocal.Direction = Vector3.TransformNormal((rayInObject.Direction), _initialObjectToLocal); rayInLocal.Origin = Vector3.Transform((rayInObject.Origin), _initialObjectToLocal); _plane = GetPlaneForDragMode(mode, (rayInObject.Direction), _initialOrigin); if (mode == GizmoParts.PositionInScreenPlane) { _plane.Normal = Vector3.Zero; } else if (!_plane.Intersects(rayInLocal, out _initialIntersectionPoint)) { _plane.D = -_plane.D; if (!_plane.Intersects(rayInLocal, out _initialIntersectionPoint)) { _plane.Normal = Vector3.Zero; Log.Debug($"Couldn't intersect pick ray with gizmo axis plane."); } } } private static void UpdatePositionDragging(in Vector3 newOrigin) { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); TrySetVector3ToInput(_transformable.TranslationInput, newOrigin); InputValue value = _transformable.TranslationInput.Input.Value; _inputCommandInFlight.AssignNewValue(value); } private static void CompletePositionDragging() { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); UndoRedoStack.Add(_inputCommandInFlight); _inputCommandInFlight = null; _draggedGizmoPart = GizmoParts.None; _draggedTransformable = null; _dragInteractionWindowId = 0; _dragNotStopped = false; } private static Vector3 GetNewOffsetInObject() { Debug.Assert(_canvas != null); Vector2 newOriginInScreen = _mousePosInScreen - _initialOffsetToOrigin; // transform back to object space Matrix4x4.Invert(_objectToClipSpace, out var clipSpaceToObject); var newOriginInCanvas = newOriginInScreen - _topLeftOnScreen; var newOriginInViewport = _canvas.InverseTransformDirection(newOriginInCanvas); var newOriginInClipSpace = new Vector4(2.0f * newOriginInViewport.X / _viewport.Width - 1.0f, -(2.0f * newOriginInViewport.Y / _viewport.Height - 1.0f), _originInClipSpace.Z, 1); var newOriginInObject = Vector4.Transform(newOriginInClipSpace, clipSpaceToObject); Vector3 newTranslation = new Vector3(newOriginInObject.X, newOriginInObject.Y, newOriginInObject.Z) / newOriginInObject.W; return new Vector3(newTranslation.X, newTranslation.Y, newTranslation.Z); } #endregion #region rotate ---------------------------------------------------------- private static bool HandleRotationGizmos(EvaluationContext context) { if (_transformable?.RotationInput == null) return false; var rotation = TryGetVectorFromInput(_transformable.RotationInput); var yaw = rotation.Y.ToRadians(); var pitch = rotation.X.ToRadians(); // Create individual quaternions var yawQ = Quaternion.CreateFromAxisAngle(Vector3.UnitY, yaw); var pitchQ = Quaternion.CreateFromAxisAngle(Vector3.UnitX, pitch); // Gimbal axes var axisYaw = Vector3.UnitY; var axisPitch = Vector3.Transform(Vector3.UnitX, yawQ); var yawPitchQ = Quaternion.Concatenate(pitchQ, yawQ); var axisRoll = Vector3.Transform(Vector3.UnitZ, yawPitchQ); var isHovering = HandleRotationOnAxis(context, axisYaw, Color.Green, GizmoParts.RotationYAxis); isHovering |= HandleRotationOnAxis(context,axisPitch, Color.Red, GizmoParts.RotationXAxis); isHovering |= HandleRotationOnAxis(context,axisRoll, Color.Blue, GizmoParts.RotationZAxis); isHovering |= HandleScreenRotation(); return isHovering; } private static bool HandleRotationOnAxis(EvaluationContext context, Vector3 axis, Color color, GizmoParts mode) { const int circleSegments = 32; var gizmoRadius = _gizmoLength * 0.6f; var circlePoints = new Vector2[circleSegments]; // Create two perpendicular vectors to form the circle plane Vector3 tangent1, tangent2; if (MathF.Abs(Vector3.Dot(axis, Vector3.UnitZ)) > 0.999f) { tangent1 = Vector3.UnitX; tangent2 = Vector3.UnitY; } else { tangent1 = Vector3.Normalize(Vector3.Cross(axis, Vector3.UnitZ)); tangent2 = Vector3.Normalize(Vector3.Cross(axis, tangent1)); } // Calculate circle points in 3D space then project to screen for (int i = 0; i < circleSegments; i++) { float angle = (float)i / circleSegments * MathF.PI * 2; Vector3 point3D = tangent1 * MathF.Cos(angle) * gizmoRadius + tangent2 * MathF.Sin(angle) * gizmoRadius; circlePoints[i] = LocalPosToScreenPos(point3D); } var isHovering = false; if (!IsDragging) { isHovering = IsPointNearPolygon(_mousePosInScreen, circlePoints); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartRotationDragging(mode, axis); } } else if (_draggedGizmoPart == mode && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; UpdateRotationDragging(_draggedRotationAxis); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompleteRotationDragging(); } } if (_renderGizmo) { Vector3[] points3D = new Vector3[circleSegments]; Vector2[] points2D = new Vector2[circleSegments]; // Recompute view vector every frame Matrix4x4.Invert(context.WorldToCamera, out var cameraToWorld); Vector3 cameraWorldPos = Vector3.Transform(Vector3.Zero, cameraToWorld); Vector3 dirWorld = Vector3.Normalize(_initialOriginWorld - cameraWorldPos); _viewDirLocal = Vector3.TransformNormal(dirWorld, _initialObjectToLocal); for (int i = 0; i < circleSegments; i++) { float angle = i / (float)circleSegments * MathF.PI * 2; float cos = MathF.Cos(angle); float sin = MathF.Sin(angle); Vector3 p3D = tangent1 * cos * gizmoRadius + tangent2 * sin * gizmoRadius; points3D[i] = p3D; points2D[i] = LocalPosToScreenPos(p3D); } for (int i = 0; i < circleSegments; i++) { int next = (i + 1) % circleSegments; Vector3 midPoint3D = (points3D[i] + points3D[next]) * 0.5f; float facing = Vector3.Dot(midPoint3D, _viewDirLocal); float alpha = facing >= 0 ? 0.3f : 1.0f; var segColor = color; segColor.Rgba.W = alpha; _drawList.AddLine(points2D[i], points2D[next], segColor, isHovering ? 3 : 2); } if (_draggedGizmoPart == mode && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { Vector3 initialWorldPos = _initialRotationVector * gizmoRadius * 1.05f; Vector3 currentWorldPos = _currentRotationVector * gizmoRadius * 1.05f; Vector2 initialScreenPos = LocalPosToScreenPos(initialWorldPos); Vector2 currentScreenPos = LocalPosToScreenPos(currentWorldPos); float markerRadius = 5.0f; _drawList.AddCircleFilled(initialScreenPos, markerRadius*0.6f, color); _drawList.AddCircleFilled(currentScreenPos, markerRadius, color); } } return isHovering; } private static Vector3 _initialOriginWorld; // Add this method to handle screen space rotation private static bool HandleScreenRotation() { const float outerRadius = 2.0f; const float innerRadius = 1.8f; var outerCirclePoints = new Vector2[32]; var innerCirclePoints = new Vector2[32]; // Calculate circle points in screen space for (int i = 0; i < 32; i++) { float angle = (float)i / 32 * MathF.PI * 2; outerCirclePoints[i] = _originInScreen + new Vector2( MathF.Cos(angle) * outerRadius, MathF.Sin(angle) * outerRadius); innerCirclePoints[i] = _originInScreen + new Vector2( MathF.Cos(angle) * innerRadius, MathF.Sin(angle) * innerRadius); } var isHovering = false; if (!IsDragging) { // Check if mouse is in the ring between inner and outer circles isHovering = IsPointInRing(_mousePosInScreen, _originInScreen, innerRadius, outerRadius); if (isHovering && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { StartRotationDragging(GizmoParts.RotationScreen, Vector3.Zero); } } else if (_draggedGizmoPart == GizmoParts.RotationScreen && _draggedTransformable == _transformable && _dragInteractionWindowId == ImGui.GetID("")) { isHovering = true; UpdateScreenRotationDragging(); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { CompleteRotationDragging(); } } if (_renderGizmo) { // Draw screen rotation ring var color = UiColors.StatusAnimated; color.Rgba.W = isHovering ? 0.8f : 0.6f; for (int i = 0; i < 32; i++) { int next = (i + 1) % 32; _drawList.AddQuad( outerCirclePoints[i], outerCirclePoints[next], innerCirclePoints[next], innerCirclePoints[i], color); } } return isHovering; } private static void StartRotationDragging(GizmoParts mode, Vector3 axis) { Debug.Assert(_instance?.Parent != null); Debug.Assert(_transformable != null); _draggedGizmoPart = mode; _inputCommandInFlight = new ChangeInputValueCommand( _instance.Parent.Symbol, _instance.SymbolChildId, _transformable.RotationInput.Input, _transformable.RotationInput.Input.Value); _draggedTransformable = _transformable; _dragInteractionWindowId = ImGui.GetID(""); _initialOrigin = _localToObject.Translation; _initialRotation = TryGetVectorFromInput(_transformable.RotationInput); _initialMousePos = _mousePosInScreen; _draggedRotationAxis = axis; // Get initial mouse pick ray in object space var ray = GetPickRayInObject(_mousePosInScreen); // Intersect with rotation plane var plane = new Plane(axis, -Vector3.Dot(axis, _initialOrigin)); if (!PlaneIntersectRay(plane, ray, out var intersection)) { Log.Warning("Couldn't intersect pick ray with rotation plane."); _initialRotationVector = Vector3.UnitX; // fallback return; } // Vector from gizmo origin to intersection, normalized _initialRotationVector = Vector3.Normalize(intersection - _initialOrigin); } private static void UpdateRotationDragging(Vector3 axis) { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); var ray = GetPickRayInObject(_mousePosInScreen); var plane = new Plane(axis, -Vector3.Dot(axis, _initialOrigin)); if (!PlaneIntersectRay(plane, ray, out var intersection)) return; var currentVector = Vector3.Normalize(intersection - _initialOrigin); _currentRotationVector = currentVector; // Compute signed angle between initial and current vectors float angleRad = MathF.Atan2( Vector3.Dot(axis, Vector3.Cross(_initialRotationVector, currentVector)), Vector3.Dot(_initialRotationVector, currentVector) ); float angleDeg = angleRad * (180f / MathF.PI); Vector3 newRotation = _initialRotation; var snap = ImGui.GetIO().KeyCtrl; switch (_draggedGizmoPart) { case GizmoParts.RotationXAxis: newRotation.X += angleDeg; if (snap) newRotation.X = MathF.Round(newRotation.X/15)*15; break; case GizmoParts.RotationYAxis: newRotation.Y += angleDeg; if(snap) newRotation.Y = MathF.Round(newRotation.Y/15)*15; break; case GizmoParts.RotationZAxis: newRotation.Z += angleDeg; if(snap) newRotation.Z = MathF.Round(newRotation.Z/15)*15; break; default: Log.Debug("No valid gizmo part for rotation."); break; } TrySetVector3ToInput(_transformable.RotationInput, newRotation); _inputCommandInFlight.AssignNewValue(_transformable.RotationInput.Input.Value); } private static bool PlaneIntersectRay(Plane plane, Ray ray, out Vector3 intersection) { float denom = Vector3.Dot(plane.Normal, ray.Direction); if (MathF.Abs(denom) < 1e-6f) { intersection = Vector3.Zero; return false; } float t = -(Vector3.Dot(plane.Normal, ray.Origin) + plane.D) / denom; intersection = ray.Origin + ray.Direction * t; return t >= 0; } private static void UpdateScreenRotationDragging() { Debug.Assert(_inputCommandInFlight != null); Debug.Assert(_transformable != null); // For screen rotation, use circular motion around the gizmo center Vector2 initialDir = Vector2.Normalize(_initialMousePos - _originInScreen); Vector2 currentDir = Vector2.Normalize(_mousePosInScreen - _originInScreen); // Calculate the angle between the two directions float dot = Vector2.Dot(initialDir, currentDir); float det = initialDir.X * currentDir.Y - initialDir.Y * currentDir.X; float angleDelta = MathF.Atan2(det, dot) * (180f / MathF.PI); // Apply to all axes equally for screen rotation, or choose one axis Vector3 newRotation = _initialRotation; newRotation.Z += angleDelta; // Apply to Z-axis for screen rotation TrySetVector3ToInput(_transformable.RotationInput, newRotation); _inputCommandInFlight.AssignNewValue(_transformable.RotationInput.Input.Value); } private static void CompleteRotationDragging() { Debug.Assert(_inputCommandInFlight != null); UndoRedoStack.Add(_inputCommandInFlight); _inputCommandInFlight = null; _draggedGizmoPart = GizmoParts.None; _draggedTransformable = null; _dragInteractionWindowId = 0; _dragNotStopped = false; } // Add this helper method to check if point is near a polygon private static bool IsPointNearPolygon(Vector2 point, Vector2[] polygon, float threshold = 5f) { for (int i = 0; i < polygon.Length; i++) { int next = (i + 1) % polygon.Length; if (IsPointOnLine(point, polygon[i], polygon[next], threshold)) { return true; } } return false; } // Add this helper method to check if point is in a ring private static bool IsPointInRing(Vector2 point, Vector2 center, float innerRadius, float outerRadius) { float distanceSquared = Vector2.DistanceSquared(point, center); return distanceSquared >= innerRadius * innerRadius && distanceSquared <= outerRadius * outerRadius; } #endregion #region setting helpers ----------------------------------------- private static Vector3 TryGetVectorFromInput(IInputSlot input, float defaultValue = 0f) { return input switch { InputSlot vec3Input => vec3Input.Value, InputSlot vec2Input => new Vector3(vec2Input.Value.X, vec2Input.Value.Y, defaultValue), _ => new Vector3(defaultValue, defaultValue, defaultValue) }; } private static void TrySetVector3ToInput(IInputSlot input, Vector3 vector3) { switch (input) { case InputSlot vec3Input: vec3Input.SetTypedInputValue(vector3); break; case InputSlot vec2Input: vec2Input.SetTypedInputValue(new Vector2(vector3.X, vector3.Y)); break; } } #endregion #region math private static Ray GetPickRayInObject(Vector2 posInScreen) { Debug.Assert(_canvas != null); Matrix4x4.Invert(_objectToClipSpace, out var clipSpaceToObject); var newOriginInCanvas = posInScreen - _topLeftOnScreen; var newOriginInViewport = _canvas.InverseTransformDirection(newOriginInCanvas); float xInClipSpace = 2.0f * newOriginInViewport.X / _viewport.Width - 1.0f; float yInClipSpace = -(2.0f * newOriginInViewport.Y / _viewport.Height - 1.0f); var rayStartInClipSpace = new Vector3(xInClipSpace, yInClipSpace, 0); var rayStartInObject = GraphicsMath.TransformCoordinate(rayStartInClipSpace, clipSpaceToObject); var rayEndInClipSpace = new Vector3(xInClipSpace, yInClipSpace, 1); var rayEndInObject = GraphicsMath.TransformCoordinate(rayEndInClipSpace, clipSpaceToObject); var rayDir = (rayEndInObject - rayStartInObject); //rayDir = Vector3.Normalize(rayDir); return new Ray(rayStartInObject, rayDir); } // Calculates the scale for a gizmo based on the distance to the cam private static float CalcGizmoScale(EvaluationContext context, in Matrix4x4 localToObject, float width, float height, float fovInDegree, float gizmoSize) { var localToCamera = localToObject * context.ObjectToWorld * context.WorldToCamera; var distance = localToCamera.Translation.Length(); // distance of local origin to cam var denom = Math.Sqrt(width * width + height * height) * Math.Tan(fovInDegree.ToRadians()); return (float)Math.Max(0.0001, (distance / denom) * gizmoSize); } private static Vector2 LocalPosToScreenPos(Vector3 posInLocal) { Debug.Assert(_canvas != null); var homogenousPosInLocal = new Vector4(posInLocal.X, posInLocal.Y, posInLocal.Z, 1); Vector4 originInClipSpace = Vector4.Transform(homogenousPosInLocal, _localToClipSpace); Vector3 posInNdc = new Vector3(originInClipSpace.X, originInClipSpace.Y, originInClipSpace.Z) / originInClipSpace.W; var viewports = ResourceManager.Device.ImmediateContext.Rasterizer.GetViewports(); var viewport = viewports[0]; var originInViewport = new Vector2(viewport.Width * (posInNdc.X * 0.5f + 0.5f), viewport.Height * (1.0f - (posInNdc.Y * 0.5f + 0.5f))); var posInCanvas = _canvas.TransformDirection(originInViewport); return _topLeftOnScreen + posInCanvas; } // Update the GetPlaneForDragMode method to handle scale modes private static Plane GetPlaneForDragMode(GizmoParts mode, Vector3 normDir, Vector3 origin) { Vector3 firstAxis, secondAxis; switch (mode) { case GizmoParts.PositionXAxis: case GizmoParts.ScaleXAxis: { firstAxis = Vector3.UnitX; secondAxis = Math.Abs(Vector3.Dot(normDir, Vector3.UnitY)) < Math.Abs(Vector3.Dot(normDir, Vector3.UnitZ)) ? Vector3.UnitY : Vector3.UnitZ; break; } case GizmoParts.PositionYAxis: case GizmoParts.ScaleYAxis: { firstAxis = Vector3.UnitY; secondAxis = Math.Abs(Vector3.Dot(normDir, Vector3.UnitX)) < Math.Abs(Vector3.Dot(normDir, Vector3.UnitZ)) ? Vector3.UnitX : Vector3.UnitZ; break; } case GizmoParts.PositionZAxis: case GizmoParts.ScaleZAxis: { firstAxis = Vector3.UnitZ; secondAxis = Math.Abs(Vector3.Dot(normDir, Vector3.UnitX)) < Math.Abs(Vector3.Dot(normDir, Vector3.UnitY)) ? Vector3.UnitX : Vector3.UnitY; break; } case GizmoParts.PositionOnXyPlane: case GizmoParts.ScaleOnXyPlane: firstAxis = Vector3.UnitX; secondAxis = Vector3.UnitY; break; case GizmoParts.PositionOnXzPlane: case GizmoParts.ScaleOnXzPlane: firstAxis = Vector3.UnitX; secondAxis = Vector3.UnitZ; break; case GizmoParts.PositionOnYzPlane: case GizmoParts.ScaleOnYzPlane: firstAxis = Vector3.UnitY; secondAxis = Vector3.UnitZ; break; case GizmoParts.ScaleUniform: // For uniform scaling, use a plane perpendicular to the view direction firstAxis = Vector3.UnitX; secondAxis = Vector3.UnitY; break; default: Log.Error($"{nameof(GetPlaneForDragMode)}(...) called with wrong GizmoDraggingMode."); firstAxis = Vector3.UnitX; secondAxis = Vector3.UnitY; break; } var point2 = origin + firstAxis; var point3 = origin + secondAxis; return Plane.CreateFromVertices(origin, point2, point3); } private static bool IsPointOnLine(Vector2 point, Vector2 lineStart, Vector2 lineEnd, float threshold = 3) { var rect = new ImRect(lineStart, lineEnd).MakePositive(); rect.Expand(threshold); if (!rect.Contains(point)) return false; var positionOnLine = GetClosestPointOnLine(point, lineStart, lineEnd); return Vector2.Distance(point, positionOnLine) <= threshold; } private static Vector2 GetClosestPointOnLine(Vector2 point, Vector2 lineStart, Vector2 lineEnd) { var v = (lineEnd - lineStart); var vLen = v.Length(); var d = Vector2.Dot(v, point - lineStart) / vLen; return lineStart + v * d / vLen; } private static bool IsPointInTriangle(Vector2 p, Vector2 p0, Vector2 p1, Vector2 p2) { var a = 0.5f * (-p1.Y * p2.X + p0.Y * (-p1.X + p2.X) + p0.X * (p1.Y - p2.Y) + p1.X * p2.Y); var sign = a < 0 ? -1 : 1; var s = (p0.Y * p2.X - p0.X * p2.Y + (p2.Y - p0.Y) * p.X + (p0.X - p2.X) * p.Y) * sign; var t = (p0.X * p1.Y - p0.Y * p1.X + (p0.Y - p1.Y) * p.X + (p1.X - p0.X) * p.Y) * sign; return s > 0 && t > 0 && (s + t) < 2 * a * sign; } private static bool IsPointInQuad(Vector2 p, Vector2[] corners) { return IsPointInTriangle(p, corners[0], corners[1], corners[2]) || IsPointInTriangle(p, corners[0], corners[2], corners[3]); } #endregion #region too many members public enum GizmoParts { None, PositionInScreenPlane, PositionXAxis, PositionYAxis, PositionZAxis, PositionOnXyPlane, PositionOnXzPlane, PositionOnYzPlane, // Scale gizmo parts ScaleXAxis, ScaleYAxis, ScaleZAxis, ScaleUniform, ScaleOnXyPlane, ScaleOnXzPlane, ScaleOnYzPlane, // Rotation gizmo parts RotationXAxis, RotationYAxis, RotationZAxis, RotationScreen, } #endregion private static Instance? _instance; private static ITransformable? _transformable; private static GizmoParts _draggedGizmoPart = GizmoParts.None; private static ITransformable? _draggedTransformable; private static ChangeInputValueCommand? _inputCommandInFlight; private static float _centerPadding; private static float _gizmoLength; private static float _planeGizmoSize; private static ImDrawListPtr _drawList = null; private static bool _isDrawListValid; private static uint _dragInteractionWindowId; private static readonly HashSet _selectedTransformables = []; private static SharpDX.Mathematics.Interop.RawViewportF _viewport; private static Vector2 _mousePosInScreen; private static ImageOutputCanvas? _canvas; private static Vector2 _topLeftOnScreen; private static Vector2 _originInScreen; private static Vector3 _originInClipSpace; private static bool _renderGizmo; // Keep values when interaction started private static Vector3 _initialOrigin; private static Vector2 _initialOffsetToOrigin; private static Matrix4x4 _initialObjectToLocal; private static Vector3 _initialIntersectionPoint; private static Plane _plane; private static Matrix4x4 _objectToClipSpace; private static Matrix4x4 _objectToWorld; private static Matrix4x4 _localToObject; private static Matrix4x4 _localToClipSpace; private static Vector3 _viewDirLocal; // Scale gizmo state private static Vector3 _initialScale; private static Vector3 _currentRotationVector; private static Vector3 _initialRotation; private static Vector3 _initialRotationVector; private static Vector3 _draggedRotationAxis; private static Vector2 _initialMousePos; }