Skip to content

Instantly share code, notes, and snippets.

@ashblue
Created December 1, 2024 21:19
Show Gist options
  • Save ashblue/c9987dc8095d19044d5d13fbde45a01f to your computer and use it in GitHub Desktop.
Save ashblue/c9987dc8095d19044d5d13fbde45a01f to your computer and use it in GitHub Desktop.
Prototype scripts to recreate the Blender 3D cursor in Unity

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
  2. Click "Resolve Conflict..."
  3. Add a new profile and activate it. Call it "Custom" if you don't already have one
  4. Use the default GameObject shortcut Ctrl+Shift+N again
  5. Choose the "Quick Cursor" shortcut
  6. Select "Rebind ..."
  7. Click "Perform Selected
  8. 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.

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");
}
}
}
}
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;
}
}
}
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");
}
}
}
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");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment