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;
}
}
}
}
}