using System;
using System.Linq;
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Tilemaps;
using Object = UnityEngine.Object;
namespace UnityEditor.Tilemaps
{
///
/// Abstract class for Editor Tool used to handle a GridSelection.
///
public abstract class GridSelectionTool : EditorTool
{
private TileBase[] m_SelectionTiles;
private TileFlags[] m_SelectionFlagsArray;
private Vector3[] m_SelectionPositions;
private Quaternion[] m_SelectionRotations;
private Vector3[] m_SelectionScales;
private int m_FirstCellWithTile;
// Gizmo state across ticks of a single drag. m_Selection* hold the drag-start anchors
// (frozen during a drag); these hold what the handle was last set to so we hand it back
// next tick. Anchoring deltas against drag-start (rather than the previous tick) means
// momentarily passing scale through 0 doesn't seed a divide-by-zero in the next divide.
private bool m_Dragging;
private Vector3 m_DragGizmoP;
private Quaternion m_DragGizmoR;
private Vector3 m_DragGizmoS;
private int selectionCellCount => Math.Abs(GridSelection.position.size.x * GridSelection.position.size.y * GridSelection.position.size.z);
///
/// Does the GUI for the GridSelectionTool for an EditorWindow.
///
/// EditorWindow which GUI is being done.
public override void OnToolGUI(EditorWindow window)
{
var selection = Selection.activeObject as GridSelection;
if (selection == null)
return;
if (window is SceneView && GridSelection.target != null && GridPaintingState.IsPartOfActivePalette(GridSelection.target))
return;
OnToolGUI();
}
internal void OnToolGUI()
{
if (GridSelection.target == null)
return;
var brushTarget = GridSelection.target;
var tilemap = brushTarget.GetComponent();
if (tilemap == null)
return;
UpdateSelection(tilemap);
if (m_SelectionFlagsArray == null || m_SelectionFlagsArray.Length <= 0)
return;
bool transformFlagsAllEqual = m_SelectionFlagsArray.All(flags => (flags & TileFlags.LockTransform) == (m_SelectionFlagsArray.First() & TileFlags.LockTransform));
if (!transformFlagsAllEqual || (m_SelectionFlagsArray[0] & TileFlags.LockTransform) != 0)
return;
// End any prior drag once no handle is held, so the next drag re-anchors against fresh state.
if (GUIUtility.hotControl == 0)
m_Dragging = false;
var index = m_FirstCellWithTile != -1 ? m_FirstCellWithTile : 0;
var startP = m_SelectionPositions[index];
var startR = m_SelectionRotations[index];
var startS = m_SelectionScales[index];
var p = m_Dragging ? m_DragGizmoP : startP;
var r = m_Dragging ? m_DragGizmoR : startR;
var s = m_Dragging ? m_DragGizmoS : startS;
Vector3 selectionPosition = GridSelection.position.position;
selectionPosition += tilemap.tileAnchor;
if (selectionCellCount > 1)
{
selectionPosition.x = GridSelection.position.center.x;
selectionPosition.y = GridSelection.position.center.y;
}
var anchorWorld = tilemap.LocalToWorld(tilemap.CellToLocalInterpolated(selectionPosition));
var gizmoPosition = anchorWorld + p;
var dragStartGizmoPosition = anchorWorld + startP;
EditorGUI.BeginChangeCheck();
HandleTool(ref gizmoPosition, ref r, ref s);
if (EditorGUI.EndChangeCheck())
{
Undo.RegisterCompleteObjectUndo(new Object[] { tilemap, tilemap.gameObject }, "Move");
// Deltas measured against drag-start, not the previous tick — so passing scale
// through 0 doesn't seed a divide-by-zero on the next tick. Falling back to 1 on
// a zero start-axis freezes that axis (no meaningful ratio exists from 0).
var deltaPos = gizmoPosition - dragStartGizmoPosition;
var deltaRotation = (Quaternion.Inverse(startR) * r).normalized;
var deltaScale = new Vector3(
startS.x != 0f ? s.x / startS.x : 1f,
startS.y != 0f ? s.y / startS.y : 1f,
startS.z != 0f ? s.z / startS.z : 1f);
m_Dragging = true;
m_DragGizmoP = gizmoPosition - anchorWorld;
m_DragGizmoR = r;
m_DragGizmoS = s;
int cellIndex = 0;
foreach (var cellPosition in GridSelection.position.allPositionsWithin)
{
if (tilemap.HasTile(cellPosition))
{
var newPosition = m_SelectionPositions[cellIndex] + deltaPos;
var newRotation = (m_SelectionRotations[cellIndex] * deltaRotation).normalized;
var newScale = new Vector3(
m_SelectionScales[cellIndex].x * deltaScale.x,
m_SelectionScales[cellIndex].y * deltaScale.y,
m_SelectionScales[cellIndex].z * deltaScale.z);
// Matrix4x4.ValidTRS only validates the matrix's bottom row (which TRS
// always fills with [0,0,0,1]), so we have to check finiteness ourselves
// to catch NaN/Inf propagation from upstream deltas.
if (float.IsFinite(newPosition.x) && float.IsFinite(newPosition.y) && float.IsFinite(newPosition.z)
&& float.IsFinite(newRotation.x) && float.IsFinite(newRotation.y) && float.IsFinite(newRotation.z) && float.IsFinite(newRotation.w)
&& float.IsFinite(newScale.x) && float.IsFinite(newScale.y) && float.IsFinite(newScale.z))
{
var trs = Matrix4x4.TRS(newPosition, newRotation, newScale);
tilemap.SetTransformMatrix(cellPosition, trs);
}
}
cellIndex++;
}
InspectorWindow.RepaintAllInspectors();
}
}
///
/// Handles the gizmo for the GridSelectionTool.
/// Implement this the handle the gizmo for the GridSelectionTool.
///
/// Position of the GridSelection gizmo.
/// Rotation of the GridSelection gizmo.
/// Scale of the GridSelection gizmo.
public abstract void HandleTool(ref Vector3 position, ref Quaternion rotation, ref Vector3 scale);
internal static bool IsActive()
{
return ToolManager.activeToolType != null && ToolManager.activeToolType.IsSubclassOf(typeof(GridSelectionTool));
}
private void UpdateSelection(Tilemap tilemap)
{
var cellCount = selectionCellCount;
// Skip during an active handle drag — the TRS shadow (m_SelectionPositions/Rotations/
// Scales) accumulates per-tick deltas in place, and re-reading the matrix here would
// overwrite it with an ambiguous decomposition of the negative-scale matrix we wrote.
if (GUIUtility.hotControl != 0
&& m_SelectionTiles != null && m_SelectionTiles.Length == cellCount
&& m_SelectionPositions != null && m_SelectionPositions.Length == cellCount
&& m_SelectionRotations != null && m_SelectionRotations.Length == cellCount
&& m_SelectionScales != null && m_SelectionScales.Length == cellCount)
{
return;
}
m_FirstCellWithTile = -1;
var selection = GridSelection.position;
if (m_SelectionTiles == null || m_SelectionTiles.Length != cellCount)
m_SelectionTiles = new TileBase[cellCount];
if (m_SelectionFlagsArray == null || m_SelectionFlagsArray.Length != cellCount)
m_SelectionFlagsArray = new TileFlags[cellCount];
if (m_SelectionPositions == null || m_SelectionPositions.Length != cellCount)
m_SelectionPositions = new Vector3[cellCount];
if (m_SelectionRotations == null || m_SelectionRotations.Length != cellCount)
m_SelectionRotations = new Quaternion[cellCount];
if (m_SelectionScales == null || m_SelectionScales.Length != cellCount)
m_SelectionScales = new Vector3[cellCount];
int index = 0;
foreach (var p in selection.allPositionsWithin)
{
m_SelectionTiles[index] = tilemap.GetTile(p);
var matrix = tilemap.GetTransformMatrix(p);
m_SelectionFlagsArray[index] = tilemap.GetTileFlags(p);
m_SelectionPositions[index] = matrix.GetPosition();
m_SelectionRotations[index] = matrix.rotation;
m_SelectionScales[index] = matrix.lossyScale;
if (m_FirstCellWithTile == -1 && m_SelectionTiles[index] != null)
m_FirstCellWithTile = index;
index++;
}
}
}
}