Protfolio-Emanuel-Polsky / Assets / _Project / Code / VerletIntegration / Editor / CreatePoints.cs
CreatePoints.cs
Raw
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Authoring;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

namespace GarmentButton.VerletIntegration.CustomEdit
{
    [CustomEditor(typeof(BakePoints))]
    public partial class CreatePoints : Editor
    {
        private const string GridModeOnlyClassName = "grid-mode-only";
        private const string GridOnlyPresetClassName = "grid-only-preset";
        private const float DefaultHandleDistance = 4f;
        private const float StickLineWidth = 3f;
        private const float SelectedStickLineWidth = 5f;
        private const float GroundRaycastSkin = 0.05f;
        private const float GroundCollisionPadding = 0.02f;
        private const int SelectedLabelLimit = 24;
        private const int GroundBoundsSampleResolution = 3;

        private static readonly Color FreePointColor = new Color(1f, 0.25f, 0.18f, 1f);
        private static readonly Color LockedPointColor = new Color(0.12f, 0.45f, 1f, 1f);
        private static readonly Color SelectedPointColor = new Color(1f, 0.83f, 0.18f, 1f);
        private static readonly Color StickColor = new Color(1f, 1f, 1f, 0.42f);
        private static readonly Color LockedStickColor = new Color(0.15f, 0.55f, 1f, 0.56f);
        private static readonly Color GridHandleColor = new Color(0.25f, 0.95f, 0.55f, 1f);
        private static readonly Color BoundColor = new Color(1f, 1f, 1f, 0.75f);
        private static readonly Color SurfaceColor = new Color(0.25f, 0.55f, 1f, 0.06f);

        public PointEditMode EditMode = PointEditMode.Move;

        private MeshModeState _meshMode;

        private bool _isBoxSelecting;
        private Vector2 _dragStart;
        private Vector2 _dragEnd;

        private readonly HashSet<int> _selectedPoints = new HashSet<int>();
        private Vector3[] _worldPointCache = Array.Empty<Vector3>();
        private bool[] _pointMaskCache = Array.Empty<bool>();
        private static GUIStyle _sceneLabelStyle;

        public enum PointEditMode
        {
            None,
            Move,
            Lock
        }

        private enum PointLockPreset
        {
            None,
            FlagRaceSides,
            HangingClothTop
        }

        private enum PreviewOverlay
        {
            Surface,
            Sticks,
            Bounds,
            Hud,
            Labels
        }

        private enum PointSelectionRegion
        {
            TopEdge,
            BottomEdge,
            Border
        }

        private readonly struct MeshModeState
        {
            public readonly MeshFilter Filter;

            public MeshModeState(MeshFilter filter)
            {
                Filter = filter;
            }

            public bool HasFilter => Filter != null;
            public Mesh Mesh => Filter != null ? Filter.sharedMesh : null;
            public bool HasMesh => Mesh != null;
            public string MeshName => Mesh != null ? Mesh.name : string.Empty;
        }

        private readonly struct GroundBoundsHit
        {
            public readonly PhysicsShapeAuthoring Shape;
            public readonly Vector3 Point;

            public GroundBoundsHit(PhysicsShapeAuthoring shape, Vector3 point)
            {
                Shape = shape;
                Point = point;
            }
        }

        private void OnEnable()
        {
            CacheMeshMode();
            Undo.undoRedoPerformed += OnUndoRedo;
        }

        private void OnDisable()
        {
            Undo.undoRedoPerformed -= OnUndoRedo;
            ResetTransientSceneState();
            ResetInspectorReferences();
        }

        public void CreateDots()
        {
            CreateDots(PointLockPreset.None);
        }

        private void CreateDots(PointLockPreset lockPreset)
        {
            var bakePoints = target as BakePoints;
            if (bakePoints == null)
                return;

            CacheMeshMode();

            if (_meshMode.HasMesh)
            {
                BuildFromMesh(_meshMode.Mesh, lockPreset);
                return;
            }

            if (!ValidateGrid(bakePoints))
                return;

            var startHandle = GetHandleWorldPosition(bakePoints, 2);
            var endHandle = GetHandleWorldPosition(bakePoints, 1);
            var width = Vector3.Distance(startHandle, endHandle);
            if (width <= Mathf.Epsilon)
            {
                Debug.LogWarning("Create Points needs the two grid handles to be separated.", bakePoints);
                return;
            }

            Undo.RecordObject(bakePoints, "Generate Verlet Points");

            GenerateGridData(bakePoints, startHandle, endHandle, out var points, out var locked, out var triangles, out var sticks);

            bakePoints.Points = points;
            bakePoints.IsLocked = locked;
            bakePoints.Tringles = triangles;
            bakePoints.Sticks = sticks;
            ApplyLockPreset(bakePoints, lockPreset);

            _selectedPoints.Clear();
            EditorUtility.SetDirty(bakePoints);
            RefreshInspectorState();
            ShowSceneNotification(GetPresetNotification(lockPreset));
            SceneView.RepaintAll();
        }

        public void CreateBound()
        {
            var script = target as BakePoints;
            if (script == null)
                return;

            var shape = script.GetComponent<PhysicsShapeAuthoring>();
            if (shape == null || shape.ShapeType != ShapeType.Box)
            {
                Debug.LogWarning("Create Bound needs a PhysicsShapeAuthoring component set to Box.", script);
                return;
            }

            Undo.RecordObject(script, "Create Verlet Bound");

            var geometry = shape.GetBoxProperties();
            var half = geometry.Size * 0.5f;
            var rotation = new float3x3(geometry.Orientation);
            var absoluteRotation = new float3x3(
                math.abs(rotation.c0),
                math.abs(rotation.c1),
                math.abs(rotation.c2)
            );

            var extents = math.mul(absoluteRotation, half);
            script.MinmumBound = geometry.Center - extents;
            script.MaximumBound = geometry.Center + extents;

            EditorUtility.SetDirty(script);
            SceneView.RepaintAll();
        }

        public void FitBoundsToPhysicsGround()
        {
            var script = target as BakePoints;
            if (script == null)
                return;

            if (!TryGetAuthoringWorldBounds(script, out var worldBounds, out var localMin, out var localMax))
            {
                Debug.LogWarning("Fit Bounds To Ground needs existing bounds or generated points to define the raycast footprint.", script);
                return;
            }

            if (!TryFindPhysicsGroundHit(script, worldBounds, out var hit))
            {
                Debug.LogWarning("Fit Bounds To Ground did not find a PhysicsShapeAuthoring surface below the current bounds footprint.", script);
                ShowSceneNotification("No Physics Shape ground found");
                return;
            }

            var fittedLocalY = hit.Point.y - script.transform.position.y + GroundCollisionPadding;
            if (fittedLocalY >= localMax.y)
            {
                Debug.LogWarning("Fit Bounds To Ground found a surface above the current upper bound. Increase or move the bounds first.", script);
                return;
            }

            Undo.RecordObject(script, "Fit Verlet Bounds To Physics Ground");
            localMin.y = fittedLocalY;
            script.MinmumBound = localMin;
            script.MaximumBound = localMax;

            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification($"Bounds floor fitted to {hit.Shape.name}");
            SceneView.RepaintAll();
        }

        public void BuildFromMesh(Mesh mesh)
        {
            BuildFromMesh(mesh, PointLockPreset.None);
        }

        private void BuildFromMesh(Mesh mesh, PointLockPreset lockPreset)
        {
            var script = target as BakePoints;
            if (script == null)
                return;

            if (mesh == null)
            {
                Debug.LogWarning("Create Points could not find a mesh to read.", script);
                return;
            }

            var particlePositionsLocal = mesh.vertices;
            if (!CanStorePointCount(particlePositionsLocal.Length, script))
                return;

            var triangles = GetAllMeshTriangles(mesh);
            if (triangles.Length == 0)
            {
                Debug.LogWarning($"Mesh '{mesh.name}' has no triangles to convert into sticks.", script);
                return;
            }

            Undo.RecordObject(script, "Generate Verlet Points From Mesh");

            script.Points = particlePositionsLocal;
            script.IsLocked = new bool[particlePositionsLocal.Length];
            script.Tringles = triangles;
            script.Sticks = BuildSticksFromTriangles(particlePositionsLocal, triangles);
            ApplyLockPreset(script, lockPreset);

            _selectedPoints.Clear();
            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification(GetPresetNotification(lockPreset));
            SceneView.RepaintAll();
        }

        public void OnSceneGUI()
        {
            if (Application.isPlaying)
                return;

            var script = target as BakePoints;
            if (script == null)
                return;

            CacheMeshMode();
            EnsureLockArray(script, false);
            PruneSelection(script);
            var worldPoints = PrepareWorldPointCache(script);

            var selectionControlId = GUIUtility.GetControlID(FocusType.Passive);
            var currentEvent = Event.current;

            if (EditMode != PointEditMode.None && currentEvent.type == EventType.Layout)
                HandleUtility.AddDefaultControl(selectionControlId);

            if (EditMode != PointEditMode.None)
                HandleBoxSelection(currentEvent, selectionControlId, script, worldPoints);

            DrawScenePreview(script, worldPoints, currentEvent.type);
            DrawPointHandles(script, worldPoints);
            SceneHandles(script);
            DrawSelectionRectangle(currentEvent);
            DrawSceneHud(script);

            if (_isBoxSelecting)
                SceneView.RepaintAll();
        }

        private void DrawPointHandles(BakePoints script, Vector3[] worldPoints)
        {
            if (script.Points == null || script.Points.Length == 0)
                return;

            for (var i = 0; i < script.Points.Length; i++)
            {
                var pointPosition = worldPoints[i];
                var isSelected = _selectedPoints.Contains(i);

                Handles.color = GetPointColor(script, i, isSelected);
                if (isSelected)
                    DrawSelectionRing(pointPosition, Mathf.Max(0.02f, script.HandleSize * 1.45f));

                switch (EditMode)
                {
                    case PointEditMode.None:
                        if (Event.current.type == EventType.Repaint)
                            Handles.SphereHandleCap(0, pointPosition, Quaternion.identity, script.HandleSize, EventType.Repaint);
                        break;
                    case PointEditMode.Move:
                        MovingPoint(script, i, pointPosition);
                        break;
                    case PointEditMode.Lock:
                        LockingPoint(script, i, pointPosition);
                        break;
                }

                if (_drawSelectedLabels && isSelected && _selectedPoints.Count <= SelectedLabelLimit)
                    DrawPointLabel(pointPosition, i, script.HandleSize);
            }
        }

        private void MovingPoint(BakePoints script, int index, Vector3 position)
        {
            var newPosition = Handles.FreeMoveHandle(position, script.HandleSize, Vector3.zero, Handles.SphereHandleCap);

            if (newPosition == position)
                return;

            Undo.RecordObject(script, "Move Verlet Point");

            var offset = newPosition - position;
            if (_selectedPoints.Count > 0 && _selectedPoints.Contains(index))
            {
                foreach (var selectedIndex in _selectedPoints)
                {
                    script.Points[selectedIndex] += offset;
                    if (selectedIndex >= 0 && selectedIndex < _worldPointCache.Length)
                        _worldPointCache[selectedIndex] += offset;
                }

                UpdateStickLengthsForSelection(script);
            }
            else
            {
                script.Points[index] += offset;
                if (index >= 0 && index < _worldPointCache.Length)
                    _worldPointCache[index] += offset;
                UpdateStickLengthsForPoint(script, index);
            }

            EditorUtility.SetDirty(script);
            SceneView.RepaintAll();
        }

        private void LockingPoint(BakePoints script, int index, Vector3 position)
        {
            if (!Handles.Button(position, Quaternion.identity, script.HandleSize, script.HandleSize, Handles.SphereHandleCap))
                return;

            Undo.RecordObject(script, "Toggle Verlet Point Lock");
            var lockedState = !script.IsLocked[index];
            var changedCount = 1;

            if (_selectedPoints.Count > 0 && _selectedPoints.Contains(index))
            {
                changedCount = _selectedPoints.Count;
                foreach (var selectedIndex in _selectedPoints)
                    script.IsLocked[selectedIndex] = lockedState;
            }
            else
            {
                script.IsLocked[index] = lockedState;
            }

            _selectedPoints.Clear();
            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification($"{changedCount} point{(changedCount == 1 ? string.Empty : "s")} {(lockedState ? "locked" : "unlocked")}");
            SceneView.RepaintAll();
        }

        private void SceneHandles(BakePoints script)
        {
            if (_meshMode.HasMesh)
                return;

            var startHandle = GetHandleWorldPosition(script, 2);
            var endHandle = GetHandleWorldPosition(script, 1);

            Handles.color = GridHandleColor;
            Handles.DrawAAPolyLine(4f, startHandle, endHandle);
            Handles.Label(startHandle, "Start", SceneLabelStyle());
            Handles.Label(endHandle, "End", SceneLabelStyle());

            EditorGUI.BeginChangeCheck();
            var newEndHandle = Handles.PositionHandle(endHandle, Quaternion.identity);
            var newStartHandle = Handles.PositionHandle(startHandle, Quaternion.identity);

            if (!EditorGUI.EndChangeCheck())
                return;

            Undo.RecordObject(script, "Move Verlet Grid Handles");
            script.HandlePositionOffSet1 = newEndHandle - script.transform.position;
            script.HandlePositionOffeset2 = newStartHandle - script.transform.position;
            EditorUtility.SetDirty(script);
            SceneView.RepaintAll();
        }

        private void ResetHandle()
        {
            var script = target as BakePoints;
            if (script == null)
                return;

            Undo.RecordObject(script, "Reset Verlet Grid Handles");
            script.HandlePositionOffSet1 = script.transform.right * DefaultHandleDistance;
            script.HandlePositionOffeset2 = -script.transform.right * DefaultHandleDistance;
            EditorUtility.SetDirty(script);
            SceneView.RepaintAll();
        }

        private void SelectAllPoints()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || script.Points.Length == 0)
                return;

            _selectedPoints.Clear();
            for (var i = 0; i < script.Points.Length; i++)
                _selectedPoints.Add(i);

            RefreshInspectorState();
            ShowSceneNotification($"{_selectedPoints.Count} points selected");
            SceneView.RepaintAll();
        }

        private void ClearSelection()
        {
            _selectedPoints.Clear();
            RefreshInspectorState();
            ShowSceneNotification("Selection cleared");
            SceneView.RepaintAll();
        }

        private void SelectPointsByLockState(bool locked)
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || !EnsureLockArray(script, false))
                return;

            _selectedPoints.Clear();
            for (var i = 0; i < script.Points.Length; i++)
            {
                if (script.IsLocked[i] == locked)
                    _selectedPoints.Add(i);
            }

            RefreshInspectorState();
            ShowSceneNotification($"{_selectedPoints.Count} {(locked ? "locked" : "free")} points selected");
            SceneView.RepaintAll();
        }

        private void SelectPointsByRegion(PointSelectionRegion region)
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || script.Points.Length == 0)
                return;

            CacheMeshMode();
            _selectedPoints.Clear();

            if (!TrySelectGridRegion(script, region))
                SelectBoundsRegion(script.Points, region);

            RefreshInspectorState();
            ShowSceneNotification($"{_selectedPoints.Count} {GetRegionLabel(region)} points selected");
            SceneView.RepaintAll();
        }

        private bool TrySelectGridRegion(BakePoints script, PointSelectionRegion region)
        {
            if (_meshMode.HasMesh || script == null || script.Points == null)
                return false;

            var width = script.Grid.x;
            var height = script.Grid.y;
            if (width < 2 || height < 2 || (long)width * height != script.Points.Length)
                return false;

            switch (region)
            {
                case PointSelectionRegion.TopEdge:
                    for (var x = 0; x < width; x++)
                        _selectedPoints.Add(x);
                    return true;

                case PointSelectionRegion.BottomEdge:
                    var bottomRowStart = (height - 1) * width;
                    for (var x = 0; x < width; x++)
                        _selectedPoints.Add(bottomRowStart + x);
                    return true;

                case PointSelectionRegion.Border:
                    for (var y = 0; y < height; y++)
                    {
                        var rowStart = y * width;
                        for (var x = 0; x < width; x++)
                        {
                            if (x == 0 || x == width - 1 || y == 0 || y == height - 1)
                                _selectedPoints.Add(rowStart + x);
                        }
                    }
                    return true;

                default:
                    return false;
            }
        }

        private void SelectBoundsRegion(Vector3[] points, PointSelectionRegion region)
        {
            if (!TryGetPointBounds(points, out var min, out var max))
                return;

            var tolerance = GetBoundsSelectionTolerance(min, max);
            for (var i = 0; i < points.Length; i++)
            {
                var point = points[i];
                switch (region)
                {
                    case PointSelectionRegion.TopEdge:
                        if (Mathf.Abs(point.y - max.y) <= tolerance.y)
                            _selectedPoints.Add(i);
                        break;

                    case PointSelectionRegion.BottomEdge:
                        if (Mathf.Abs(point.y - min.y) <= tolerance.y)
                            _selectedPoints.Add(i);
                        break;

                    case PointSelectionRegion.Border:
                        if (Mathf.Abs(point.x - min.x) <= tolerance.x ||
                            Mathf.Abs(point.x - max.x) <= tolerance.x ||
                            Mathf.Abs(point.y - min.y) <= tolerance.y ||
                            Mathf.Abs(point.y - max.y) <= tolerance.y)
                        {
                            _selectedPoints.Add(i);
                        }
                        break;
                }
            }
        }

        private static bool TryGetPointBounds(Vector3[] points, out Vector3 min, out Vector3 max)
        {
            min = Vector3.zero;
            max = Vector3.zero;

            if (points == null || points.Length == 0)
                return false;

            min = points[0];
            max = points[0];
            for (var i = 1; i < points.Length; i++)
            {
                min = Vector3.Min(min, points[i]);
                max = Vector3.Max(max, points[i]);
            }

            return true;
        }

        private static bool TryGetAuthoringWorldBounds(BakePoints script, out Bounds worldBounds, out Vector3 localMin, out Vector3 localMax)
        {
            worldBounds = default;
            localMin = Vector3.zero;
            localMax = Vector3.zero;

            if (script == null)
                return false;

            if (script.MaximumBound != script.MinmumBound)
            {
                localMin = Vector3.Min(script.MinmumBound, script.MaximumBound);
                localMax = Vector3.Max(script.MinmumBound, script.MaximumBound);
            }
            else if (TryGetPointBounds(script.Points, out localMin, out localMax))
            {
                var padding = Mathf.Max(script.HandleSize * 2f, 0.1f);
                localMin -= Vector3.one * padding;
                localMax += Vector3.one * padding;
            }
            else
            {
                return false;
            }

            var worldMin = script.GetRelative(localMin);
            var worldMax = script.GetRelative(localMax);
            worldBounds.SetMinMax(Vector3.Min(worldMin, worldMax), Vector3.Max(worldMin, worldMax));
            return true;
        }

        private static bool TryFindPhysicsGroundHit(BakePoints owner, Bounds worldBounds, out GroundBoundsHit hit)
        {
            hit = default;
            var hasHit = false;
            var bestY = float.NegativeInfinity;
            var rayStartY = worldBounds.max.y + GroundRaycastSkin;
            var lowestAcceptedY = worldBounds.min.y - Mathf.Max(worldBounds.size.y * 4f, 20f);
            var samples = GetGroundSamplePoints(worldBounds);

#if UNITY_2023_1_OR_NEWER
            var shapes = UnityEngine.Object.FindObjectsByType<PhysicsShapeAuthoring>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
#else
            var shapes = UnityEngine.Object.FindObjectsOfType<PhysicsShapeAuthoring>();
#endif

            for (var i = 0; i < shapes.Length; i++)
            {
                var shape = shapes[i];
                if (!CanUsePhysicsShapeAsGround(owner, shape))
                    continue;

                if (!TryGetPhysicsShapeWorldBounds(shape, out var shapeBounds))
                    continue;

                var hitY = shapeBounds.max.y;
                if (hitY > rayStartY || hitY < lowestAcceptedY || hitY <= bestY || !OverlapsXZ(shapeBounds, worldBounds))
                    continue;

                var hitPoint = Vector3.zero;
                var hasSampleHit = false;
                for (var sampleIndex = 0; sampleIndex < samples.Count; sampleIndex++)
                {
                    var sample = samples[sampleIndex];
                    if (!ContainsXZ(shapeBounds, sample))
                        continue;

                    hitPoint = new Vector3(sample.x, hitY, sample.z);
                    hasSampleHit = true;
                    break;
                }

                if (!hasSampleHit)
                {
                    var overlapCenter = GetXZOverlapCenter(worldBounds, shapeBounds);
                    hitPoint = new Vector3(overlapCenter.x, hitY, overlapCenter.y);
                }

                bestY = hitY;
                hasHit = true;
                hit = new GroundBoundsHit(shape, hitPoint);
            }

            return hasHit;
        }

        private static List<Vector3> GetGroundSamplePoints(Bounds bounds)
        {
            var samples = new List<Vector3>(GroundBoundsSampleResolution * GroundBoundsSampleResolution);
            for (var x = 0; x < GroundBoundsSampleResolution; x++)
            {
                var normalizedX = GroundBoundsSampleResolution == 1 ? 0.5f : x / (GroundBoundsSampleResolution - 1f);
                for (var z = 0; z < GroundBoundsSampleResolution; z++)
                {
                    var normalizedZ = GroundBoundsSampleResolution == 1 ? 0.5f : z / (GroundBoundsSampleResolution - 1f);
                    samples.Add(new Vector3(
                        Mathf.Lerp(bounds.min.x, bounds.max.x, normalizedX),
                        bounds.max.y + GroundRaycastSkin,
                        Mathf.Lerp(bounds.min.z, bounds.max.z, normalizedZ)
                    ));
                }
            }

            return samples;
        }

        private static bool CanUsePhysicsShapeAsGround(BakePoints owner, PhysicsShapeAuthoring shape)
        {
            return owner != null &&
                   shape != null &&
                   shape.isActiveAndEnabled &&
                   !shape.transform.IsChildOf(owner.transform);
        }

        private static bool ContainsXZ(Bounds bounds, Vector3 point)
        {
            return point.x >= bounds.min.x &&
                   point.x <= bounds.max.x &&
                   point.z >= bounds.min.z &&
                   point.z <= bounds.max.z;
        }

        private static bool OverlapsXZ(Bounds a, Bounds b)
        {
            return a.min.x <= b.max.x &&
                   a.max.x >= b.min.x &&
                   a.min.z <= b.max.z &&
                   a.max.z >= b.min.z;
        }

        private static Vector2 GetXZOverlapCenter(Bounds a, Bounds b)
        {
            var minX = Mathf.Max(a.min.x, b.min.x);
            var maxX = Mathf.Min(a.max.x, b.max.x);
            var minZ = Mathf.Max(a.min.z, b.min.z);
            var maxZ = Mathf.Min(a.max.z, b.max.z);

            return new Vector2((minX + maxX) * 0.5f, (minZ + maxZ) * 0.5f);
        }

        private static bool TryGetPhysicsShapeWorldBounds(PhysicsShapeAuthoring shape, out Bounds bounds)
        {
            bounds = default;
            if (shape == null)
                return false;

            switch (shape.ShapeType)
            {
                case ShapeType.Box:
                {
                    var box = shape.GetBoxProperties();
                    return TryGetOrientedLocalBoxWorldBounds(shape.transform, box.Center, box.Orientation, box.Size * 0.5f, out bounds);
                }

                case ShapeType.Plane:
                {
                    shape.GetPlaneProperties(out var center, out var size, out var orientation);
                    return TryGetOrientedLocalBoxWorldBounds(shape.transform, center, orientation, new float3(size.x * 0.5f, 0f, size.y * 0.5f), out bounds);
                }

                case ShapeType.Sphere:
                {
                    var sphere = shape.GetSphereProperties(out _);
                    var center = shape.transform.TransformPoint(ToVector3(sphere.Center));
                    var scale = shape.transform.lossyScale;
                    var radius = sphere.Radius * Mathf.Max(Mathf.Abs(scale.x), Mathf.Max(Mathf.Abs(scale.y), Mathf.Abs(scale.z)));
                    bounds = new Bounds(center, Vector3.one * radius * 2f);
                    return true;
                }

                case ShapeType.Capsule:
                {
                    var capsule = shape.GetCapsuleProperties();
                    return TryGetOrientedLocalBoxWorldBounds(
                        shape.transform,
                        capsule.Center,
                        capsule.Orientation,
                        new float3(capsule.Radius, capsule.Radius, capsule.Height * 0.5f),
                        out bounds);
                }

                case ShapeType.Cylinder:
                {
                    var cylinder = shape.GetCylinderProperties();
                    return TryGetOrientedLocalBoxWorldBounds(
                        shape.transform,
                        cylinder.Center,
                        cylinder.Orientation,
                        new float3(cylinder.Radius, cylinder.Radius, cylinder.Height * 0.5f),
                        out bounds);
                }

                case ShapeType.ConvexHull:
                case ShapeType.Mesh:
                    return TryGetRendererBounds(shape, out bounds);

                default:
                    return false;
            }
        }

        private static bool TryGetOrientedLocalBoxWorldBounds(Transform transform, float3 center, quaternion orientation, float3 halfExtents, out Bounds bounds)
        {
            bounds = default;
            var hasBounds = false;

            for (var x = -1; x <= 1; x += 2)
            {
                for (var y = -1; y <= 1; y += 2)
                {
                    for (var z = -1; z <= 1; z += 2)
                    {
                        var localOffset = new float3(halfExtents.x * x, halfExtents.y * y, halfExtents.z * z);
                        var localPoint = center + math.mul(orientation, localOffset);
                        EncapsulateWorldPoint(ref bounds, ref hasBounds, transform.TransformPoint(ToVector3(localPoint)));
                    }
                }
            }

            return hasBounds;
        }

        private static bool TryGetRendererBounds(Component shape, out Bounds bounds)
        {
            bounds = default;
            var hasBounds = false;
            var renderers = shape.GetComponentsInChildren<Renderer>();
            for (var i = 0; i < renderers.Length; i++)
            {
                var renderer = renderers[i];
                if (renderer == null || !renderer.enabled)
                    continue;

                if (!hasBounds)
                {
                    bounds = renderer.bounds;
                    hasBounds = true;
                }
                else
                {
                    bounds.Encapsulate(renderer.bounds);
                }
            }

            return hasBounds;
        }

        private static void EncapsulateWorldPoint(ref Bounds bounds, ref bool hasBounds, Vector3 point)
        {
            if (!hasBounds)
            {
                bounds = new Bounds(point, Vector3.zero);
                hasBounds = true;
                return;
            }

            bounds.Encapsulate(point);
        }

        private static Vector3 ToVector3(float3 value)
        {
            return new Vector3(value.x, value.y, value.z);
        }

        private static Vector3 GetBoundsSelectionTolerance(Vector3 min, Vector3 max)
        {
            var size = max - min;
            var longestAxis = Mathf.Max(size.x, Mathf.Max(size.y, size.z));
            var fallback = Mathf.Max(longestAxis * 0.015f, 0.001f);

            return new Vector3(
                Mathf.Max(size.x * 0.015f, fallback),
                Mathf.Max(size.y * 0.015f, fallback),
                Mathf.Max(size.z * 0.015f, fallback)
            );
        }

        private static string GetRegionLabel(PointSelectionRegion region)
        {
            switch (region)
            {
                case PointSelectionRegion.TopEdge:
                    return "top edge";
                case PointSelectionRegion.BottomEdge:
                    return "bottom edge";
                case PointSelectionRegion.Border:
                    return "border";
                default:
                    return "region";
            }
        }

        private void InvertSelection()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null)
                return;

            EnsurePointMaskCache(script.Points.Length);
            foreach (var selectedIndex in _selectedPoints)
            {
                if (selectedIndex >= 0 && selectedIndex < _pointMaskCache.Length)
                    _pointMaskCache[selectedIndex] = true;
            }

            _selectedPoints.Clear();
            for (var i = 0; i < script.Points.Length; i++)
            {
                if (!_pointMaskCache[i])
                    _selectedPoints.Add(i);
            }

            Array.Clear(_pointMaskCache, 0, script.Points.Length);
            RefreshInspectorState();
            ShowSceneNotification($"{_selectedPoints.Count} points selected");
            SceneView.RepaintAll();
        }

        private void SetSelectedPointsLocked(bool locked)
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || !EnsureLockArray(script, false) || _selectedPoints.Count == 0)
                return;

            Undo.RecordObject(script, locked ? "Lock Selected Verlet Points" : "Unlock Selected Verlet Points");

            var changedCount = 0;
            foreach (var selectedIndex in _selectedPoints)
            {
                if (!IsValidPointIndex(selectedIndex, script.Points) || script.IsLocked[selectedIndex] == locked)
                    continue;

                script.IsLocked[selectedIndex] = locked;
                changedCount++;
            }

            _selectedPoints.Clear();
            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification(changedCount == 0
                ? $"Selection was already {(locked ? "locked" : "unlocked")}"
                : $"{changedCount} point{(changedCount == 1 ? string.Empty : "s")} {(locked ? "locked" : "unlocked")}");
            SceneView.RepaintAll();
        }

        private void ToggleSelectedLocks()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || !EnsureLockArray(script, false) || _selectedPoints.Count == 0)
                return;

            Undo.RecordObject(script, "Toggle Selected Verlet Point Locks");

            var lockedCount = 0;
            var unlockedCount = 0;
            foreach (var selectedIndex in _selectedPoints)
            {
                if (!IsValidPointIndex(selectedIndex, script.Points))
                    continue;

                script.IsLocked[selectedIndex] = !script.IsLocked[selectedIndex];
                if (script.IsLocked[selectedIndex])
                    lockedCount++;
                else
                    unlockedCount++;
            }

            _selectedPoints.Clear();
            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification($"{lockedCount} locked | {unlockedCount} unlocked");
            SceneView.RepaintAll();
        }

        private void FrameSelectedPoints()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || _selectedPoints.Count == 0)
                return;

            var sceneView = SceneView.lastActiveSceneView;
            if (sceneView == null)
                return;

            var worldPoints = PrepareWorldPointCache(script);
            var hasBounds = false;
            var bounds = new Bounds();
            var selectedCount = 0;

            foreach (var selectedIndex in _selectedPoints)
            {
                if (selectedIndex < 0 || selectedIndex >= worldPoints.Length)
                    continue;

                if (!hasBounds)
                {
                    bounds = new Bounds(worldPoints[selectedIndex], Vector3.zero);
                    hasBounds = true;
                }
                else
                {
                    bounds.Encapsulate(worldPoints[selectedIndex]);
                }

                selectedCount++;
            }

            if (!hasBounds)
                return;

            bounds.Expand(Mathf.Max(script.HandleSize * 6f, 0.25f));
            sceneView.Frame(bounds, false);
            ShowSceneNotification($"{selectedCount} points framed");
        }

        private void RecalculateAllStickLengths()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || script.Sticks == null)
                return;

            Undo.RecordObject(script, "Recalculate Verlet Stick Lengths");

            var recalculatedCount = 0;
            for (var i = 0; i < script.Sticks.Length; i++)
            {
                var stick = script.Sticks[i];
                if (!IsValidPointIndex(stick.PointA, script.Points) || !IsValidPointIndex(stick.PointB, script.Points))
                    continue;

                UpdateStickLength(script, i, stick);
                recalculatedCount++;
            }

            EditorUtility.SetDirty(script);
            ShowSceneNotification($"{recalculatedCount} stick lengths recalculated");
            SceneView.RepaintAll();
        }

        private void RecalculateSelectedStickLengths()
        {
            var script = target as BakePoints;
            if (script == null || script.Points == null || script.Sticks == null || _selectedPoints.Count == 0)
                return;

            Undo.RecordObject(script, "Recalculate Selected Verlet Stick Lengths");
            var recalculatedCount = UpdateStickLengthsForSelection(script);

            EditorUtility.SetDirty(script);
            RefreshInspectorState();
            ShowSceneNotification($"{recalculatedCount} selected stick lengths recalculated");
            SceneView.RepaintAll();
        }

        private void UpdateStickLengthsForPoint(BakePoints script, int pointIndex)
        {
            if (script == null || script.Sticks == null || script.Points == null)
                return;

            for (var i = 0; i < script.Sticks.Length; i++)
            {
                var stick = script.Sticks[i];
                if (stick.PointA != pointIndex && stick.PointB != pointIndex)
                    continue;

                UpdateStickLength(script, i, stick);
            }
        }

        private int UpdateStickLengthsForSelection(BakePoints script)
        {
            if (script == null || script.Sticks == null || script.Points == null || _selectedPoints.Count == 0)
                return 0;

            EnsurePointMaskCache(script.Points.Length);
            foreach (var selectedIndex in _selectedPoints)
            {
                if (IsValidPointIndex(selectedIndex, script.Points))
                    _pointMaskCache[selectedIndex] = true;
            }

            var updatedCount = 0;
            for (var i = 0; i < script.Sticks.Length; i++)
            {
                var stick = script.Sticks[i];
                if (!IsValidPointIndex(stick.PointA, script.Points) || !IsValidPointIndex(stick.PointB, script.Points))
                    continue;

                if (_pointMaskCache[stick.PointA] || _pointMaskCache[stick.PointB])
                {
                    UpdateStickLength(script, i, stick);
                    updatedCount++;
                }
            }

            foreach (var selectedIndex in _selectedPoints)
            {
                if (selectedIndex >= 0 && selectedIndex < _pointMaskCache.Length)
                    _pointMaskCache[selectedIndex] = false;
            }

            return updatedCount;
        }

        private static void UpdateStickLength(BakePoints script, int stickIndex, StickPartiallyMono stick)
        {
            if (!IsValidPointIndex(stick.PointA, script.Points) || !IsValidPointIndex(stick.PointB, script.Points))
                return;

            stick.Lenght = ToStoredLength(Vector3.Distance(script.Points[stick.PointA], script.Points[stick.PointB]));
            script.Sticks[stickIndex] = stick;
        }

        private void HandleBoxSelection(Event currentEvent, int selectionControlId, BakePoints holder, Vector3[] worldPoints)
        {
            if (holder.Points == null || holder.Points.Length == 0 || currentEvent.alt)
                return;

            switch (currentEvent.type)
            {
                case EventType.MouseDown:
                {
                    if (currentEvent.button != 0)
                        return;

                    if (HandleUtility.nearestControl != selectionControlId)
                        return;

                    _isBoxSelecting = true;
                    _dragStart = currentEvent.mousePosition;
                    _dragEnd = currentEvent.mousePosition;
                    GUIUtility.hotControl = selectionControlId;

                    if (!IsAdditiveSelection(currentEvent))
                        _selectedPoints.Clear();

                    currentEvent.Use();
                    break;
                }

                case EventType.MouseDrag:
                {
                    if (!_isBoxSelecting || GUIUtility.hotControl != selectionControlId)
                        return;

                    _dragEnd = currentEvent.mousePosition;
                    currentEvent.Use();
                    break;
                }

                case EventType.MouseUp:
                {
                    if (!_isBoxSelecting || GUIUtility.hotControl != selectionControlId)
                        return;

                    _isBoxSelecting = false;
                    GUIUtility.hotControl = 0;

                    var rect = MakeRect(_dragStart, _dragEnd);
                    for (var i = 0; i < holder.Points.Length; i++)
                    {
                        var worldPosition = worldPoints[i];
                        var guiPosition = HandleUtility.WorldToGUIPointWithDepth(worldPosition);

                        if (guiPosition.z <= 0f)
                            continue;

                        if (rect.Contains(new Vector2(guiPosition.x, guiPosition.y)))
                            _selectedPoints.Add(i);
                    }

                    RefreshInspectorState();
                    currentEvent.Use();
                    break;
                }
            }
        }

        private void DrawScenePreview(BakePoints script, Vector3[] worldPoints, EventType eventType)
        {
            if (eventType != EventType.Repaint)
                return;

            if (_drawSurface)
                DrawTriangleSurface(script, worldPoints);

            if (_drawSticks)
                DrawSticks(script, worldPoints);

            if (_drawBounds)
                DrawBounds(script);
        }

        private void DrawTriangleSurface(BakePoints script, Vector3[] worldPoints)
        {
            if (script.Points == null || script.Tringles == null)
                return;

            Handles.color = SurfaceColor;
            for (var i = 0; i + 2 < script.Tringles.Length; i += 3)
            {
                var a = script.Tringles[i];
                var b = script.Tringles[i + 1];
                var c = script.Tringles[i + 2];
                if (!IsValidPointIndex(a, script.Points) || !IsValidPointIndex(b, script.Points) || !IsValidPointIndex(c, script.Points))
                    continue;

                Handles.DrawAAConvexPolygon(
                    worldPoints[a],
                    worldPoints[b],
                    worldPoints[c]
                );
            }
        }

        private void DrawSticks(BakePoints script, Vector3[] worldPoints)
        {
            if (script.Points == null || script.Sticks == null)
                return;

            var previousZTest = Handles.zTest;
            Handles.zTest = CompareFunction.LessEqual;

            for (var i = 0; i < script.Sticks.Length; i++)
            {
                var stick = script.Sticks[i];
                if (!IsValidPointIndex(stick.PointA, script.Points) || !IsValidPointIndex(stick.PointB, script.Points))
                    continue;

                var pointA = worldPoints[stick.PointA];
                var pointB = worldPoints[stick.PointB];
                var isSelected = _selectedPoints.Contains(stick.PointA) || _selectedPoints.Contains(stick.PointB);
                var isLocked = IsPointLocked(script, stick.PointA) || IsPointLocked(script, stick.PointB);

                Handles.color = isSelected ? SelectedPointColor : isLocked ? LockedStickColor : StickColor;
                Handles.DrawAAPolyLine(isSelected ? SelectedStickLineWidth : StickLineWidth, pointA, pointB);
            }

            Handles.zTest = previousZTest;
        }

        private void DrawBounds(BakePoints script)
        {
            if (script.MaximumBound == script.MinmumBound)
                return;

            var worldMin = script.GetRelative(Vector3.Min(script.MinmumBound, script.MaximumBound));
            var worldMax = script.GetRelative(Vector3.Max(script.MinmumBound, script.MaximumBound));

            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 0f, 0f), new Vector3(1f, 0f, 0f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 0f, 0f), new Vector3(0f, 0f, 1f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(1f, 0f, 0f), new Vector3(1f, 0f, 1f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 0f, 1f), new Vector3(1f, 0f, 1f));

            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 1f, 0f), new Vector3(1f, 1f, 0f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 1f, 0f), new Vector3(0f, 1f, 1f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(1f, 1f, 0f), new Vector3(1f, 1f, 1f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 1f, 1f), new Vector3(1f, 1f, 1f));

            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 0f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(0f, 0f, 1f), new Vector3(0f, 1f, 1f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(1f, 0f, 0f), new Vector3(1f, 1f, 0f));
            DrawBoundsLine(worldMin, worldMax, new Vector3(1f, 0f, 1f), new Vector3(1f, 1f, 1f));
        }

        private static void DrawBoundsLine(Vector3 min, Vector3 max, Vector3 from, Vector3 to)
        {
            Handles.color = BoundColor;
            Handles.DrawAAPolyLine(2.5f, LerpBounds(min, max, from), LerpBounds(min, max, to));
        }

        private static Vector3 LerpBounds(Vector3 min, Vector3 max, Vector3 value)
        {
            return new Vector3(
                Mathf.Lerp(min.x, max.x, value.x),
                Mathf.Lerp(min.y, max.y, value.y),
                Mathf.Lerp(min.z, max.z, value.z)
            );
        }

        private void DrawSelectionRectangle(Event currentEvent)
        {
            if (!_isBoxSelecting || currentEvent.type != EventType.Repaint)
                return;

            var rect = MakeRect(_dragStart, _dragEnd);

            Handles.BeginGUI();

            EditorGUI.DrawRect(rect, new Color(0.25f, 0.55f, 1f, 0.15f));

            var previousColor = GUI.color;
            GUI.color = new Color(0.25f, 0.55f, 1f, 1f);
            GUI.Box(rect, GUIContent.none);
            GUI.color = previousColor;

            Handles.EndGUI();
        }

        private void DrawSceneHud(BakePoints script)
        {
            if (!_drawHud || Event.current.type != EventType.Repaint)
                return;

            var pointCount = script.Points?.Length ?? 0;
            if (pointCount == 0)
                return;

            var rect = new Rect(12f, 12f, 260f, 76f);
            Handles.BeginGUI();
            GUI.Box(rect, GUIContent.none, EditorStyles.helpBox);
            GUI.Label(new Rect(rect.x + 10f, rect.y + 6f, rect.width - 20f, 18f), "Verlet Points", EditorStyles.boldLabel);
            GUI.Label(new Rect(rect.x + 10f, rect.y + 28f, rect.width - 20f, 18f), $"{pointCount} points | {_selectedPoints.Count} selected", EditorStyles.label);
            GUI.Label(new Rect(rect.x + 10f, rect.y + 50f, rect.width - 20f, 18f), $"Mode: {EditMode}", EditorStyles.label);
            Handles.EndGUI();
        }

        private void DrawSelectionRing(Vector3 position, float radius)
        {
            var camera = SceneView.currentDrawingSceneView != null ? SceneView.currentDrawingSceneView.camera : null;
            var normal = camera != null ? camera.transform.forward : Vector3.forward;

            Handles.color = SelectedPointColor;
            Handles.DrawWireDisc(position, normal, radius);
        }

        private static void DrawPointLabel(Vector3 position, int index, float handleSize)
        {
            Handles.Label(position + Vector3.up * Mathf.Max(0.08f, handleSize * 1.4f), index.ToString(), SceneLabelStyle());
        }

        private static void GenerateGridData(
            BakePoints bakePoints,
            Vector3 startHandle,
            Vector3 endHandle,
            out Vector3[] points,
            out bool[] locked,
            out int[] triangles,
            out StickPartiallyMono[] sticks)
        {
            var grid = bakePoints.Grid;
            var direction = (endHandle - startHandle).normalized;
            var horizontalDistance = Vector3.Distance(startHandle, endHandle) / (grid.x - 1);
            var downDirection = -bakePoints.transform.up;
            var pointCount = grid.x * grid.y;

            points = new Vector3[pointCount];
            locked = new bool[pointCount];

            for (var y = 0; y < grid.y; y++)
            {
                for (var x = 0; x < grid.x; x++)
                {
                    var index = y * grid.x + x;
                    var worldPosition = startHandle + direction * (x * horizontalDistance) + downDirection * (y * bakePoints.Distance);
                    points[index] = worldPosition - bakePoints.transform.position;
                }
            }

            triangles = BuildGridTriangles(grid.x, grid.y);
            sticks = BuildGridSticks(grid.x, grid.y, points);
        }

        private static int[] BuildGridTriangles(int width, int height)
        {
            var triangles = new int[(width - 1) * (height - 1) * 6];
            var triangleIndex = 0;

            for (var row = 0; row < height - 1; row++)
            {
                for (var column = 0; column < width - 1; column++)
                {
                    var lowerLeft = row * width + column;
                    var upperLeft = (row + 1) * width + column;
                    var upperRight = upperLeft + 1;
                    var lowerRight = lowerLeft + 1;

                    triangles[triangleIndex++] = lowerLeft;
                    triangles[triangleIndex++] = upperLeft;
                    triangles[triangleIndex++] = upperRight;

                    triangles[triangleIndex++] = lowerLeft;
                    triangles[triangleIndex++] = upperRight;
                    triangles[triangleIndex++] = lowerRight;
                }
            }

            return triangles;
        }

        private static StickPartiallyMono[] BuildGridSticks(int width, int height, Vector3[] points)
        {
            var estimatedStickCount = width * (height - 1) + height * (width - 1);
            var edgeSet = new HashSet<Edge>(estimatedStickCount);
            var sticks = new List<StickPartiallyMono>(estimatedStickCount);

            for (var row = 0; row < height - 1; row++)
            {
                for (var column = 0; column < width - 1; column++)
                {
                    var lowerLeft = row * width + column;
                    var upperLeft = (row + 1) * width + column;
                    var upperRight = upperLeft + 1;
                    var lowerRight = lowerLeft + 1;

                    AddStick(edgeSet, sticks, points, lowerLeft, upperLeft);
                    AddStick(edgeSet, sticks, points, upperLeft, upperRight);
                    AddStick(edgeSet, sticks, points, upperRight, lowerRight);
                    AddStick(edgeSet, sticks, points, lowerRight, lowerLeft);
                }
            }

            return sticks.ToArray();
        }

        private static StickPartiallyMono[] BuildSticksFromTriangles(Vector3[] points, int[] triangles)
        {
            var edgeSet = new HashSet<Edge>(triangles.Length);
            var sticks = new List<StickPartiallyMono>(triangles.Length);

            for (var triangleIndex = 0; triangleIndex + 2 < triangles.Length; triangleIndex += 3)
            {
                var a = triangles[triangleIndex];
                var b = triangles[triangleIndex + 1];
                var c = triangles[triangleIndex + 2];

                AddStick(edgeSet, sticks, points, a, b);
                AddStick(edgeSet, sticks, points, b, c);
                AddStick(edgeSet, sticks, points, c, a);
            }

            return sticks.ToArray();
        }

        private static void AddStick(HashSet<Edge> edgeSet, List<StickPartiallyMono> sticks, Vector3[] points, int pointA, int pointB)
        {
            if (!IsValidPointIndex(pointA, points) || !IsValidPointIndex(pointB, points))
                return;

            var edge = new Edge(pointA, pointB);
            if (!edgeSet.Add(edge))
                return;

            sticks.Add(new StickPartiallyMono
            {
                PointA = (short)edge.A,
                PointB = (short)edge.B,
                Lenght = ToStoredLength(Vector3.Distance(points[edge.A], points[edge.B]))
            });
        }

        private static int[] GetAllMeshTriangles(Mesh mesh)
        {
            if (mesh.subMeshCount <= 1)
                return mesh.triangles;

            var indexCount = 0L;
            for (var subMesh = 0; subMesh < mesh.subMeshCount; subMesh++)
                indexCount += (long)mesh.GetIndexCount(subMesh);

            var capacity = indexCount > int.MaxValue ? int.MaxValue : (int)indexCount;
            var triangles = new List<int>(capacity);
            for (var subMesh = 0; subMesh < mesh.subMeshCount; subMesh++)
                triangles.AddRange(mesh.GetTriangles(subMesh));

            return triangles.ToArray();
        }

        private void ApplyLockPreset(BakePoints bakePoints, PointLockPreset lockPreset)
        {
            if (lockPreset == PointLockPreset.None || bakePoints.Points == null || bakePoints.IsLocked == null)
                return;

            Array.Clear(bakePoints.IsLocked, 0, bakePoints.IsLocked.Length);

            if (_meshMode.HasMesh)
            {
                ApplyMeshLockPreset(_meshMode.Mesh, bakePoints.Points, bakePoints.IsLocked, lockPreset);
                return;
            }

            var gridPointCount = (long)bakePoints.Grid.x * bakePoints.Grid.y;
            if (!_meshMode.HasMesh && gridPointCount == bakePoints.Points.Length)
            {
                ApplyGridLockPreset(bakePoints, lockPreset);
                return;
            }

            ApplyBoundsLockPreset(bakePoints.Points, bakePoints.IsLocked, lockPreset);
        }

        private static void ApplyMeshLockPreset(Mesh mesh, Vector3[] points, bool[] locked, PointLockPreset lockPreset)
        {
            switch (lockPreset)
            {
                case PointLockPreset.HangingClothTop:
                    ApplyMeshTopLockPreset(mesh, points, locked);
                    break;
                case PointLockPreset.FlagRaceSides:
                    Debug.LogWarning("Flag Race Locks are grid-only. Use Hanging Cloth Locks for mesh-based cloth presets.", mesh);
                    break;
            }
        }

        private static void ApplyMeshTopLockPreset(Mesh mesh, Vector3[] points, bool[] locked)
        {
            if (points.Length == 0)
                return;

            var bounds = mesh.bounds;
            var top = bounds.max.y;
            var tolerance = Mathf.Max(0.001f, bounds.size.y * 0.015f);

            for (var i = 0; i < points.Length; i++)
                locked[i] = Mathf.Abs(points[i].y - top) <= tolerance;
        }

        private static void ApplyGridLockPreset(BakePoints bakePoints, PointLockPreset lockPreset)
        {
            var width = bakePoints.Grid.x;
            var height = bakePoints.Grid.y;

            switch (lockPreset)
            {
                case PointLockPreset.FlagRaceSides:
                    for (var y = 0; y < height; y++)
                    {
                        var rowStart = y * width;
                        bakePoints.IsLocked[rowStart] = true;
                        bakePoints.IsLocked[rowStart + width - 1] = true;
                    }
                    break;
                case PointLockPreset.HangingClothTop:
                    for (var x = 0; x < width; x++)
                        bakePoints.IsLocked[x] = true;
                    break;
            }
        }

        private static void ApplyBoundsLockPreset(Vector3[] points, bool[] locked, PointLockPreset lockPreset)
        {
            if (points.Length == 0)
                return;

            var min = points[0];
            var max = points[0];
            for (var i = 1; i < points.Length; i++)
            {
                min = Vector3.Min(min, points[i]);
                max = Vector3.Max(max, points[i]);
            }

            var size = max - min;
            var sideTolerance = Mathf.Max(0.001f, size.x * 0.015f);
            var topTolerance = Mathf.Max(0.001f, size.y * 0.015f);

            switch (lockPreset)
            {
                case PointLockPreset.FlagRaceSides:
                    for (var i = 0; i < points.Length; i++)
                        locked[i] = Mathf.Abs(points[i].x - min.x) <= sideTolerance || Mathf.Abs(points[i].x - max.x) <= sideTolerance;
                    break;
                case PointLockPreset.HangingClothTop:
                    for (var i = 0; i < points.Length; i++)
                        locked[i] = Mathf.Abs(points[i].y - max.y) <= topTolerance;
                    break;
            }
        }

        private static string GetPresetNotification(PointLockPreset lockPreset)
        {
            switch (lockPreset)
            {
                case PointLockPreset.FlagRaceSides:
                    return "Generated points with flag-race side locks";
                case PointLockPreset.HangingClothTop:
                    return "Generated points with hanging-cloth top locks";
                default:
                    return "Generated free Verlet points";
            }
        }

        private static ushort ToStoredLength(float length)
        {
            if (float.IsNaN(length) || float.IsInfinity(length))
                return 0;

            return (ushort)Mathf.Clamp(Mathf.RoundToInt(length * 100f), 0, ushort.MaxValue);
        }

        private bool ValidateGrid(BakePoints bakePoints)
        {
            if (bakePoints.Grid.x < 2 || bakePoints.Grid.y < 2)
            {
                Debug.LogWarning("Create Points needs a grid of at least 2 x 2.", bakePoints);
                return false;
            }

            var pointCount = (long)bakePoints.Grid.x * bakePoints.Grid.y;
            if (!CanStorePointCount(pointCount, bakePoints))
                return false;

            return true;
        }

        private static bool CanStorePointCount(long pointCount, UnityEngine.Object context)
        {
            if (pointCount <= short.MaxValue + 1)
                return true;

            Debug.LogError($"Verlet points are stored with short indices. Reduce the point count below {short.MaxValue + 1}.", context);
            return false;
        }

        private static bool IsValidPointIndex(int index, Vector3[] points)
        {
            return points != null && index >= 0 && index < points.Length && index <= short.MaxValue;
        }

        private static bool IsAdditiveSelection(Event currentEvent)
        {
            return currentEvent.shift || currentEvent.control || currentEvent.command;
        }

        private Vector3[] PrepareWorldPointCache(BakePoints script)
        {
            var points = script.Points;
            if (points == null || points.Length == 0)
            {
                _worldPointCache = Array.Empty<Vector3>();
                return _worldPointCache;
            }

            if (_worldPointCache.Length != points.Length)
                _worldPointCache = new Vector3[points.Length];

            var offset = script.transform.position;
            for (var i = 0; i < points.Length; i++)
                _worldPointCache[i] = points[i] + offset;

            return _worldPointCache;
        }

        private void EnsurePointMaskCache(int pointCount)
        {
            if (_pointMaskCache.Length != pointCount)
                _pointMaskCache = new bool[pointCount];
        }

        private bool EnsureLockArray(BakePoints script, bool recordUndo)
        {
            var pointCount = script.Points?.Length ?? 0;
            if (pointCount == 0)
                return false;

            if (script.IsLocked != null && script.IsLocked.Length == pointCount)
                return true;

            if (recordUndo)
                Undo.RecordObject(script, "Resize Verlet Lock Array");

            var locked = new bool[pointCount];
            if (script.IsLocked != null)
                Array.Copy(script.IsLocked, locked, Mathf.Min(script.IsLocked.Length, locked.Length));

            script.IsLocked = locked;
            EditorUtility.SetDirty(script);
            return true;
        }

        private void PruneSelection(BakePoints script)
        {
            var pointCount = script.Points?.Length ?? 0;
            _selectedPoints.RemoveWhere(index => index < 0 || index >= pointCount);
        }

        private void CacheMeshMode()
        {
            var script = target as BakePoints;
            var filter = default(MeshFilter);
            if (script != null)
                script.TryGetComponent(out filter);

            _meshMode = new MeshModeState(filter);
        }

        private static Vector3 GetHandleWorldPosition(BakePoints script, int index)
        {
            var offset = index == 1 ? script.HandlePositionOffSet1 : script.HandlePositionOffeset2;
            if (offset != Vector3.zero)
                return script.GetRelative(offset);

            var direction = index == 1 ? script.transform.right : -script.transform.right;
            return script.transform.position + direction * DefaultHandleDistance;
        }

        private bool IsPointLocked(BakePoints script, int index)
        {
            return script.IsLocked != null && index >= 0 && index < script.IsLocked.Length && script.IsLocked[index];
        }

        private static int CountLocked(bool[] locked)
        {
            if (locked == null)
                return 0;

            var count = 0;
            for (var i = 0; i < locked.Length; i++)
            {
                if (locked[i])
                    count++;
            }

            return count;
        }

        private int CountValidSelectedPoints(BakePoints script)
        {
            if (script == null || script.Points == null || _selectedPoints.Count == 0)
                return 0;

            var count = 0;
            foreach (var selectedIndex in _selectedPoints)
            {
                if (IsValidPointIndex(selectedIndex, script.Points))
                    count++;
            }

            return count;
        }

        private int CountSelectedLockedPoints(BakePoints script)
        {
            if (script == null || script.Points == null || script.IsLocked == null || _selectedPoints.Count == 0)
                return 0;

            var count = 0;
            foreach (var selectedIndex in _selectedPoints)
            {
                if (IsValidPointIndex(selectedIndex, script.Points) && IsPointLocked(script, selectedIndex))
                    count++;
            }

            return count;
        }

        private Color GetPointColor(BakePoints script, int index, bool isSelected)
        {
            if (isSelected)
                return SelectedPointColor;

            return IsPointLocked(script, index) ? LockedPointColor : FreePointColor;
        }

        private static Rect MakeRect(Vector2 a, Vector2 b)
        {
            return Rect.MinMaxRect(
                Mathf.Min(a.x, b.x),
                Mathf.Min(a.y, b.y),
                Mathf.Max(a.x, b.x),
                Mathf.Max(a.y, b.y)
            );
        }

        private static GUIStyle SceneLabelStyle()
        {
            if (_sceneLabelStyle != null)
                return _sceneLabelStyle;

            _sceneLabelStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                normal = { textColor = Color.white },
                alignment = TextAnchor.MiddleCenter
            };

            return _sceneLabelStyle;
        }

        private static void ShowSceneNotification(string message)
        {
            var sceneView = SceneView.lastActiveSceneView;
            if (sceneView == null)
                return;

            sceneView.ShowNotification(new GUIContent(message));
        }

        private void OnUndoRedo()
        {
            var script = target as BakePoints;
            if (script != null)
                PruneSelection(script);

            RefreshInspectorState();
            SceneView.RepaintAll();
        }

        private readonly struct Edge : IEquatable<Edge>
        {
            public readonly int A;
            public readonly int B;

            public Edge(int pointA, int pointB)
            {
                if (pointA < pointB)
                {
                    A = pointA;
                    B = pointB;
                }
                else
                {
                    A = pointB;
                    B = pointA;
                }
            }

            public bool Equals(Edge other)
            {
                return A == other.A && B == other.B;
            }

            public override bool Equals(object obj)
            {
                return obj is Edge other && Equals(other);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return (A * 397) ^ B;
                }
            }
        }
    }
}