516 lines
21 KiB
C#
516 lines
21 KiB
C#
using ImGuiNET;
|
|
using T3.Core.Animation;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Editor.Gui.Styling;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
using T3.Editor.UiModel.Commands;
|
|
using T3.Editor.UiModel.Commands.Animation;
|
|
|
|
namespace T3.Editor.Gui.Interaction.WithCurves;
|
|
|
|
internal static class CurvePoint
|
|
{
|
|
public static void Draw(in Guid compositionSymbolId, VDefinition vDef, ScalableCanvas curveEditCanvas, bool isSelected, CurveEditing curveEditing, bool isNeighborOfSelected = false,
|
|
VDefinition? prevKey = null, VDefinition? nextKey = null)
|
|
{
|
|
_drawList = ImGui.GetWindowDrawList();
|
|
_curveEditCanvas = curveEditCanvas;
|
|
_curveEditing = curveEditing;
|
|
_vDef = vDef;
|
|
|
|
// Compute neighbor info for snapping and proportional handle length
|
|
_neighborAngleIn = null;
|
|
_neighborAngleOut = null;
|
|
_segmentWidthIn = 1.0;
|
|
_segmentWidthOut = 1.0;
|
|
|
|
if (prevKey != null)
|
|
{
|
|
_neighborAngleIn = Math.PI / 2 - Math.Atan2(vDef.U - prevKey.U, vDef.Value - prevKey.Value);
|
|
_segmentWidthIn = Math.Abs(vDef.U - prevKey.U);
|
|
}
|
|
if (nextKey != null)
|
|
{
|
|
_neighborAngleOut = Math.PI / 2 - Math.Atan2(vDef.U - nextKey.U, vDef.Value - nextKey.Value);
|
|
_segmentWidthOut = Math.Abs(vDef.U - nextKey.U);
|
|
}
|
|
|
|
var pCenter = _curveEditCanvas.TransformPosition(new Vector2((float)vDef.U, (float)vDef.Value));
|
|
var pTopLeft = pCenter - _controlSizeHalf;
|
|
|
|
if (isSelected)
|
|
{
|
|
UpdateTangentVectors();
|
|
DrawTangentGuides(pCenter);
|
|
DrawTangentHandle(pCenter, TangentSide.In);
|
|
DrawTangentHandle(pCenter, TangentSide.Out);
|
|
}
|
|
else if (isNeighborOfSelected)
|
|
{
|
|
UpdateTangentVectors();
|
|
DrawTangentGuides(pCenter);
|
|
DrawTangentHandle(pCenter, TangentSide.In, dimmed: true);
|
|
DrawTangentHandle(pCenter, TangentSide.Out, dimmed: true);
|
|
}
|
|
|
|
// Keyframe interaction
|
|
ImGui.SetCursorScreenPos(pTopLeft);
|
|
ImGui.InvisibleButton("key" + vDef.UniqueId, _controlSize);
|
|
DrawUtils.DebugItemRect();
|
|
|
|
Icons.DrawIconOnLastItem(isSelected
|
|
? Icon.CurveKeyframeSelected
|
|
: Icon.CurveKeyframe, Color.White);
|
|
|
|
// Cross-view outline: if this same keyframe (by UniqueId) is hovered in the other
|
|
// timeline pane, draw a soft outline here so the link reads visually.
|
|
if (curveEditing?.GetHoveredKeyframeUniqueId() is { } hoveredId && hoveredId == vDef.UniqueId)
|
|
{
|
|
_drawList.AddRect(pTopLeft - Vector2.One,
|
|
pTopLeft + _controlSize + Vector2.One,
|
|
UiColors.ForegroundFull, rounding: 1f, ImDrawFlags.None, 1f);
|
|
}
|
|
|
|
curveEditing?.HandleCurvePointDragging(compositionSymbolId, _vDef, isSelected);
|
|
}
|
|
|
|
private enum TangentSide { In, Out }
|
|
|
|
private static void DrawTangentHandle(Vector2 pCenter, TangentSide side, bool dimmed = false)
|
|
{
|
|
var isIn = side == TangentSide.In;
|
|
var handleOffset = isIn ? _leftTangentInScreen : _rightTangentInScreen;
|
|
var handleCenter = pCenter + handleOffset;
|
|
var isWeighted = _vDef.Weighted;
|
|
|
|
var lineColor = dimmed ? _tangentHandleColor.Fade(0.25f) : _tangentHandleColor.Fade(0.8f);
|
|
var knobColor = dimmed ? UiColors.Text.Fade(0.4f) : UiColors.Text.Fade(0.8f);
|
|
|
|
var buttonId = isIn ? "keyLT" + _vDef.UniqueId : "keyRT" + _vDef.UniqueId;
|
|
|
|
// Scale click area based on handle screen length — shrink when small, disable when tiny
|
|
var handleScreenLength = handleOffset.Length();
|
|
var clickSize = handleScreenLength < MinClickableHandleLength
|
|
? Vector2.One // Too small to click — 1px dummy button
|
|
: Vector2.Lerp(Vector2.One, _tangentHandleSize,
|
|
Math.Clamp((handleScreenLength - MinClickableHandleLength)
|
|
/ (FullClickableHandleLength - MinClickableHandleLength), 0f, 1f));
|
|
|
|
ImGui.SetCursorScreenPos(handleCenter - clickSize * 0.5f);
|
|
ImGui.InvisibleButton(buttonId, clickSize);
|
|
var isHovered = ImGui.IsItemHovered();
|
|
if (isHovered)
|
|
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
|
|
|
|
// Capture drag start offset for relative dragging (avoids initial jump) and open an
|
|
// undo command capturing this keyframe's pre-drag tangent state.
|
|
if (ImGui.IsItemActivated())
|
|
{
|
|
_dragStartOffset = ImGui.GetMousePos() - handleCenter;
|
|
if (_curveEditing != null)
|
|
_tangentDragCommand = new ChangeKeyframesCommand(new[] { _vDef }, _curveEditing.GetCurvesForUndo());
|
|
}
|
|
|
|
// Dragging
|
|
if (ImGui.IsItemActive() && ImGui.IsMouseDragging(0, 0f))
|
|
{
|
|
HandleTangentDrag(pCenter, side);
|
|
// Recompute screen vectors after drag modified angles/tensions
|
|
// so the visual drawn below matches the updated state
|
|
UpdateTangentVectors();
|
|
handleOffset = isIn ? _leftTangentInScreen : _rightTangentInScreen;
|
|
handleCenter = pCenter + handleOffset;
|
|
}
|
|
else if (ImGui.IsItemDeactivated())
|
|
{
|
|
// Commit pending snap conversions on release
|
|
if ((_pendingSnaps & TangentSnaps.Linear) != 0)
|
|
{
|
|
if (_pendingSnapSide == TangentSide.In)
|
|
_vDef.InInterpolation = VDefinition.KeyInterpolation.Linear;
|
|
else
|
|
_vDef.OutInterpolation = VDefinition.KeyInterpolation.Linear;
|
|
|
|
_vDef.Weighted = false;
|
|
}
|
|
|
|
if ((_pendingSnaps & TangentSnaps.DefaultLength) != 0)
|
|
{
|
|
_vDef.Weighted = false;
|
|
}
|
|
|
|
_pendingSnaps = TangentSnaps.None;
|
|
_isDraggingTangent = false;
|
|
_dragStartOffset = Vector2.Zero;
|
|
|
|
// Finalize and push the tangent-edit undo command. Values are already applied to
|
|
// the live VDefinition; StoreCurrentValues captures them as the "Do" state so
|
|
// redo restores them after an Undo.
|
|
if (_tangentDragCommand != null)
|
|
{
|
|
_tangentDragCommand.StoreCurrentValues();
|
|
UndoRedoStack.Add(_tangentDragCommand);
|
|
_tangentDragCommand = null;
|
|
}
|
|
}
|
|
|
|
// Draw handle: square for weighted, circle for unweighted
|
|
var handleColor = isHovered ? UiColors.ForegroundFull : knobColor;
|
|
if (isWeighted)
|
|
{
|
|
_drawList.AddRectFilled(handleCenter - _tangentSizeHalf, handleCenter + _tangentSize, handleColor);
|
|
}
|
|
else
|
|
{
|
|
_drawList.AddCircleFilled(handleCenter, 2.5f * T3Ui.UiScaleFactor, handleColor);
|
|
}
|
|
|
|
_drawList.AddLine(pCenter, handleCenter, lineColor);
|
|
}
|
|
|
|
private static void HandleTangentDrag(Vector2 pCenter, TangentSide side)
|
|
{
|
|
var isIn = side == TangentSide.In;
|
|
var wasWeighted = _vDef.Weighted;
|
|
|
|
// Convert this side to Tangent interpolation mode
|
|
if (isIn)
|
|
_vDef.InInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
else
|
|
_vDef.OutInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
|
|
// Shift unlocks weight permanently; otherwise keep current weighted state
|
|
// We can't use Alt here, because this is already being assigned to panning background. :-(
|
|
// Very unfortunate overlap with disable snapping.
|
|
if (ImGui.GetIO().KeyShift && !wasWeighted)
|
|
_vDef.Weighted = true;
|
|
|
|
// Compute angle from mouse position, adjusted for grab offset to avoid initial jump
|
|
var adjustedMousePos = ImGui.GetMousePos() - _dragStartOffset;
|
|
var vectorInCanvas = _curveEditCanvas.InverseTransformDirection(adjustedMousePos - pCenter);
|
|
var rawAngle = isIn
|
|
? Math.PI / 2 - Math.Atan2(-vectorInCanvas.X, -vectorInCanvas.Y)
|
|
: -Math.PI / 2 - Math.Atan2(vectorInCanvas.X, vectorInCanvas.Y);
|
|
|
|
// Compute tension from screen distance, proportional to segment width
|
|
var segmentWidth = isIn ? _segmentWidthIn : _segmentWidthOut;
|
|
var segmentPixels = (float)segmentWidth * Math.Abs(_curveEditCanvas.Scale.X);
|
|
var refLength = segmentPixels / 3.0f * T3Ui.UiScaleFactor;
|
|
var screenDistance = (adjustedMousePos - pCenter).Length();
|
|
var rawTension = _vDef.Weighted
|
|
? Math.Clamp(screenDistance / Math.Max(refLength, 1f), 0.05f, 3.0f)
|
|
: 1.0f; // Unweighted: tension locked at 1.0
|
|
|
|
// Apply snapping (unless Shift held)
|
|
double finalAngle;
|
|
float finalTension;
|
|
|
|
if (ImGui.GetIO().KeyShift)
|
|
{
|
|
finalAngle = rawAngle;
|
|
finalTension = rawTension;
|
|
}
|
|
else
|
|
{
|
|
var neighborAngle = isIn ? _neighborAngleIn : _neighborAngleOut;
|
|
var snaps = ApplyTangentSnaps(adjustedMousePos, pCenter,
|
|
rawAngle, rawTension, neighborAngle, segmentWidth,
|
|
out finalAngle, out finalTension);
|
|
|
|
// Track pending snap conversions (committed on release, not during drag)
|
|
_pendingSnaps = snaps & (TangentSnaps.DefaultLength | TangentSnaps.Linear);
|
|
_pendingSnapSide = side;
|
|
|
|
// Snap to linear: apply the angle and tension visually during drag
|
|
if ((snaps & TangentSnaps.Linear) != 0)
|
|
finalTension = 1.0f;
|
|
|
|
if ((snaps & (TangentSnaps.Horizontal | TangentSnaps.Linear)) != 0)
|
|
{
|
|
_lastAngleSnaps = snaps & (TangentSnaps.Horizontal | TangentSnaps.Linear);
|
|
_lastAngleSnapAngle = finalAngle;
|
|
_lastSnapCenter = pCenter;
|
|
_lastAngleSnapTime = ImGui.GetTime();
|
|
}
|
|
}
|
|
|
|
// Apply to this side
|
|
if (isIn)
|
|
{
|
|
_vDef.InTangentAngle = (float)finalAngle;
|
|
_vDef.TensionIn = finalTension;
|
|
}
|
|
else
|
|
{
|
|
_vDef.OutTangentAngle = (float)finalAngle;
|
|
_vDef.TensionOut = finalTension;
|
|
}
|
|
|
|
// Track drag state
|
|
_isDraggingTangent = true;
|
|
_draggedKeyId = _vDef.UniqueId;
|
|
_activeDragAngle = finalAngle;
|
|
_activeDragSegmentWidth = segmentWidth;
|
|
|
|
|
|
// Ctrl breaks tangents
|
|
if (ImGui.GetIO().KeyCtrl)
|
|
_vDef.BrokenTangents = true;
|
|
|
|
// Auto-break on first drag frame if tangents aren't actually mirrored
|
|
// (e.g., Smooth with different auto-angles for each side)
|
|
if (!_vDef.BrokenTangents && !_isDraggingTangent)
|
|
{
|
|
var angleDiff = _vDef.InTangentAngle - (_vDef.OutTangentAngle - Math.PI);
|
|
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
|
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
|
if (Math.Abs(angleDiff) > 0.1)
|
|
_vDef.BrokenTangents = true;
|
|
}
|
|
|
|
// Mirror angle (but not tension) to opposite side if tangents are linked
|
|
// Recompute the mirrored side's screen vector from its own tension (don't copy screen length)
|
|
if (!_vDef.BrokenTangents)
|
|
{
|
|
if (isIn)
|
|
{
|
|
_vDef.OutInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
_vDef.OutTangentAngle = _vDef.InTangentAngle + (float)Math.PI;
|
|
_rightTangentInScreen = ComputeScreenTangent(_vDef.OutTangentAngle, _vDef.TensionOut, _segmentWidthOut);
|
|
}
|
|
else
|
|
{
|
|
_vDef.InInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
_vDef.InTangentAngle = _vDef.OutTangentAngle + (float)Math.PI;
|
|
_leftTangentInScreen = ComputeScreenTangent(_vDef.InTangentAngle, _vDef.TensionIn, _segmentWidthIn);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Promote the other side from Linear if needed
|
|
var otherInterpolation = isIn ? _vDef.OutInterpolation : _vDef.InInterpolation;
|
|
if (otherInterpolation == VDefinition.KeyInterpolation.Linear)
|
|
{
|
|
if (isIn)
|
|
_vDef.OutInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
else
|
|
_vDef.InInterpolation = VDefinition.KeyInterpolation.Tangent;
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Tangent display
|
|
|
|
private static void UpdateTangentVectors()
|
|
{
|
|
if (_curveEditCanvas == null)
|
|
return;
|
|
|
|
_leftTangentInScreen = ComputeScreenTangent(_vDef.InTangentAngle, _vDef.TensionIn, _segmentWidthIn);
|
|
_rightTangentInScreen = ComputeScreenTangent(_vDef.OutTangentAngle, _vDef.TensionOut, _segmentWidthOut);
|
|
}
|
|
|
|
private static Vector2 ComputeScreenTangent(double angle, float tension, double segmentWidth)
|
|
{
|
|
var normVector = new Vector2((float)-Math.Cos(angle), (float)Math.Sin(angle));
|
|
var screenDir = new Vector2(normVector.X * _curveEditCanvas.Scale.X,
|
|
-_curveEditCanvas.TransformDirection(normVector).Y);
|
|
|
|
var len = screenDir.Length();
|
|
if (len < 0.001f)
|
|
return screenDir;
|
|
|
|
// Reference length = 1/3 of the SEGMENT width (not 1/3 of one time unit)
|
|
var segmentPixels = (float)segmentWidth * Math.Abs(_curveEditCanvas.Scale.X);
|
|
var refLength = segmentPixels / 3.0f;
|
|
var targetLength = Math.Max(refLength * tension, 5f) * T3Ui.UiScaleFactor;
|
|
return screenDir * (targetLength / len);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Snapping
|
|
|
|
[Flags]
|
|
private enum TangentSnaps
|
|
{
|
|
None = 0,
|
|
Horizontal = 1,
|
|
Linear = 2,
|
|
DefaultLength = 4,
|
|
}
|
|
|
|
private const float SnapThresholdPx = 3f;
|
|
|
|
private static TangentSnaps ApplyTangentSnaps(
|
|
Vector2 mouseScreenPos, Vector2 keyScreenPos,
|
|
double rawAngle, float rawTension,
|
|
double? neighborAngle, double segmentWidth,
|
|
out double snappedAngle, out float snappedTension)
|
|
{
|
|
snappedAngle = rawAngle;
|
|
snappedTension = rawTension;
|
|
var snaps = TangentSnaps.None;
|
|
var mouseOffset = mouseScreenPos - keyScreenPos;
|
|
var handleDistance = mouseOffset.Length();
|
|
|
|
// Disable angle snaps when handle is too short — snap zones dominate at small radii
|
|
var enableAngleSnaps = handleDistance > 50f;
|
|
|
|
// 1. Horizontal — perpendicular pixel distance to horizontal line
|
|
if (enableAngleSnaps && Math.Abs(mouseOffset.Y) < SnapThresholdPx)
|
|
{
|
|
snappedAngle = Math.Round(snappedAngle / Math.PI) * Math.PI;
|
|
snaps |= TangentSnaps.Horizontal;
|
|
}
|
|
|
|
// 2. Linear (toward neighbor) — perpendicular pixel distance to neighbor line
|
|
if (enableAngleSnaps && neighborAngle.HasValue && (snaps & TangentSnaps.Horizontal) == 0)
|
|
{
|
|
var lineDir = GetScreenDirForAngle(neighborAngle.Value);
|
|
var lineLen = lineDir.Length();
|
|
if (lineLen > 0.001f)
|
|
{
|
|
var perpDist = Math.Abs(mouseOffset.X * lineDir.Y - mouseOffset.Y * lineDir.X) / lineLen;
|
|
if (perpDist < SnapThresholdPx)
|
|
{
|
|
snappedAngle = neighborAngle.Value;
|
|
snaps |= TangentSnaps.Linear;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Default length (tension=1.0) — distance from mouse to reference radius (proportional to segment)
|
|
var segmentPixels = (float)segmentWidth * Math.Abs(_curveEditCanvas.Scale.X);
|
|
var refLength = segmentPixels / 3.0f * T3Ui.UiScaleFactor;
|
|
if (Math.Abs(mouseOffset.Length() - refLength) < SnapThresholdPx)
|
|
{
|
|
snappedTension = 1.0f;
|
|
snaps |= TangentSnaps.DefaultLength;
|
|
}
|
|
|
|
return snaps;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Tangent guides
|
|
|
|
private static void DrawTangentGuides(Vector2 pCenter)
|
|
{
|
|
if (_draggedKeyId != _vDef.UniqueId)
|
|
return;
|
|
|
|
const float guideExtent = 4000f;
|
|
|
|
// 1. Tangent extension line — always visible while dragging
|
|
if (_isDraggingTangent && _curveEditCanvas != null)
|
|
{
|
|
var extensionColor = UiColors.ForegroundFull.Fade(0.03f);
|
|
var screenDir = GetScreenDirForAngle(_activeDragAngle);
|
|
if (screenDir.Length() > 0.001f)
|
|
{
|
|
screenDir *= guideExtent / screenDir.Length();
|
|
_drawList.AddLine(pCenter - screenDir, pCenter + screenDir, extensionColor, 1f);
|
|
}
|
|
|
|
// Default length dots — proportional to active segment
|
|
var segPixels = (float)_activeDragSegmentWidth * Math.Abs(_curveEditCanvas.Scale.X);
|
|
var refLength = segPixels / 3.0f * T3Ui.UiScaleFactor;
|
|
if (screenDir.Length() > 0.001f)
|
|
{
|
|
var dotDir = screenDir * (refLength / guideExtent);
|
|
var dotColor = UiColors.ForegroundFull.Fade(0.1f);
|
|
_drawList.AddCircleFilled(pCenter + dotDir, 2f, dotColor, 6);
|
|
_drawList.AddCircleFilled(pCenter - dotDir, 2f, dotColor,6);
|
|
}
|
|
}
|
|
|
|
// 2. Snap angle guides — orange, with fade-out
|
|
var elapsed = ImGui.GetTime() - _lastAngleSnapTime;
|
|
if (elapsed < SnapFadeDuration && _lastAngleSnaps != TangentSnaps.None)
|
|
{
|
|
var opacity = (float)Math.Max(0, 1.0 - elapsed / SnapFadeDuration);
|
|
var snapColor = UiColors.StatusAnimated.Fade(0.24f * opacity);
|
|
|
|
if ((_lastAngleSnaps & TangentSnaps.Horizontal) != 0)
|
|
{
|
|
_drawList.AddLine(
|
|
_lastSnapCenter - new Vector2(guideExtent, 0),
|
|
_lastSnapCenter + new Vector2(guideExtent, 0),
|
|
snapColor, 1f);
|
|
}
|
|
|
|
if ((_lastAngleSnaps & TangentSnaps.Linear) != 0)
|
|
{
|
|
var screenDir = GetScreenDirForAngle(_lastAngleSnapAngle);
|
|
if (screenDir.Length() > 0.001f)
|
|
{
|
|
screenDir *= guideExtent / screenDir.Length();
|
|
_drawList.AddLine(_lastSnapCenter - screenDir, _lastSnapCenter + screenDir, snapColor, 1f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Vector2 GetScreenDirForAngle(double angle)
|
|
{
|
|
var dir = new Vector2((float)-Math.Cos(angle), (float)Math.Sin(angle));
|
|
return new Vector2(dir.X * _curveEditCanvas.Scale.X,
|
|
-_curveEditCanvas.TransformDirection(dir).Y);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private const double SnapFadeDuration = 0.8;
|
|
|
|
// Active drag state — tied to a specific keyframe via UniqueId
|
|
private static bool _isDraggingTangent;
|
|
private static double _activeDragAngle;
|
|
private static double _activeDragSegmentWidth;
|
|
private static int _draggedKeyId;
|
|
private static TangentSnaps _pendingSnaps;
|
|
private static TangentSide _pendingSnapSide;
|
|
private static Vector2 _dragStartOffset; // Offset from handle center to grab point
|
|
|
|
// Angle snap fade state
|
|
private static TangentSnaps _lastAngleSnaps = TangentSnaps.None;
|
|
private static double _lastAngleSnapAngle;
|
|
private static Vector2 _lastSnapCenter;
|
|
private static double _lastAngleSnapTime;
|
|
|
|
private static ScalableCanvas _curveEditCanvas;
|
|
private static CurveEditing? _curveEditing;
|
|
|
|
// Tracks the undo command for an in-progress tangent drag (null outside of a drag).
|
|
// Static because CurvePoint is a static helper; only one tangent is dragged at any time.
|
|
private static ChangeKeyframesCommand? _tangentDragCommand;
|
|
private static VDefinition _vDef;
|
|
private static ImDrawListPtr _drawList;
|
|
|
|
private static Vector2 _leftTangentInScreen;
|
|
private static Vector2 _rightTangentInScreen;
|
|
private static double? _neighborAngleIn;
|
|
private static double? _neighborAngleOut;
|
|
private static double _segmentWidthIn = 1.0;
|
|
private static double _segmentWidthOut = 1.0;
|
|
|
|
#endregion
|
|
|
|
#region Style constants
|
|
|
|
private static readonly Vector2 _controlSize = new(21, 21);
|
|
private static readonly Vector2 _controlSizeHalf = _controlSize * 0.5f;
|
|
private static readonly Color _tangentHandleColor = new(0.3f);
|
|
private static readonly Vector2 _tangentHandleSize = new(15, 15);
|
|
private static readonly Vector2 _tangentHandleSizeHalf = _tangentHandleSize * 0.5f;
|
|
private const float MinClickableHandleLength = 10f;
|
|
private const float FullClickableHandleLength = 20f;
|
|
private static Vector2 _tangentSize => new(3 * T3Ui.UiScaleFactor);
|
|
private static readonly Vector2 _tangentSizeHalf = _tangentSize * 0.5f;
|
|
|
|
#endregion
|
|
}
|