tooll3--t3
1349 行
52 KiB
C#
1349 行
52 KiB
C#
#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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// We need the foreground draw list at the moment when the output texture is drawn to the to output view...
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
//
|
|
/// <summary>
|
|
/// In certain situations the release is not correctly handled during the update.
|
|
/// In SetDrawList
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from <see cref="ITransformable"/> operators during update call
|
|
/// </summary>
|
|
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<SharpDX.Mathematics.Interop.RawViewportF>();
|
|
_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<System.Numerics.Vector3> vec3Input => vec3Input.Value,
|
|
InputSlot<System.Numerics.Vector2> 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<System.Numerics.Vector3> vec3Input:
|
|
vec3Input.SetTypedInputValue(vector3);
|
|
break;
|
|
case InputSlot<System.Numerics.Vector2> 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<SharpDX.Mathematics.Interop.RawViewportF>();
|
|
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<ITransformable> _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;
|
|
} |