Skip to content

Instantly share code, notes, and snippets.

@ashblue
Created December 1, 2024 21:19

Revisions

  1. ashblue created this gist Dec 1, 2024.
    170 changes: 170 additions & 0 deletions GlobalCursor.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,170 @@
    using UnityEditor;
    using UnityEngine;

    namespace CleverCrow.ColdIronCity.QuickCursor {
    [InitializeOnLoad]
    public class GlobalCursor {
    const float normalizationFactor = 1.2f;
    static Vector3 cursorPosition = Vector3.zero;
    static Quaternion cursorRotation = Quaternion.identity;
    static readonly Color lineColor = Color.black;
    static readonly Color circleColor = Color.red;
    static bool shortcutSetPosition = true;
    static bool shortcutSetRotation;

    public static bool IsCursorVisible { get; private set; } = true;

    public static float CursorSizeMultiplier => CursorSizeFactor * normalizationFactor;

    public static float CursorSizeFactor { get; private set; } = 1.0f;

    static GlobalCursor () {
    SceneView.duringSceneGui += OnScene;

    LoadCursorData();
    }

    static void OnScene (SceneView sceneView) {
    if (!IsCursorVisible) return;

    UpdateCursorPositionShortcut();
    DrawCursor(sceneView.camera);
    }

    static void UpdateCursorPositionShortcut () {
    if (Event.current.shift && Event.current.type == EventType.MouseDown && Event.current.button == 1) {
    var ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
    if (Physics.Raycast(ray, out var hit)) {
    var pos = hit.point;
    if (!shortcutSetPosition) pos = cursorPosition;

    var rotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
    if (!shortcutSetRotation) rotation = cursorRotation;

    SetPosition(pos, rotation);
    }
    }
    }

    static void DrawCursor (Camera sceneCamera) {
    var distanceToCursor = Vector3.Distance(sceneCamera.transform.position, cursorPosition);
    var lineLength = distanceToCursor * 0.05f * CursorSizeMultiplier;
    var gapSize = lineLength * 0.2f * CursorSizeMultiplier;
    var whiteCircleOffset =
    -0.001f * distanceToCursor; // Adjust this value to change the distance between the circles.

    Handles.color = lineColor;

    // Draw lines with a gap in the middle for the left/right axes
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.left * gapSize),
    cursorPosition + cursorRotation * (Vector3.left * lineLength));
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.right * gapSize),
    cursorPosition + cursorRotation * (Vector3.right * lineLength));

    // Highlight Y-axis in yellow
    Handles.color = Color.green;
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.up * gapSize),
    cursorPosition + cursorRotation * (Vector3.up * lineLength));
    Handles.color = lineColor; // Revert to original color
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.down * gapSize),
    cursorPosition + cursorRotation * (Vector3.down * lineLength));

    // Draw x axis
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.forward * gapSize),
    cursorPosition + cursorRotation * (Vector3.forward * lineLength));
    Handles.DrawLine(cursorPosition + cursorRotation * (Vector3.back * gapSize),
    cursorPosition + cursorRotation * (Vector3.back * lineLength));

    // Draw red circle outline in the center
    Handles.color = circleColor;
    Handles.DrawWireDisc(cursorPosition, sceneCamera.transform.forward, gapSize);

    // Draw white circle outline next to the red one
    Handles.color = Color.white;
    Handles.DrawWireDisc(cursorPosition, sceneCamera.transform.forward, gapSize + whiteCircleOffset);
    }

    public static void SetPosition (Vector3 newPosition, Quaternion? newRotation = null) {
    cursorPosition = newPosition;
    if (newRotation.HasValue) cursorRotation = newRotation.Value;

    SceneView.RepaintAll();

    SaveCursorData();
    }

    public static Vector3 GetCursorPosition () {
    return cursorPosition;
    }

    public static Quaternion GetCursorRotation () {
    return cursorRotation;
    }

    public static bool GetUpdatePosition () {
    return shortcutSetPosition;
    }

    public static bool GetUpdateRotation () {
    return shortcutSetRotation;
    }

    public static void SetUpdatePosition (bool value) {
    shortcutSetPosition = value;
    SaveCursorData();
    }

    public static void SetUpdateRotation (bool value) {
    shortcutSetRotation = value;
    SaveCursorData();
    }

    public static void SetIsCursorVisible (bool value) {
    IsCursorVisible = value;
    SaveCursorData();
    }

    public static void SetCursorSizeFactor (float value) {
    CursorSizeFactor = value;
    SaveCursorData();
    }

    static void SaveCursorData () {
    EditorPrefs.SetFloat("GlobalCursorPosX", cursorPosition.x);
    EditorPrefs.SetFloat("GlobalCursorPosY", cursorPosition.y);
    EditorPrefs.SetFloat("GlobalCursorPosZ", cursorPosition.z);

    EditorPrefs.SetFloat("GlobalCursorRotX", cursorRotation.x);
    EditorPrefs.SetFloat("GlobalCursorRotY", cursorRotation.y);
    EditorPrefs.SetFloat("GlobalCursorRotZ", cursorRotation.z);
    EditorPrefs.SetFloat("GlobalCursorRotW", cursorRotation.w);

    EditorPrefs.SetBool("QuickCursorShortcutSetPosition", shortcutSetPosition);
    EditorPrefs.SetBool("QuickCursorShortcutSetRotation", shortcutSetRotation);

    EditorPrefs.SetBool("GlobalCursorIsVisible", IsCursorVisible);
    EditorPrefs.SetFloat("GlobalCursorSizeMultiplier", CursorSizeFactor);
    }

    static void LoadCursorData () {
    if (EditorPrefs.HasKey("GlobalCursorPosX")) {
    cursorPosition = new Vector3(
    EditorPrefs.GetFloat("GlobalCursorPosX"),
    EditorPrefs.GetFloat("GlobalCursorPosY"),
    EditorPrefs.GetFloat("GlobalCursorPosZ"));

    cursorRotation = new Quaternion(
    EditorPrefs.GetFloat("GlobalCursorRotX"),
    EditorPrefs.GetFloat("GlobalCursorRotY"),
    EditorPrefs.GetFloat("GlobalCursorRotZ"),
    EditorPrefs.GetFloat("GlobalCursorRotW"));

    shortcutSetPosition = EditorPrefs.GetBool("QuickCursorShortcutSetPosition");
    shortcutSetRotation = EditorPrefs.GetBool("QuickCursorShortcutSetRotation");

    IsCursorVisible = EditorPrefs.GetBool("GlobalCursorIsVisible");
    CursorSizeFactor = EditorPrefs.GetFloat("GlobalCursorSizeMultiplier");
    }
    }
    }
    }
    101 changes: 101 additions & 0 deletions QuickCursorActionsWindow.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,101 @@
    using UnityEditor;
    using UnityEngine;

    namespace CleverCrow.ColdIronCity.QuickCursor {
    public class QuickCursorActionsWindow : EditorWindow {
    void OnGUI () {
    if (GUILayout.Button("Cursor to Origin")) CursorToOrigin();

    if (GUILayout.Button("Cursor to Selected")) CursorToSelected();

    if (GUILayout.Button("Selection to Cursor")) SelectionToCursor();

    if (GUILayout.Button("Selection to Cursor (offset)")) SelectionToCursorOffset();
    }

    [MenuItem("Tools/Quick Cursor/Actions/Window")]
    public static void ShowWindow () {
    GetWindow<QuickCursorActionsWindow>("Quick Cursor Actions");
    }

    [MenuItem("Tools/Quick Cursor/Actions/Cursor to Origin %#p")]
    public static void CursorToOrigin () {
    var position = Vector3.zero;
    if (!GlobalCursor.GetUpdatePosition()) position = GlobalCursor.GetCursorPosition();

    var rotation = Quaternion.identity;
    if (!GlobalCursor.GetUpdateRotation()) rotation = GlobalCursor.GetCursorRotation();

    GlobalCursor.SetPosition(position, rotation);
    }

    [MenuItem("Tools/Quick Cursor/Actions/Cursor to Selected")]
    public static void CursorToSelected () {
    var avgPos = Vector3.zero;
    foreach (var obj in Selection.transforms) avgPos += obj.position;
    avgPos /= Selection.transforms.Length;
    if (!GlobalCursor.GetUpdatePosition()) avgPos = GlobalCursor.GetCursorPosition();

    var rotation = Quaternion.identity;
    if (Selection.transforms.Length > 0) rotation = Selection.transforms[0].rotation;
    if (!GlobalCursor.GetUpdateRotation()) rotation = GlobalCursor.GetCursorRotation();

    GlobalCursor.SetPosition(avgPos, rotation);
    }

    [MenuItem("Tools/Quick Cursor/Actions/Selection to Cursor")]
    public static void SelectionToCursor () {
    var cursorPos = GlobalCursor.GetCursorPosition();
    var cursorRot = GlobalCursor.GetCursorRotation();

    if (GlobalCursor.GetUpdatePosition() || GlobalCursor.GetUpdateRotation())
    Undo.RecordObjects(Selection.transforms, "Selection to Cursor");

    foreach (var obj in Selection.transforms) {
    if (GlobalCursor.GetUpdateRotation()) obj.rotation = cursorRot;
    if (GlobalCursor.GetUpdatePosition()) obj.position = cursorPos;
    }
    }

    [MenuItem("Tools/Quick Cursor/Actions/Selection to Cursor (offset)")]
    public static void SelectionToCursorOffset () {
    var cursorPos = GlobalCursor.GetCursorPosition();

    if (GlobalCursor.GetUpdatePosition() || GlobalCursor.GetUpdateRotation())
    Undo.RecordObjects(Selection.transforms, "Selection to Cursor (offset)");

    // Calculate the average position of selected objects
    var selectionCenter = Vector3.zero;
    foreach (var obj in Selection.transforms) selectionCenter += obj.position;
    selectionCenter /= Selection.transforms.Length;

    foreach (var obj in Selection.transforms) {
    var offset = obj.position - selectionCenter;
    obj.position = cursorPos + offset;

    if (GlobalCursor.GetUpdateRotation())
    ApplyRelativeRotationToTargetAroundOrigin(
    obj.transform,
    GlobalCursor.GetCursorRotation(),
    cursorPos);
    }
    }

    public static void ApplyRelativeRotationToTargetAroundOrigin (Transform target, Quaternion rotation,
    Vector3 origin) {
    var relativeRotation = target.rotation * Quaternion.Inverse(target.rotation) * rotation;

    // Calculate the vector from the origin to the target
    var toTarget = target.position - origin;

    // Rotate this vector by our relative rotation
    var rotatedVector = relativeRotation * toTarget;

    // Update the target's position
    target.position = origin + rotatedVector;

    // Apply the relative rotation to the target's rotation
    target.rotation = relativeRotation * target.rotation;
    }
    }
    }
    51 changes: 51 additions & 0 deletions QuickCursorPositionWindow.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    using UnityEditor;
    using UnityEngine;

    namespace CleverCrow.ColdIronCity.QuickCursor {
    public class QuickCursorPositionWindow : EditorWindow {
    void OnGUI () {
    var cursorPosition = GlobalCursor.GetCursorPosition();
    var rotationEulerAngles = GlobalCursor.GetCursorRotation().eulerAngles;

    EditorGUILayout.LabelField("Quick Cursor Position and Rotation", EditorStyles.boldLabel);

    cursorPosition = EditorGUILayout.Vector3Field("Position", cursorPosition);
    rotationEulerAngles = EditorGUILayout.Vector3Field("Rotation (Euler Angles)", rotationEulerAngles);

    // Apply precision correction
    cursorPosition = CorrectForFloatPointPrecision(cursorPosition);
    if (rotationEulerAngles != GlobalCursor.GetCursorRotation().eulerAngles) {
    var cursorRotation = Quaternion.Euler(rotationEulerAngles);
    GlobalCursor.SetPosition(cursorPosition, cursorRotation);
    } else {
    GlobalCursor.SetPosition(cursorPosition);
    }

    EditorGUILayout.Space();

    EditorGUILayout.LabelField("Updates", EditorStyles.boldLabel);
    EditorGUILayout.HelpBox(
    "Hold Shift and right-click to set the cursor position and rotation to the point under the mouse cursor.",
    MessageType.Info);
    var pos = EditorGUILayout.Toggle("Set Position", GlobalCursor.GetUpdatePosition());
    var rotation = EditorGUILayout.Toggle("Set Rotation", GlobalCursor.GetUpdateRotation());

    if (pos != GlobalCursor.GetUpdatePosition()) GlobalCursor.SetUpdatePosition(pos);

    if (rotation != GlobalCursor.GetUpdateRotation()) GlobalCursor.SetUpdateRotation(rotation);
    }

    Vector3 CorrectForFloatPointPrecision (Vector3 vector) {
    return new Vector3(
    Mathf.Approximately(vector.x, 0) ? 0 : vector.x,
    Mathf.Approximately(vector.y, 0) ? 0 : vector.y,
    Mathf.Approximately(vector.z, 0) ? 0 : vector.z
    );
    }

    [MenuItem("Tools/Quick Cursor/Position")]
    public static void ShowWindow () {
    GetWindow<QuickCursorPositionWindow>("Quick Cursor Position");
    }
    }
    }
    53 changes: 53 additions & 0 deletions QuickCursorWindow.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    using System;
    using UnityEditor;
    using UnityEngine;

    namespace CleverCrow.ColdIronCity.QuickCursor {
    public class QuickCursorWindow : EditorWindow {
    void OnGUI () {
    EditorGUILayout.LabelField("Quick Cursor Settings", EditorStyles.boldLabel);

    var visible = EditorGUILayout.Toggle("Cursor Visible", GlobalCursor.IsCursorVisible);
    if (GlobalCursor.IsCursorVisible != visible) {
    GlobalCursor.SetIsCursorVisible(visible);
    SceneView.RepaintAll();
    }

    var size = EditorGUILayout.Slider("Cursor Size", GlobalCursor.CursorSizeFactor, 0.1f, 3.0f);
    if (Math.Abs(GlobalCursor.CursorSizeFactor - size) > 0.01f) {
    GlobalCursor.SetCursorSizeFactor(size);
    SceneView.RepaintAll();
    }
    }

    /**
    * You will need to create an override profile to rebind the default GameObject creation shortcuts
    */
    [MenuItem("Tools/Quick Cursor/Create Empty %#n")]
    public static void CreateGameObjectGlobally () {
    var newObject = new GameObject("GameObject");
    newObject.transform.position = GlobalCursor.GetCursorPosition();
    newObject.transform.rotation = GlobalCursor.GetCursorRotation();

    // Select the newly created GameObject
    Selection.activeGameObject = newObject;
    }

    [MenuItem("Tools/Quick Cursor/Create Empty Child &#n")]
    public static void CreateGameObjectLocally () {
    var newObject = new GameObject("GameObject");
    newObject.transform.position = GlobalCursor.GetCursorPosition();
    newObject.transform.rotation = GlobalCursor.GetCursorRotation();

    if (Selection.activeTransform != null) newObject.transform.SetParent(Selection.activeTransform);

    // Select the newly created GameObject
    Selection.activeGameObject = newObject;
    }

    [MenuItem("Tools/Quick Cursor/Settings")]
    public static void ShowWindow () {
    GetWindow<QuickCursorWindow>("Quick Cursor");
    }
    }
    }
    33 changes: 33 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    # Quick Cursor

    A faithful adaptation of Blender's cursor system for Unity.

    ## Getting Started

    1. Add the `QuickCursor` folder to your project.
    2. Open the `QuickCursor` window from the `Tools/Quick Cursor/Settings` menu.

    ### Updating the cursor position

    You can snap the cursor to any "collider" in the scene. If you don't have a collider on your objects it wont work.

    1. Hold shift and right click
    2. The cursor will auto update the position
    3. Open the position window from the `Tools/Quick Cursor/Position` for more details

    ### Creating new GameObjects at the cursor

    You can create new GameObjects at the cursor. But you have to override Unity's default inputs.

    1. Use the default GameObject shortcut `Ctrl+Shift+N`
    1. Click "Resolve Conflict..."
    1. Add a new profile and activate it. Call it "Custom" if you don't already have one
    1. Use the default GameObject shortcut `Ctrl+Shift+N` again
    1. Choose the "Quick Cursor" shortcut
    1. Select "Rebind ..."
    1. Click "Perform Selected
    1. Do the same for the `Alt+Shift+N` shortcut

    ### Action Menu

    You can use the action menu from `Tools/Quick Cursor/Actions` to quickly move objects around the scene.