Editor/ThreeDCursor.cs

Editor utility that stores and renders a per-session Blender-style 3D cursor for the scene editor. It tracks cursor position and pivot usage per SceneEditorSession, handles keyboard shortcuts to reset or toggle pivot, snaps to nearest vertex on a modifier chord, and draws a camera-facing gizmo ring via an isolated Gizmo.Instance.

Reflection
#nullable enable
using Editor;
using Sandbox;
using System;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace BlenderActions;

/// <summary>Stores, positions, and renders the per-session Blender-style 3D cursor.</summary>
public static class ThreeDCursor
{
    /// <summary>Handles new.</summary>
    private static readonly CursorGizmoBridge GizmoBridge = new();
    /// <summary>Handles new.</summary>
    private static ConditionalWeakTable<SceneEditorSession, CursorState> _states = new();

    /// <summary>Returns state for the active editor session and optionally creates it.</summary>
    public static Vector3 Position => GetCurrentState(false)?.Position ?? Vector3.Zero;
    /// <summary>Returns state for the active editor session and optionally creates it.</summary>
    public static bool UseAsTransformPivot => GetCurrentState(false)?.UseAsTransformPivot ?? false;

    /// <summary>Resets the active session cursor to the world origin.</summary>
    [Shortcut("blender_actions.cursor_reset", "SHIFT+C", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void ResetPosition()
    {
        var state = GetCurrentState(true);

        if(state != null)
            state.Position = Vector3.Zero;
    }

    /// <summary>Toggles use of the 3D cursor as the transform pivot.</summary>
    [Shortcut("blender_actions.cursor_toggle_pivot", "ALT+C", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void ToggleTransformPivot()
    {
        var state = GetCurrentState(true);

        if(state != null)
            state.UseAsTransformPivot = !state.UseAsTransformPivot;
    }

    /// <summary>Advances active modal operations once per editor tool frame.</summary>
    [Event("tool.frame")]
    private static void OnToolFrame()
    {
        CheckSetCursorChord();
        DrawCursor();
    }

    /// <summary>Aborts active operations and resets session state after hotload.</summary>
    [EditorEvent.Hotload]
    private static void OnHotload()
    {
        _states = new ConditionalWeakTable<SceneEditorSession, CursorState>();
    }

    /// <summary>Detects the cursor-placement modifier chord without interfering with modal operations.</summary>
    private static void CheckSetCursorChord()
    {
        var state = GetCurrentState(true);

        if(state == null)
            return;

        var modifiers = Editor.Application.KeyboardModifiers;
        var required =
            KeyboardModifiers.Alt |
            KeyboardModifiers.Ctrl |
            KeyboardModifiers.Shift;
        var chordDown = (modifiers & required) == required;

        if(ModalOperationArbiter.Active != null)
        {
            state.SetCursorChordDown = chordDown;
            return;
        }

        if(chordDown && !state.SetCursorChordDown)
            SetAtNearestVertex(state);

        state.SetCursorChordDown = chordDown;
    }

    /// <summary>Moves the cursor to the nearest target vertex under the pointer.</summary>
    private static void SetAtNearestVertex(CursorState state)
    {
        var sceneView = SceneViewWidget.Current;
        var viewport = sceneView?.LastSelectedViewportWidget;
        var session = SceneEditorSession.Active;
        var camera =
            sceneView?.Tools.CurrentSubTool?.Camera ??
            sceneView?.Tools.CurrentTool?.Camera;

        if(viewport == null || session == null || camera == null)
            return;

        var result = VertexSnapService.FindTargetVertex(
            session.Scene,
            camera,
            viewport,
            Array.Empty<GameObject>());

        if(result.Found)
            state.Position = result.Vertex;
    }

    /// <summary>Draws the active session cursor in the current scene viewport.</summary>
    private static void DrawCursor()
    {
        var state = GetCurrentState(false);

        if(state == null)
            return;

        var sceneView = SceneViewWidget.Current;
        var viewport = sceneView?.LastSelectedViewportWidget;
        var camera =
            sceneView?.Tools.CurrentSubTool?.Camera ??
            sceneView?.Tools.CurrentTool?.Camera;

        if(viewport == null || !viewport.IsValid || camera == null)
            return;

        GizmoBridge.Draw(viewport, camera, state.Position);
    }

    /// <summary>Returns state for the active editor session and optionally creates it.</summary>
    private static CursorState? GetCurrentState(bool create)
    {
        var session = SceneEditorSession.Active;

        if(session == null)
            return null;

        if(create)
            return _states.GetValue(session, _ => new CursorState());

        return _states.TryGetValue(session, out var state)
            ? state
            : null;
    }

    /// <summary>Stores 3D cursor state associated with one editor session.</summary>
    private sealed class CursorState
    {
        /// <summary>Gets the active editor session's 3D cursor position.</summary>
        public Vector3 Position { get; set; }
        /// <summary>Gets whether the active session uses the 3D cursor as transform pivot.</summary>
        public bool UseAsTransformPivot { get; set; }
        /// <summary>Gets set cursor chord down.</summary>
        public bool SetCursorChordDown { get; set; }
    }

    /// <summary>Renders the 3D cursor through an isolated gizmo instance.</summary>
    private sealed class CursorGizmoBridge
    {
        /// <summary>Handles new.</summary>
        private readonly Gizmo.Instance _instance = new();
        /// <summary>Handles typeof.</summary>
        private readonly FieldInfo? _worldField = typeof(Gizmo.Instance).GetField(
            "_world",
            BindingFlags.Instance | BindingFlags.NonPublic);

        /// <summary>Draws a camera-facing cursor ring through the isolated gizmo bridge.</summary>
        public void Draw(
            SceneViewportWidget viewport,
            CameraComponent camera,
            Vector3 position)
        {
            var world = viewport.GizmoInstance.World;

            if(world == null || _worldField == null)
                return;

            if(_instance.World != world)
                _worldField.SetValue(_instance, world);

            _instance.Settings = viewport.GizmoInstance.Settings;

            var toCamera = camera.GameObject.WorldPosition - position;

            if(toCamera.Length < 0.001f)
                return;

            var rotation = Rotation.LookAt(toCamera.Normal);
            var radius = MathF.Max(toCamera.Length * 0.015f, 2f);

            using(_instance.Push())
            using(Gizmo.Scope(
                "blender-actions-3d-cursor",
                position,
                rotation,
                1f))
            {
                Gizmo.Draw.LineCircle(0, radius);
            }
        }
    }
}