Snapping to vertex on one axis only (while holding V in scene editor)?

In the Unity scene editor, I’d like to position an object in relation to another one nearby. In a 3d software, I could move it once in one or two axis’ horizontally first and then move it again to align it vertically.

To do this, I would need to be able to lock one or two axis’. For example, to move an object down to snap in the Z axis, you can temporarily disable the X and Y movement.

In Blender you would press Z to lock an object you’re moving to the Z axis or shift-Z to lock it to X and Y.

Is there a way to do this with a Unity editor script?

Could it be scripted? Is there an asset in the asset store that allows this?

Are you trying to move the object via an editor script, or via your mouse?

If it’s your mouse, you can just use the individual arrows on the position gizmo to move the object on only a particular axis, or use one of the 3 small “squares” at the center of the gizmo to move the object along a particular plane (along 2 axes).

“Snapping to vertex”. It’s in the title :wink:

And I am looking for the same functionality… Holding v to snap vertices is great and all, but one would normally also have the functionality to constrain the vertex-snap along a chosen axis. I haven’t found this to be possible in Unity. But I will code it myself now

Ok, I have made a thing. It might have been made before by others, but at least I never found it.
This recreate some functionality I have been missing from 3Ds Max, and which the OP also seem to need, where you can constrain movement in the editor to only one axis, even when snapping to vertices and such. It is an Overlay script with a new element that goes along. And so it NEEDS to be placed in folder called “Editor” (Filename: AxisConstrainOverlay.cs).

To activate it you as you do with other scene overlay tools: Three dots->Overlay Menu->Axis Constrain
It’s a toggle and dropdown in one, and can be activated and deactived by clicking the name, and the axis chosen by clicking the arrow. Also F5, F6 and F7 changes the axis (And if held down while the tool is deactive, it will activate until key up)

Anyways. Fun to make, learned something new. Some hacks in it (Don’t mention the icon creation!!!), but otherwise pretty robust. Probably did something backwards, but it is what it is :smile:

using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Toolbars;
using UnityEditor.Overlays;
using UnityEngine.UIElements;
using UnityEditor;

[Overlay(typeof(SceneView), "AxisConstrain", "Axis Constrain", true)]
public class AxisConstrainOverlay : ToolbarOverlay
{
    public static Dictionary<Transform, Vector3> initPositions = new Dictionary<Transform, Vector3>();
    public static Texture2D icon;
    AxisConstrainOverlay() : base(
        AxisDropDown.id
        )
    {
        icon = Icon();
        collapsedIcon = icon;
    }

    public static Texture2D Icon ()
    {
        //Build the icon to prevent needing an asset for it. Oh well...
        Color[] txColor = new Color[256];
        txColor[0] = new Color(1f, 1f, 1f, 0f);
        txColor[1] = new Color(1f, 1f, 1f, 0f);
        txColor[2] = new Color(1f, 1f, 1f, 0f);
        txColor[3] = new Color(1f, 1f, 1f, 0f);
        txColor[4] = new Color(1f, 1f, 1f, 0f);
        txColor[5] = new Color(1f, 1f, 1f, 0f);
        txColor[6] = new Color(1f, 1f, 1f, 0f);
        txColor[7] = new Color(1f, 1f, 1f, 0f);
        txColor[8] = new Color(1f, 1f, 1f, 0f);
        txColor[9] = new Color(1f, 1f, 1f, 0f);
        txColor[10] = new Color(1f, 1f, 1f, 0f);
        txColor[11] = new Color(1f, 1f, 1f, 0f);
        txColor[12] = new Color(1f, 1f, 1f, 0f);
        txColor[13] = new Color(1f, 1f, 1f, 0f);
        txColor[14] = new Color(1f, 1f, 1f, 0f);
        txColor[15] = new Color(1f, 1f, 1f, 0f);
        txColor[16] = new Color(1f, 1f, 1f, 0f);
        txColor[17] = new Color(1f, 1f, 1f, 0f);
        txColor[18] = new Color(1f, 1f, 1f, 0f);
        txColor[19] = new Color(1f, 1f, 1f, 0f);
        txColor[20] = new Color(1f, 1f, 1f, 0f);
        txColor[21] = new Color(1f, 1f, 1f, 0f);
        txColor[22] = new Color(1f, 1f, 1f, 0f);
        txColor[23] = new Color(1f, 1f, 1f, 0f);
        txColor[24] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[25] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[26] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[27] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[28] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[29] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[30] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[31] = new Color(1f, 1f, 1f, 0f);
        txColor[32] = new Color(1f, 1f, 1f, 0f);
        txColor[33] = new Color(1f, 1f, 1f, 0f);
        txColor[34] = new Color(1f, 1f, 1f, 0f);
        txColor[35] = new Color(1f, 1f, 1f, 0f);
        txColor[36] = new Color(1f, 1f, 1f, 0f);
        txColor[37] = new Color(1f, 1f, 1f, 0f);
        txColor[38] = new Color(1f, 1f, 1f, 0f);
        txColor[39] = new Color(1f, 1f, 1f, 0f);
        txColor[40] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[41] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[42] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[43] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[44] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[45] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[46] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[47] = new Color(1f, 1f, 1f, 0f);
        txColor[48] = new Color(1f, 1f, 1f, 0f);
        txColor[49] = new Color(1f, 1f, 1f, 0f);
        txColor[50] = new Color(1f, 1f, 1f, 0f);
        txColor[51] = new Color(1f, 1f, 1f, 0f);
        txColor[52] = new Color(1f, 1f, 1f, 0f);
        txColor[53] = new Color(1f, 1f, 1f, 0f);
        txColor[54] = new Color(1f, 1f, 1f, 0f);
        txColor[55] = new Color(1f, 1f, 1f, 0f);
        txColor[56] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[57] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[58] = new Color(1f, 1f, 1f, 0.03921569f);
        txColor[59] = new Color(1f, 1f, 1f, 0.6078432f);
        txColor[60] = new Color(1f, 1f, 1f, 0.8352942f);
        txColor[61] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[62] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[63] = new Color(1f, 1f, 1f, 0f);
        txColor[64] = new Color(1f, 1f, 1f, 0f);
        txColor[65] = new Color(1f, 1f, 1f, 0.2941177f);
        txColor[66] = new Color(1f, 1f, 1f, 0.1960784f);
        txColor[67] = new Color(1f, 1f, 1f, 0f);
        txColor[68] = new Color(1f, 1f, 1f, 0f);
        txColor[69] = new Color(1f, 1f, 1f, 0f);
        txColor[70] = new Color(1f, 1f, 1f, 0f);
        txColor[71] = new Color(1f, 1f, 1f, 0f);
        txColor[72] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[73] = new Color(1f, 1f, 1f, 0.1960784f);
        txColor[74] = new Color(1f, 1f, 1f, 0.8470589f);
        txColor[75] = new Color(1f, 1f, 1f, 0.5882353f);
        txColor[76] = new Color(1f, 1f, 1f, 0.03921569f);
        txColor[77] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[78] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[79] = new Color(1f, 1f, 1f, 0f);
        txColor[80] = new Color(1f, 1f, 1f, 0f);
        txColor[81] = new Color(1f, 1f, 1f, 0.282353f);
        txColor[82] = new Color(1f, 1f, 1f, 0.854902f);
        txColor[83] = new Color(1f, 1f, 1f, 0.7607844f);
        txColor[84] = new Color(1f, 1f, 1f, 0.3333333f);
        txColor[85] = new Color(1f, 1f, 1f, 0.01568628f);
        txColor[86] = new Color(1f, 1f, 1f, 0f);
        txColor[87] = new Color(1f, 1f, 1f, 0.003921569f);
        txColor[88] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[89] = new Color(1f, 1f, 1f, 0.8980393f);
        txColor[90] = new Color(1f, 1f, 1f, 0.3058824f);
        txColor[91] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[92] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[93] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[94] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[95] = new Color(1f, 1f, 1f, 0f);
        txColor[96] = new Color(1f, 1f, 1f, 0f);
        txColor[97] = new Color(1f, 1f, 1f, 0f);
        txColor[98] = new Color(1f, 1f, 1f, 0.007843138f);
        txColor[99] = new Color(1f, 1f, 1f, 0.2941177f);
        txColor[100] = new Color(1f, 1f, 1f, 0.7254902f);
        txColor[101] = new Color(1f, 1f, 1f, 0.8862746f);
        txColor[102] = new Color(1f, 1f, 1f, 0.4784314f);
        txColor[103] = new Color(1f, 1f, 1f, 0.7333333f);
        txColor[104] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[105] = new Color(1f, 1f, 1f, 0.1019608f);
        txColor[106] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[107] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[108] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[109] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[110] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[111] = new Color(1f, 1f, 1f, 0f);
        txColor[112] = new Color(1f, 1f, 1f, 0f);
        txColor[113] = new Color(1f, 1f, 1f, 0f);
        txColor[114] = new Color(1f, 1f, 1f, 0f);
        txColor[115] = new Color(1f, 1f, 1f, 0f);
        txColor[116] = new Color(1f, 1f, 1f, 0f);
        txColor[117] = new Color(1f, 1f, 1f, 0.1568628f);
        txColor[118] = new Color(1f, 1f, 1f, 0.5490196f);
        txColor[119] = new Color(1f, 1f, 1f, 0.937255f);
        txColor[120] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[121] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[122] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[123] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[124] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[125] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[126] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[127] = new Color(1f, 1f, 1f, 0f);
        txColor[128] = new Color(1f, 1f, 1f, 0f);
        txColor[129] = new Color(1f, 1f, 1f, 0f);
        txColor[130] = new Color(1f, 1f, 1f, 0f);
        txColor[131] = new Color(1f, 1f, 1f, 0f);
        txColor[132] = new Color(1f, 1f, 1f, 0f);
        txColor[133] = new Color(1f, 1f, 1f, 0f);
        txColor[134] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[135] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[136] = new Color(1f, 1f, 1f, 0f);
        txColor[137] = new Color(1f, 1f, 1f, 0f);
        txColor[138] = new Color(1f, 1f, 1f, 0f);
        txColor[139] = new Color(1f, 1f, 1f, 0f);
        txColor[140] = new Color(1f, 1f, 1f, 0f);
        txColor[141] = new Color(1f, 1f, 1f, 0f);
        txColor[142] = new Color(1f, 1f, 1f, 0f);
        txColor[143] = new Color(1f, 1f, 1f, 0f);
        txColor[144] = new Color(1f, 1f, 1f, 0f);
        txColor[145] = new Color(1f, 1f, 1f, 0f);
        txColor[146] = new Color(1f, 1f, 1f, 0f);
        txColor[147] = new Color(1f, 1f, 1f, 0f);
        txColor[148] = new Color(1f, 1f, 1f, 0f);
        txColor[149] = new Color(1f, 1f, 1f, 0f);
        txColor[150] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[151] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[152] = new Color(1f, 1f, 1f, 0f);
        txColor[153] = new Color(1f, 1f, 1f, 0f);
        txColor[154] = new Color(1f, 1f, 1f, 0f);
        txColor[155] = new Color(1f, 1f, 1f, 0f);
        txColor[156] = new Color(1f, 1f, 1f, 0f);
        txColor[157] = new Color(1f, 1f, 1f, 0f);
        txColor[158] = new Color(1f, 1f, 1f, 0f);
        txColor[159] = new Color(1f, 1f, 1f, 0f);
        txColor[160] = new Color(1f, 1f, 1f, 0f);
        txColor[161] = new Color(1f, 1f, 1f, 0f);
        txColor[162] = new Color(1f, 1f, 1f, 0f);
        txColor[163] = new Color(1f, 1f, 1f, 0f);
        txColor[164] = new Color(1f, 1f, 1f, 0f);
        txColor[165] = new Color(1f, 1f, 1f, 0f);
        txColor[166] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[167] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[168] = new Color(1f, 1f, 1f, 0f);
        txColor[169] = new Color(1f, 1f, 1f, 0f);
        txColor[170] = new Color(1f, 1f, 1f, 0f);
        txColor[171] = new Color(1f, 1f, 1f, 0f);
        txColor[172] = new Color(1f, 1f, 1f, 0f);
        txColor[173] = new Color(1f, 1f, 1f, 0f);
        txColor[174] = new Color(1f, 1f, 1f, 0f);
        txColor[175] = new Color(1f, 1f, 1f, 0f);
        txColor[176] = new Color(1f, 1f, 1f, 0f);
        txColor[177] = new Color(1f, 1f, 1f, 0f);
        txColor[178] = new Color(1f, 1f, 1f, 0f);
        txColor[179] = new Color(1f, 1f, 1f, 0f);
        txColor[180] = new Color(1f, 1f, 1f, 0f);
        txColor[181] = new Color(1f, 1f, 1f, 0f);
        txColor[182] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[183] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[184] = new Color(1f, 1f, 1f, 0f);
        txColor[185] = new Color(1f, 1f, 1f, 0f);
        txColor[186] = new Color(1f, 1f, 1f, 0f);
        txColor[187] = new Color(1f, 1f, 1f, 0f);
        txColor[188] = new Color(1f, 1f, 1f, 0f);
        txColor[189] = new Color(1f, 1f, 1f, 0f);
        txColor[190] = new Color(1f, 1f, 1f, 0f);
        txColor[191] = new Color(1f, 1f, 1f, 0f);
        txColor[192] = new Color(1f, 1f, 1f, 0f);
        txColor[193] = new Color(1f, 1f, 1f, 0f);
        txColor[194] = new Color(1f, 1f, 1f, 0f);
        txColor[195] = new Color(1f, 1f, 1f, 0f);
        txColor[196] = new Color(1f, 1f, 1f, 0f);
        txColor[197] = new Color(1f, 1f, 1f, 0f);
        txColor[198] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[199] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[200] = new Color(1f, 1f, 1f, 0f);
        txColor[201] = new Color(1f, 1f, 1f, 0f);
        txColor[202] = new Color(1f, 1f, 1f, 0f);
        txColor[203] = new Color(1f, 1f, 1f, 0f);
        txColor[204] = new Color(1f, 1f, 1f, 0f);
        txColor[205] = new Color(1f, 1f, 1f, 0f);
        txColor[206] = new Color(1f, 1f, 1f, 0f);
        txColor[207] = new Color(1f, 1f, 1f, 0f);
        txColor[208] = new Color(1f, 1f, 1f, 0f);
        txColor[209] = new Color(1f, 1f, 1f, 0f);
        txColor[210] = new Color(1f, 1f, 1f, 0f);
        txColor[211] = new Color(1f, 1f, 1f, 0f);
        txColor[212] = new Color(1f, 1f, 1f, 0f);
        txColor[213] = new Color(1f, 1f, 1f, 0f);
        txColor[214] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[215] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[216] = new Color(1f, 1f, 1f, 0f);
        txColor[217] = new Color(1f, 1f, 1f, 0f);
        txColor[218] = new Color(1f, 1f, 1f, 0f);
        txColor[219] = new Color(1f, 1f, 1f, 0f);
        txColor[220] = new Color(1f, 1f, 1f, 0f);
        txColor[221] = new Color(1f, 1f, 1f, 0f);
        txColor[222] = new Color(1f, 1f, 1f, 0f);
        txColor[223] = new Color(1f, 1f, 1f, 0f);
        txColor[224] = new Color(1f, 1f, 1f, 0f);
        txColor[225] = new Color(1f, 1f, 1f, 0f);
        txColor[226] = new Color(1f, 1f, 1f, 0f);
        txColor[227] = new Color(1f, 1f, 1f, 0f);
        txColor[228] = new Color(1f, 1f, 1f, 0f);
        txColor[229] = new Color(1f, 1f, 1f, 0f);
        txColor[230] = new Color(1f, 1f, 1f, 0.03137255f);
        txColor[231] = new Color(1f, 1f, 1f, 0.4705883f);
        txColor[232] = new Color(1f, 1f, 1f, 0f);
        txColor[233] = new Color(1f, 1f, 1f, 0f);
        txColor[234] = new Color(1f, 1f, 1f, 0f);
        txColor[235] = new Color(1f, 1f, 1f, 0f);
        txColor[236] = new Color(1f, 1f, 1f, 0f);
        txColor[237] = new Color(1f, 1f, 1f, 0f);
        txColor[238] = new Color(1f, 1f, 1f, 0f);
        txColor[239] = new Color(1f, 1f, 1f, 0f);
        txColor[240] = new Color(1f, 1f, 1f, 0f);
        txColor[241] = new Color(1f, 1f, 1f, 0f);
        txColor[242] = new Color(1f, 1f, 1f, 0f);
        txColor[243] = new Color(1f, 1f, 1f, 0f);
        txColor[244] = new Color(1f, 1f, 1f, 0f);
        txColor[245] = new Color(1f, 1f, 1f, 0f);
        txColor[246] = new Color(1f, 1f, 1f, 0f);
        txColor[247] = new Color(1f, 1f, 1f, 0f);
        txColor[248] = new Color(1f, 1f, 1f, 0f);
        txColor[249] = new Color(1f, 1f, 1f, 0f);
        txColor[250] = new Color(1f, 1f, 1f, 0f);
        txColor[251] = new Color(1f, 1f, 1f, 0f);
        txColor[252] = new Color(1f, 1f, 1f, 0f);
        txColor[253] = new Color(1f, 1f, 1f, 0f);
        txColor[254] = new Color(1f, 1f, 1f, 0f);
        txColor[255] = new Color(1f, 1f, 1f, 0f);
        Texture2D iconTx = new Texture2D(16, 16, TextureFormat.ARGB32, false);
        iconTx.filterMode = FilterMode.Point;
        iconTx.wrapMode = TextureWrapMode.Clamp;
        iconTx.SetPixels(txColor);
        iconTx.Apply();

        return iconTx;
    }
}

[EditorToolbarElement(id, typeof(SceneView))]
class AxisDropDown : EditorToolbarDropdownToggle, IAccessContainerWindow
{

    public const string id = "AxisConstrain/Axis";
    public EditorWindow containerWindow { get; set; }
    public bool constrainActive;
    static int axisIndex = 0;
    static readonly string[] axisName = new string[] { "x", "y", "z" };
 
    UndoPropertyModification[] changedMods = null;
    static readonly Color[] btnColors = new Color[] { new Color(0.3686275f, 0.1254902f, 0.1341253f), new Color(0.162876f, 0.3679245f, 0.126691f), new Color(0.1460929f, 0.19f, 0.4622641f) };
 
    bool preConstrainActive;
    bool down;
    public AxisDropDown()
    {
        //Load some values from registry to make tool persistent in editor load instances.
        SetActive(EditorPrefs.GetBool("AxisConstrainActive", false), EditorPrefs.GetInt("AxisConstrainAxis", 0));

        //Subscribe the dropdown function
        dropdownClicked += ShowMenu;
        //Subscribe the toggle click
        this.RegisterValueChangedCallback(SetConstrain);
        //Subscribe to scene view changes
        SceneView.duringSceneGui -= ConstrainPositions;
        SceneView.duringSceneGui += ConstrainPositions;

        icon = AxisConstrainOverlay.icon;
    }

    void ConstrainPositions(SceneView view)
    {
        //Only run when visible
        if (containerWindow != null && containerWindow.TryGetOverlay("AxisConstrain", out Overlay result))
        {
            if (!result.displayed)
            {
                SceneView.duringSceneGui -= ConstrainPositions;
                ObjectChangeEvents.changesPublished -= ObjectChangeEventsHandler;
                Undo.postprocessModifications -= PostprocessModifications;
                return;
            }
        }

        if (view != containerWindow)
        {
            return;
        }

        if (constrainActive)
        {
            //Convert curretn rotation handle to its matrix form, so we can grab an axis
            Matrix4x4 handleOrientation = Matrix4x4.TRS(Vector3.zero, Tools.handleRotation, Vector3.one);
            Vector3 compareAxis = handleOrientation.GetColumn(axisIndex).normalized;

            //Do to rounding errors in floats, constraining with initial value before moving is best.
            foreach (KeyValuePair<Transform, Vector3> entry in AxisConstrainOverlay.initPositions)
            {
                //Delta vector of current local position vs initial local position when action started
                Vector3 moveVector = entry.Key.localPosition - entry.Value;

                if (entry.Key.parent != null)
                {
                    //The changed property is in local coordinates. Transform to world using parent
                    moveVector = entry.Key.parent.localToWorldMatrix.MultiplyPoint3x4(moveVector) - entry.Key.parent.position;
                }
                //Project the delta vector on to the constrain axis (In world space)
                Vector3 modMoveVector = Vector3.Project(moveVector, compareAxis);

                //Offset the transform by how the projected vector differs from the delta vector
                entry.Key.position += modMoveVector - moveVector;
            }
        }

        if (Event.current != null)
        {
            if (Event.current.type == EventType.MouseUp)
            {
                //Clear all init positions on MouseUp, to prepare for next move action
                AxisConstrainOverlay.initPositions.Clear();
            }
            else if (Event.current.type == EventType.KeyDown && !down)
            {
                preConstrainActive = constrainActive;
            
                if (Event.current.keyCode == KeyCode.F5)
                {
                    down = true;
                    SetActive(true, 0);
                }
                else if (Event.current.keyCode == KeyCode.F6)
                {
                    down = true;
                    SetActive(true, 1);
                }
                else if (Event.current.keyCode == KeyCode.F7)
                {
                    down = true;
                    SetActive(true, 2);
                }
            }
            else if (Event.current.type == EventType.KeyUp && down)
            {
                if (Event.current.keyCode == KeyCode.F5)
                {
                    down = false;
                    SetActive(preConstrainActive, 0);
                }
                else if (Event.current.keyCode == KeyCode.F6)
                {
                    down = false;
                    SetActive(preConstrainActive, 1);
                }
                else if (Event.current.keyCode == KeyCode.F7)
                {
                    down = false;
                    SetActive(preConstrainActive, 2);
                }
            }
        }
    }

    void SetConstrain(ChangeEvent<bool> toggle)
    {
        //Save toggle state in both var and registry and appearance
        SetActive(toggle.newValue, axisIndex);
    }

    public UndoPropertyModification[] PostprocessModifications(UndoPropertyModification[] modifications)
    {
        //Save this undo event to make sure we have the initial values of the objects being moved
        changedMods = modifications;
        return modifications;
    }

    public void ObjectChangeEventsHandler(ref ObjectChangeEventStream stream)
    {
        int changes = stream.length;

        for (int i = 0; i < changes && changedMods != null && changedMods.Length > 0; i++)
        {
            //Find the "kind" of change i
            ObjectChangeKind changeKind = stream.GetEventType(i);
            if (changeKind == ObjectChangeKind.ChangeGameObjectOrComponentProperties)
            {
                //"Kind" is the one we are looking for: Component changes.
                //Grab the data of this change
                stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out ChangeGameObjectOrComponentPropertiesEventArgs data);

                //Get the Object of this change based on instanceID
                Object instance = EditorUtility.InstanceIDToObject(data.instanceId);

                if (instance is Transform)
                {
                    //Changed object is of type: Transform
                    Transform changedTransform = instance as Transform;

                    //Check if we have this object stored already!
                    if (!AxisConstrainOverlay.initPositions.ContainsKey(changedTransform))
                    {
                        //Create and initialize initPos with local positions of changed transform (So that components of the Vector3 localPosition that are NOT changed are still saved)
                        Vector3 initPos;
                        initPos = changedTransform.localPosition;

                        //Loop through undo ops and find one that matches this change
                        for (int j = 0; j < changedMods.Length; j++)
                        {
                            UndoPropertyModification currentMod = changedMods[j];

                            if (currentMod.previousValue.target == changedTransform)
                            {
                                if (float.TryParse(currentMod.previousValue.value, out float valueAsFloat))
                                {
                                    //Value is a float. Find the vector component by the property path
                                    if (currentMod.previousValue.propertyPath == "m_LocalPosition.x")
                                    {
                                        initPos.x = valueAsFloat;
                                    }
                                    else if (currentMod.previousValue.propertyPath == "m_LocalPosition.y")
                                    {
                                        initPos.y = valueAsFloat;
                                    }
                                    else if (currentMod.previousValue.propertyPath == "m_LocalPosition.z")
                                    {
                                        initPos.z = valueAsFloat;
                                    }
                                }
                            }
                        }
                        //Store init value in dict.
                        AxisConstrainOverlay.initPositions.Add(changedTransform, initPos);
                    }
                }
            }
        }
    }

    void ShowMenu()
    {
        //Create and show drop down of toggle element. Add actions on value change
        GenericMenu menu = new GenericMenu();
        menu.AddItem(new GUIContent("x-axis"), axisIndex == 0, () => { SetActive(true, 0); });
        menu.AddItem(new GUIContent("y-axis"), axisIndex == 1, () => { SetActive(true, 1); });
        menu.AddItem(new GUIContent("z-axis"), axisIndex == 2, () => { SetActive(true, 2); });
        menu.ShowAsContext();
    }

    void SetActive (bool active, int axis)
    {
        constrainActive = active;
        EditorPrefs.SetBool("AxisConstrainActive", constrainActive);
        SetValueWithoutNotify(constrainActive);
        axisIndex = axis;
        EditorPrefs.SetInt("AxisConstrainAxis", axisIndex);
        SetText();

        ObjectChangeEvents.changesPublished -= ObjectChangeEventsHandler;
        Undo.postprocessModifications -= PostprocessModifications;
        if (active)
        {
            ObjectChangeEvents.changesPublished += ObjectChangeEventsHandler;
            Undo.postprocessModifications += PostprocessModifications;
        }
    }

    void SetText ()
    {
        //Change text based on if active...
        if (constrainActive)
        {
            text = "<color=#" + ColorUtility.ToHtmlStringRGB(btnColors[axisIndex] * 4) + "><b>Constrain to " + axisName[axisIndex] + "-axis";
        }
        else
        {
            text = "<b>Axis Constrain Off";
        }
    }
}
2 Likes

Thank you for sharing this script, you’ve probably saved me hours of tweaking decimals to the 4th place to get something exactly where I want it.

The key shortcuts weren’t functional for me in Unity 6000.0.59f2 so I made some tweaks. They’re now working and available in the Shortcuts window to be rebound if need be (under Scene View/Axis Constrain).

It’s always amazed me that this most basic of 3D movement features is missing from Unity. They almost had it in ProBuilder, you can constrain an extruded surface and snap it to a vertex, but you can’t do the same with movement.

Hopefully this takes us another step closer to 3D manipulation in Unity being as functional as SketchUp from 10 years ago.

*edited to add controls, they work slightly differently:

Quick Tap toggles constraints of or off. eg:

  • If constraint is OFF, tap F5 turns it ON with X-axis
  • If constraint is ON with X-axis, tap F5 turns it OFF
  • If constraint is ON with Y-axis, tap F5 switches to X-axis (constraint stays on)

Long Hold overrides the constrain state:

  • Holding F5 temporarily activates X-axis constraint
  • Releasing F5 restores previous state (either constraint off or the previously selected axis)
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Toolbars;
using UnityEditor.Overlays;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.ShortcutManagement;

[Overlay(typeof(SceneView), "AxisConstrain", "Axis Constrain", true)]
public class AxisConstrainOverlay : ToolbarOverlay
{
    public static Dictionary<Transform, Vector3> initPositions = new Dictionary<Transform, Vector3>();
    public static Texture2D icon;
    AxisConstrainOverlay() : base(
        AxisDropDown.id
        )
    {
        icon = Icon();
        collapsedIcon = icon;
    }

    public static Texture2D Icon ()
    {
        //Build the icon to prevent needing an asset for it. Oh well...
        Color[] txColor = new Color[256];
        txColor[0] = new Color(1f, 1f, 1f, 0f);
        txColor[1] = new Color(1f, 1f, 1f, 0f);
        txColor[2] = new Color(1f, 1f, 1f, 0f);
        txColor[3] = new Color(1f, 1f, 1f, 0f);
        txColor[4] = new Color(1f, 1f, 1f, 0f);
        txColor[5] = new Color(1f, 1f, 1f, 0f);
        txColor[6] = new Color(1f, 1f, 1f, 0f);
        txColor[7] = new Color(1f, 1f, 1f, 0f);
        txColor[8] = new Color(1f, 1f, 1f, 0f);
        txColor[9] = new Color(1f, 1f, 1f, 0f);
        txColor[10] = new Color(1f, 1f, 1f, 0f);
        txColor[11] = new Color(1f, 1f, 1f, 0f);
        txColor[12] = new Color(1f, 1f, 1f, 0f);
        txColor[13] = new Color(1f, 1f, 1f, 0f);
        txColor[14] = new Color(1f, 1f, 1f, 0f);
        txColor[15] = new Color(1f, 1f, 1f, 0f);
        txColor[16] = new Color(1f, 1f, 1f, 0f);
        txColor[17] = new Color(1f, 1f, 1f, 0f);
        txColor[18] = new Color(1f, 1f, 1f, 0f);
        txColor[19] = new Color(1f, 1f, 1f, 0f);
        txColor[20] = new Color(1f, 1f, 1f, 0f);
        txColor[21] = new Color(1f, 1f, 1f, 0f);
        txColor[22] = new Color(1f, 1f, 1f, 0f);
        txColor[23] = new Color(1f, 1f, 1f, 0f);
        txColor[24] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[25] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[26] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[27] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[28] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[29] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[30] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[31] = new Color(1f, 1f, 1f, 0f);
        txColor[32] = new Color(1f, 1f, 1f, 0f);
        txColor[33] = new Color(1f, 1f, 1f, 0f);
        txColor[34] = new Color(1f, 1f, 1f, 0f);
        txColor[35] = new Color(1f, 1f, 1f, 0f);
        txColor[36] = new Color(1f, 1f, 1f, 0f);
        txColor[37] = new Color(1f, 1f, 1f, 0f);
        txColor[38] = new Color(1f, 1f, 1f, 0f);
        txColor[39] = new Color(1f, 1f, 1f, 0f);
        txColor[40] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[41] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[42] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[43] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[44] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[45] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[46] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[47] = new Color(1f, 1f, 1f, 0f);
        txColor[48] = new Color(1f, 1f, 1f, 0f);
        txColor[49] = new Color(1f, 1f, 1f, 0f);
        txColor[50] = new Color(1f, 1f, 1f, 0f);
        txColor[51] = new Color(1f, 1f, 1f, 0f);
        txColor[52] = new Color(1f, 1f, 1f, 0f);
        txColor[53] = new Color(1f, 1f, 1f, 0f);
        txColor[54] = new Color(1f, 1f, 1f, 0f);
        txColor[55] = new Color(1f, 1f, 1f, 0f);
        txColor[56] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[57] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[58] = new Color(1f, 1f, 1f, 0.03921569f);
        txColor[59] = new Color(1f, 1f, 1f, 0.6078432f);
        txColor[60] = new Color(1f, 1f, 1f, 0.8352942f);
        txColor[61] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[62] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[63] = new Color(1f, 1f, 1f, 0f);
        txColor[64] = new Color(1f, 1f, 1f, 0f);
        txColor[65] = new Color(1f, 1f, 1f, 0.2941177f);
        txColor[66] = new Color(1f, 1f, 1f, 0.1960784f);
        txColor[67] = new Color(1f, 1f, 1f, 0f);
        txColor[68] = new Color(1f, 1f, 1f, 0f);
        txColor[69] = new Color(1f, 1f, 1f, 0f);
        txColor[70] = new Color(1f, 1f, 1f, 0f);
        txColor[71] = new Color(1f, 1f, 1f, 0f);
        txColor[72] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[73] = new Color(1f, 1f, 1f, 0.1960784f);
        txColor[74] = new Color(1f, 1f, 1f, 0.8470589f);
        txColor[75] = new Color(1f, 1f, 1f, 0.5882353f);
        txColor[76] = new Color(1f, 1f, 1f, 0.03921569f);
        txColor[77] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[78] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[79] = new Color(1f, 1f, 1f, 0f);
        txColor[80] = new Color(1f, 1f, 1f, 0f);
        txColor[81] = new Color(1f, 1f, 1f, 0.282353f);
        txColor[82] = new Color(1f, 1f, 1f, 0.854902f);
        txColor[83] = new Color(1f, 1f, 1f, 0.7607844f);
        txColor[84] = new Color(1f, 1f, 1f, 0.3333333f);
        txColor[85] = new Color(1f, 1f, 1f, 0.01568628f);
        txColor[86] = new Color(1f, 1f, 1f, 0f);
        txColor[87] = new Color(1f, 1f, 1f, 0.003921569f);
        txColor[88] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[89] = new Color(1f, 1f, 1f, 0.8980393f);
        txColor[90] = new Color(1f, 1f, 1f, 0.3058824f);
        txColor[91] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[92] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[93] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[94] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[95] = new Color(1f, 1f, 1f, 0f);
        txColor[96] = new Color(1f, 1f, 1f, 0f);
        txColor[97] = new Color(1f, 1f, 1f, 0f);
        txColor[98] = new Color(1f, 1f, 1f, 0.007843138f);
        txColor[99] = new Color(1f, 1f, 1f, 0.2941177f);
        txColor[100] = new Color(1f, 1f, 1f, 0.7254902f);
        txColor[101] = new Color(1f, 1f, 1f, 0.8862746f);
        txColor[102] = new Color(1f, 1f, 1f, 0.4784314f);
        txColor[103] = new Color(1f, 1f, 1f, 0.7333333f);
        txColor[104] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[105] = new Color(1f, 1f, 1f, 0.1019608f);
        txColor[106] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[107] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[108] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[109] = new Color(1f, 0.4313726f, 0.2509804f, 0f);
        txColor[110] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[111] = new Color(1f, 1f, 1f, 0f);
        txColor[112] = new Color(1f, 1f, 1f, 0f);
        txColor[113] = new Color(1f, 1f, 1f, 0f);
        txColor[114] = new Color(1f, 1f, 1f, 0f);
        txColor[115] = new Color(1f, 1f, 1f, 0f);
        txColor[116] = new Color(1f, 1f, 1f, 0f);
        txColor[117] = new Color(1f, 1f, 1f, 0.1568628f);
        txColor[118] = new Color(1f, 1f, 1f, 0.5490196f);
        txColor[119] = new Color(1f, 1f, 1f, 0.937255f);
        txColor[120] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[121] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[122] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[123] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[124] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[125] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[126] = new Color(1f, 0.4313726f, 0.2509804f, 1f);
        txColor[127] = new Color(1f, 1f, 1f, 0f);
        txColor[128] = new Color(1f, 1f, 1f, 0f);
        txColor[129] = new Color(1f, 1f, 1f, 0f);
        txColor[130] = new Color(1f, 1f, 1f, 0f);
        txColor[131] = new Color(1f, 1f, 1f, 0f);
        txColor[132] = new Color(1f, 1f, 1f, 0f);
        txColor[133] = new Color(1f, 1f, 1f, 0f);
        txColor[134] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[135] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[136] = new Color(1f, 1f, 1f, 0f);
        txColor[137] = new Color(1f, 1f, 1f, 0f);
        txColor[138] = new Color(1f, 1f, 1f, 0f);
        txColor[139] = new Color(1f, 1f, 1f, 0f);
        txColor[140] = new Color(1f, 1f, 1f, 0f);
        txColor[141] = new Color(1f, 1f, 1f, 0f);
        txColor[142] = new Color(1f, 1f, 1f, 0f);
        txColor[143] = new Color(1f, 1f, 1f, 0f);
        txColor[144] = new Color(1f, 1f, 1f, 0f);
        txColor[145] = new Color(1f, 1f, 1f, 0f);
        txColor[146] = new Color(1f, 1f, 1f, 0f);
        txColor[147] = new Color(1f, 1f, 1f, 0f);
        txColor[148] = new Color(1f, 1f, 1f, 0f);
        txColor[149] = new Color(1f, 1f, 1f, 0f);
        txColor[150] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[151] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[152] = new Color(1f, 1f, 1f, 0f);
        txColor[153] = new Color(1f, 1f, 1f, 0f);
        txColor[154] = new Color(1f, 1f, 1f, 0f);
        txColor[155] = new Color(1f, 1f, 1f, 0f);
        txColor[156] = new Color(1f, 1f, 1f, 0f);
        txColor[157] = new Color(1f, 1f, 1f, 0f);
        txColor[158] = new Color(1f, 1f, 1f, 0f);
        txColor[159] = new Color(1f, 1f, 1f, 0f);
        txColor[160] = new Color(1f, 1f, 1f, 0f);
        txColor[161] = new Color(1f, 1f, 1f, 0f);
        txColor[162] = new Color(1f, 1f, 1f, 0f);
        txColor[163] = new Color(1f, 1f, 1f, 0f);
        txColor[164] = new Color(1f, 1f, 1f, 0f);
        txColor[165] = new Color(1f, 1f, 1f, 0f);
        txColor[166] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[167] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[168] = new Color(1f, 1f, 1f, 0f);
        txColor[169] = new Color(1f, 1f, 1f, 0f);
        txColor[170] = new Color(1f, 1f, 1f, 0f);
        txColor[171] = new Color(1f, 1f, 1f, 0f);
        txColor[172] = new Color(1f, 1f, 1f, 0f);
        txColor[173] = new Color(1f, 1f, 1f, 0f);
        txColor[174] = new Color(1f, 1f, 1f, 0f);
        txColor[175] = new Color(1f, 1f, 1f, 0f);
        txColor[176] = new Color(1f, 1f, 1f, 0f);
        txColor[177] = new Color(1f, 1f, 1f, 0f);
        txColor[178] = new Color(1f, 1f, 1f, 0f);
        txColor[179] = new Color(1f, 1f, 1f, 0f);
        txColor[180] = new Color(1f, 1f, 1f, 0f);
        txColor[181] = new Color(1f, 1f, 1f, 0f);
        txColor[182] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[183] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[184] = new Color(1f, 1f, 1f, 0f);
        txColor[185] = new Color(1f, 1f, 1f, 0f);
        txColor[186] = new Color(1f, 1f, 1f, 0f);
        txColor[187] = new Color(1f, 1f, 1f, 0f);
        txColor[188] = new Color(1f, 1f, 1f, 0f);
        txColor[189] = new Color(1f, 1f, 1f, 0f);
        txColor[190] = new Color(1f, 1f, 1f, 0f);
        txColor[191] = new Color(1f, 1f, 1f, 0f);
        txColor[192] = new Color(1f, 1f, 1f, 0f);
        txColor[193] = new Color(1f, 1f, 1f, 0f);
        txColor[194] = new Color(1f, 1f, 1f, 0f);
        txColor[195] = new Color(1f, 1f, 1f, 0f);
        txColor[196] = new Color(1f, 1f, 1f, 0f);
        txColor[197] = new Color(1f, 1f, 1f, 0f);
        txColor[198] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[199] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[200] = new Color(1f, 1f, 1f, 0f);
        txColor[201] = new Color(1f, 1f, 1f, 0f);
        txColor[202] = new Color(1f, 1f, 1f, 0f);
        txColor[203] = new Color(1f, 1f, 1f, 0f);
        txColor[204] = new Color(1f, 1f, 1f, 0f);
        txColor[205] = new Color(1f, 1f, 1f, 0f);
        txColor[206] = new Color(1f, 1f, 1f, 0f);
        txColor[207] = new Color(1f, 1f, 1f, 0f);
        txColor[208] = new Color(1f, 1f, 1f, 0f);
        txColor[209] = new Color(1f, 1f, 1f, 0f);
        txColor[210] = new Color(1f, 1f, 1f, 0f);
        txColor[211] = new Color(1f, 1f, 1f, 0f);
        txColor[212] = new Color(1f, 1f, 1f, 0f);
        txColor[213] = new Color(1f, 1f, 1f, 0f);
        txColor[214] = new Color(1f, 1f, 1f, 0.05882353f);
        txColor[215] = new Color(1f, 1f, 1f, 0.9411765f);
        txColor[216] = new Color(1f, 1f, 1f, 0f);
        txColor[217] = new Color(1f, 1f, 1f, 0f);
        txColor[218] = new Color(1f, 1f, 1f, 0f);
        txColor[219] = new Color(1f, 1f, 1f, 0f);
        txColor[220] = new Color(1f, 1f, 1f, 0f);
        txColor[221] = new Color(1f, 1f, 1f, 0f);
        txColor[222] = new Color(1f, 1f, 1f, 0f);
        txColor[223] = new Color(1f, 1f, 1f, 0f);
        txColor[224] = new Color(1f, 1f, 1f, 0f);
        txColor[225] = new Color(1f, 1f, 1f, 0f);
        txColor[226] = new Color(1f, 1f, 1f, 0f);
        txColor[227] = new Color(1f, 1f, 1f, 0f);
        txColor[228] = new Color(1f, 1f, 1f, 0f);
        txColor[229] = new Color(1f, 1f, 1f, 0f);
        txColor[230] = new Color(1f, 1f, 1f, 0.03137255f);
        txColor[231] = new Color(1f, 1f, 1f, 0.4705883f);
        txColor[232] = new Color(1f, 1f, 1f, 0f);
        txColor[233] = new Color(1f, 1f, 1f, 0f);
        txColor[234] = new Color(1f, 1f, 1f, 0f);
        txColor[235] = new Color(1f, 1f, 1f, 0f);
        txColor[236] = new Color(1f, 1f, 1f, 0f);
        txColor[237] = new Color(1f, 1f, 1f, 0f);
        txColor[238] = new Color(1f, 1f, 1f, 0f);
        txColor[239] = new Color(1f, 1f, 1f, 0f);
        txColor[240] = new Color(1f, 1f, 1f, 0f);
        txColor[241] = new Color(1f, 1f, 1f, 0f);
        txColor[242] = new Color(1f, 1f, 1f, 0f);
        txColor[243] = new Color(1f, 1f, 1f, 0f);
        txColor[244] = new Color(1f, 1f, 1f, 0f);
        txColor[245] = new Color(1f, 1f, 1f, 0f);
        txColor[246] = new Color(1f, 1f, 1f, 0f);
        txColor[247] = new Color(1f, 1f, 1f, 0f);
        txColor[248] = new Color(1f, 1f, 1f, 0f);
        txColor[249] = new Color(1f, 1f, 1f, 0f);
        txColor[250] = new Color(1f, 1f, 1f, 0f);
        txColor[251] = new Color(1f, 1f, 1f, 0f);
        txColor[252] = new Color(1f, 1f, 1f, 0f);
        txColor[253] = new Color(1f, 1f, 1f, 0f);
        txColor[254] = new Color(1f, 1f, 1f, 0f);
        txColor[255] = new Color(1f, 1f, 1f, 0f);
        Texture2D iconTx = new Texture2D(16, 16, TextureFormat.ARGB32, false);
        iconTx.filterMode = FilterMode.Point;
        iconTx.wrapMode = TextureWrapMode.Clamp;
        iconTx.SetPixels(txColor);
        iconTx.Apply();

        return iconTx;
    }
}

[EditorToolbarElement(id, typeof(SceneView))]
class AxisDropDown : EditorToolbarDropdownToggle, IAccessContainerWindow
{

    public const string id = "AxisConstrain/Axis";
    public EditorWindow containerWindow { get; set; }
    public bool constrainActive;
    static int axisIndex = 0;
    static readonly string[] axisName = new string[] { "x", "y", "z" };

    UndoPropertyModification[] changedMods = null;
    static readonly Color[] btnColors = new Color[] { new Color(0.3686275f, 0.1254902f, 0.1341253f), new Color(0.162876f, 0.3679245f, 0.126691f), new Color(0.1460929f, 0.19f, 0.4622641f) };

    public AxisDropDown()
    {
        //Load some values from registry to make tool persistent in editor load instances.
        SetActive(EditorPrefs.GetBool("AxisConstrainActive", false), EditorPrefs.GetInt("AxisConstrainAxis", 0));

        //Subscribe the dropdown function
        dropdownClicked += ShowMenu;
        //Subscribe the toggle click
        this.RegisterValueChangedCallback(SetConstrain);
        //Subscribe to scene view changes
        SceneView.duringSceneGui -= ConstrainPositions;
        SceneView.duringSceneGui += ConstrainPositions;

        icon = AxisConstrainOverlay.icon;
    }

    void ConstrainPositions(SceneView view)
    {
        // Sync state from EditorPrefs FIRST (updated by shortcuts)
        bool prefActive = EditorPrefs.GetBool("AxisConstrainActive", false);
        int prefAxis = EditorPrefs.GetInt("AxisConstrainAxis", 0);
        if (constrainActive != prefActive || axisIndex != prefAxis)
        {
            constrainActive = prefActive;
            axisIndex = prefAxis;
            SetValueWithoutNotify(constrainActive);
            SetText();

            // Update event handlers based on new state
            ObjectChangeEvents.changesPublished -= ObjectChangeEventsHandler;
            Undo.postprocessModifications -= PostprocessModifications;
            if (constrainActive)
            {
                ObjectChangeEvents.changesPublished += ObjectChangeEventsHandler;
                Undo.postprocessModifications += PostprocessModifications;
            }
        }

        //Only run when visible
        if (containerWindow != null && containerWindow.TryGetOverlay("AxisConstrain", out Overlay result))
        {
            if (!result.displayed)
            {
                SceneView.duringSceneGui -= ConstrainPositions;
                ObjectChangeEvents.changesPublished -= ObjectChangeEventsHandler;
                Undo.postprocessModifications -= PostprocessModifications;
                return;
            }
        }

        if (view != containerWindow)
        {
            return;
        }

        if (constrainActive)
        {
            //Convert curretn rotation handle to its matrix form, so we can grab an axis
            Matrix4x4 handleOrientation = Matrix4x4.TRS(Vector3.zero, Tools.handleRotation, Vector3.one);
            Vector3 compareAxis = handleOrientation.GetColumn(axisIndex).normalized;

            //Do to rounding errors in floats, constraining with initial value before moving is best.
            foreach (KeyValuePair<Transform, Vector3> entry in AxisConstrainOverlay.initPositions)
            {
                //Delta vector of current local position vs initial local position when action started
                Vector3 moveVector = entry.Key.localPosition - entry.Value;

                if (entry.Key.parent != null)
                {
                    //The changed property is in local coordinates. Transform to world using parent
                    moveVector = entry.Key.parent.localToWorldMatrix.MultiplyPoint3x4(moveVector) - entry.Key.parent.position;
                }
                //Project the delta vector on to the constrain axis (In world space)
                Vector3 modMoveVector = Vector3.Project(moveVector, compareAxis);

                //Offset the transform by how the projected vector differs from the delta vector
                entry.Key.position += modMoveVector - moveVector;
            }
        }

        if (Event.current != null && Event.current.type == EventType.MouseUp)
        {
            //Clear all init positions on MouseUp, to prepare for next move action
            AxisConstrainOverlay.initPositions.Clear();
        }
    }

    void SetConstrain(ChangeEvent<bool> toggle)
    {
        //Save toggle state in both var and registry and appearance
        SetActive(toggle.newValue, axisIndex);
    }

    public UndoPropertyModification[] PostprocessModifications(UndoPropertyModification[] modifications)
    {
        //Save this undo event to make sure we have the initial values of the objects being moved
        changedMods = modifications;
        return modifications;
    }

    public void ObjectChangeEventsHandler(ref ObjectChangeEventStream stream)
    {
        int changes = stream.length;

        for (int i = 0; i < changes && changedMods != null && changedMods.Length > 0; i++)
        {
            //Find the "kind" of change i
            ObjectChangeKind changeKind = stream.GetEventType(i);
            if (changeKind == ObjectChangeKind.ChangeGameObjectOrComponentProperties)
            {
                //"Kind" is the one we are looking for: Component changes.
                //Grab the data of this change
                stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out ChangeGameObjectOrComponentPropertiesEventArgs data);

                //Get the Object of this change based on instanceID
                Object instance = EditorUtility.InstanceIDToObject(data.instanceId);

                if (instance is Transform)
                {
                    //Changed object is of type: Transform
                    Transform changedTransform = instance as Transform;

                    //Check if we have this object stored already!
                    if (!AxisConstrainOverlay.initPositions.ContainsKey(changedTransform))
                    {
                        //Create and initialize initPos with local positions of changed transform (So that components of the Vector3 localPosition that are NOT changed are still saved)
                        Vector3 initPos;
                        initPos = changedTransform.localPosition;

                        //Loop through undo ops and find one that matches this change
                        for (int j = 0; j < changedMods.Length; j++)
                        {
                            UndoPropertyModification currentMod = changedMods[j];

                            if (currentMod.previousValue.target == changedTransform)
                            {
                                if (float.TryParse(currentMod.previousValue.value, out float valueAsFloat))
                                {
                                    //Value is a float. Find the vector component by the property path
                                    if (currentMod.previousValue.propertyPath == "m_LocalPosition.x")
                                    {
                                        initPos.x = valueAsFloat;
                                    }
                                    else if (currentMod.previousValue.propertyPath == "m_LocalPosition.y")
                                    {
                                        initPos.y = valueAsFloat;
                                    }
                                    else if (currentMod.previousValue.propertyPath == "m_LocalPosition.z")
                                    {
                                        initPos.z = valueAsFloat;
                                    }
                                }
                            }
                        }
                        //Store init value in dict.
                        AxisConstrainOverlay.initPositions.Add(changedTransform, initPos);
                    }
                }
            }
        }
    }

    void ShowMenu()
    {
        //Create and show drop down of toggle element. Add actions on value change
        GenericMenu menu = new GenericMenu();
        menu.AddItem(new GUIContent("x-axis"), axisIndex == 0, () => { SetActive(true, 0); });
        menu.AddItem(new GUIContent("y-axis"), axisIndex == 1, () => { SetActive(true, 1); });
        menu.AddItem(new GUIContent("z-axis"), axisIndex == 2, () => { SetActive(true, 2); });
        menu.ShowAsContext();
    }

    void SetActive (bool active, int axis)
    {
        constrainActive = active;
        EditorPrefs.SetBool("AxisConstrainActive", constrainActive);
        SetValueWithoutNotify(constrainActive);
        axisIndex = axis;
        EditorPrefs.SetInt("AxisConstrainAxis", axisIndex);
        SetText();

        ObjectChangeEvents.changesPublished -= ObjectChangeEventsHandler;
        Undo.postprocessModifications -= PostprocessModifications;
        if (active)
        {
            ObjectChangeEvents.changesPublished += ObjectChangeEventsHandler;
            Undo.postprocessModifications += PostprocessModifications;
        }
    }

    void SetText ()
    {
        //Change text based on if active...
        if (constrainActive)
        {
            text = "<color=#" + ColorUtility.ToHtmlStringRGB(btnColors[axisIndex] * 4) + "><b>Constrain to " + axisName[axisIndex] + "-axis";
        }
        else
        {
            text = "<b>Axis Constrain Off";
        }
    }

    // Shortcuts - tap to toggle/switch axis, hold for temporary activation
    [ClutchShortcut("Scene View/Axis Constrain/X-Axis", KeyCode.F5)]
    static void ShortcutConstrainX(ShortcutArguments args)
    {
        HandleShortcutClutch(args, 0);
    }

    [ClutchShortcut("Scene View/Axis Constrain/Y-Axis", KeyCode.F6)]
    static void ShortcutConstrainY(ShortcutArguments args)
    {
        HandleShortcutClutch(args, 1);
    }

    [ClutchShortcut("Scene View/Axis Constrain/Z-Axis", KeyCode.F7)]
    static void ShortcutConstrainZ(ShortcutArguments args)
    {
        HandleShortcutClutch(args, 2);
    }

    static bool shortcutWasActive = false;
    static int shortcutPreviousAxis = 0;
    static float shortcutBeginTime = 0f;
    const float TAP_THRESHOLD = 0.2f; // 200ms threshold for tap vs hold

    static void HandleShortcutClutch(ShortcutArguments args, int axis)
    {
        if (args.stage == ShortcutStage.Begin)
        {
            // Save current state and time
            shortcutWasActive = EditorPrefs.GetBool("AxisConstrainActive", false);
            shortcutPreviousAxis = EditorPrefs.GetInt("AxisConstrainAxis", 0);
            shortcutBeginTime = (float)EditorApplication.timeSinceStartup;

            // Activate constraint immediately
            EditorPrefs.SetBool("AxisConstrainActive", true);
            EditorPrefs.SetInt("AxisConstrainAxis", axis);

            // Force immediate update on all scene views
            foreach (SceneView sv in SceneView.sceneViews)
            {
                sv.Repaint();
            }
        }
        else if (args.stage == ShortcutStage.End)
        {
            float duration = (float)EditorApplication.timeSinceStartup - shortcutBeginTime;

            // Check if this was a quick tap
            if (duration < TAP_THRESHOLD)
            {
                // Quick tap - toggle behavior
                if (shortcutWasActive && shortcutPreviousAxis == axis)
                {
                    // Was already active on this axis - turn off
                    EditorPrefs.SetBool("AxisConstrainActive", false);
                }
                else
                {
                    // Was off or on different axis - keep it on with new axis (already set in Begin)
                    // Do nothing, leave it active
                }
            }
            else
            {
                // Long hold - restore previous state (clutch behavior)
                EditorPrefs.SetBool("AxisConstrainActive", shortcutWasActive);
                EditorPrefs.SetInt("AxisConstrainAxis", shortcutPreviousAxis);
            }

            // Force immediate update on all scene views
            foreach (SceneView sv in SceneView.sceneViews)
            {
                sv.Repaint();
            }
        }
    }
}