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 _selectedPoints = new HashSet(); private Vector3[] _worldPointCache = Array.Empty(); private bool[] _pointMaskCache = Array.Empty(); 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(); 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(FindObjectsInactive.Exclude, FindObjectsSortMode.None); #else var shapes = UnityEngine.Object.FindObjectsOfType(); #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 GetGroundSamplePoints(Bounds bounds) { var samples = new List(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(); 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(estimatedStickCount); var sticks = new List(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(triangles.Length); var sticks = new List(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 edgeSet, List 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(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(); 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 { 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; } } } } }