using UnityEngine; using UnityEditor; using UnityEditor.EditorTools; using System; using UnityEditor.Overlays; using UnityEngine.UIElements; namespace Obi { [EditorTool("Obi Path Editor Tool",typeof(ObiRopeBase))] public class ObiPathEditor : EditorTool { [Overlay(typeof(SceneView), "Obi Path Editor", "Obi Path Editor", "Obi Path Editor", true)] [Icon("Assets/Obi/Editor/Resources/EditCurves.psd")] class PathEditorOverlay : Overlay, ITransientOverlay { public static ObiPathEditor editor; public override VisualElement CreatePanelContent() { var root = new VisualElement(); root.Add(new IMGUIContainer(editor.DrawToolPanel)); return root; } // Use the visible property to hide or show this instance from within the class. public bool visible { get { return ToolManager.activeToolType == typeof(ObiPathEditor); } } } enum PathEditorTool { TranslatePoints, RotatePoints, ScalePoints, OrientPoints, InsertPoints, RemovePoints } ObiPath path; Quaternion prevRot = Quaternion.identity; Vector3 prevScale = Vector3.one; PathEditorTool currentTool = PathEditorTool.TranslatePoints; bool showTangentHandles = true; bool showThicknessHandles = true; public bool needsRepaint = false; protected bool[] selectedStatus; protected int lastSelected = 0; protected int selectedCount = 0; protected Vector3 selectionAverage; protected bool useOrientation = false; protected static Color handleColor = new Color(1, 0.55f, 0.1f); protected GUIContent m_IconContent; public override GUIContent toolbarIcon { get { if (m_IconContent == null) { m_IconContent = new GUIContent() { image = Resources.Load("EditCurves"), text = "Obi Path Editor Tool", tooltip = "Obi Path Editor Tool" }; } return m_IconContent; } } ObiRopeBlueprintBase blueprint { get { return (target as ObiRopeBase).sharedBlueprint as ObiRopeBlueprintBase; } } public void OnEnable() { this.useOrientation = target is ObiRod; selectedStatus = new bool[0]; PathEditorOverlay.editor = this; } public void ResizeCPArrays() { Array.Resize(ref selectedStatus, path.ControlPointCount); } public override void OnToolGUI(EditorWindow window) { needsRepaint = false; float thicknessScale = blueprint.thickness; this.path = (target as ObiRopeBase).path; var matrix = (target as ObiRopeBase).transform.localToWorldMatrix; ResizeCPArrays(); HandleUtility.AddDefaultControl(GUIUtility.GetControlID("PathEditor".GetHashCode(), FocusType.Passive)); Matrix4x4 prevMatrix = Handles.matrix; Handles.matrix = matrix; // Draw control points: Handles.color = handleColor; for (int i = 0; i < path.ControlPointCount; ++i) { needsRepaint |= DrawControlPoint(i); } // Count selected and calculate average position: selectionAverage = GetControlPointAverage(out lastSelected, out selectedCount); // Draw cp tool handles: needsRepaint |= SplineCPTools(matrix); if (showThicknessHandles) needsRepaint |= DoThicknessHandles(thicknessScale); // Control point selection handle: needsRepaint |= ObiPathHandles.SplineCPSelector(path, selectedStatus); Handles.matrix = prevMatrix; // During edit mode, allow to add/remove control points. if (currentTool == PathEditorTool.InsertPoints) AddControlPointsMode(matrix); if (currentTool == PathEditorTool.RemovePoints) RemoveControlPointsMode(matrix); if (needsRepaint) window.Repaint(); } private void AddControlPointsMode(Matrix4x4 matrix) { float mu = ScreenPointToCurveMu(path, Event.current.mousePosition, matrix); Vector3 pointOnSpline = matrix.MultiplyPoint3x4(path.points.GetPositionAtMu(path.Closed, mu)); float size = HandleUtility.GetHandleSize(pointOnSpline) * 0.12f; Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); Handles.color = Color.green; Handles.DrawDottedLine(pointOnSpline, ray.origin, 4); Handles.SphereHandleCap(0, pointOnSpline, Quaternion.identity, size, Event.current.type); if (Event.current.type == EventType.MouseDown && Event.current.modifiers == EventModifiers.None) { Undo.RecordObject(blueprint, "Add"); int newIndex = path.InsertControlPoint(mu); if (newIndex >= 0) { ResizeCPArrays(); for (int i = 0; i < selectedStatus.Length; ++i) selectedStatus[i] = false; selectedStatus[newIndex] = true; } path.FlushEvents(); Event.current.Use(); } // Repaint the scene, so that the add control point helpers are updated every frame. SceneView.RepaintAll(); } private void RemoveControlPointsMode(Matrix4x4 matrix) { float mu = ScreenPointToCurveMu(path, Event.current.mousePosition, matrix); Vector3 pointOnSpline = matrix.MultiplyPoint3x4(path.points.GetPositionAtMu(path.Closed, mu)); float size = HandleUtility.GetHandleSize(pointOnSpline) * 0.12f; Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); Handles.color = Color.red; Handles.DrawDottedLine(pointOnSpline, ray.origin, 4); int index = path.GetClosestControlPointIndex(mu); Handles.SphereHandleCap(0, matrix.MultiplyPoint3x4(path.points[index].position), Quaternion.identity, size, Event.current.type); if (Event.current.type == EventType.MouseDown && Event.current.modifiers == EventModifiers.None && index >= 0 && path.ControlPointCount > 2) { Undo.RecordObject(blueprint, "Remove"); path.RemoveControlPoint(index); ResizeCPArrays(); for (int i = 0; i < selectedStatus.Length; ++i) selectedStatus[i] = false; path.FlushEvents(); Event.current.Use(); } // Repaint the scene, so that the add control point helpers are updated every frame. SceneView.RepaintAll(); } protected bool DrawControlPoint(int i) { bool repaint = false; var wp = path.points[i]; float size = HandleUtility.GetHandleSize(wp.position) * 0.04f; if (selectedStatus[i] && showTangentHandles) { Handles.color = handleColor; if (!(i == 0 && !path.Closed)) { Vector3 tangentPosition = wp.inTangentEndpoint; if (Event.current.type == EventType.Repaint) Handles.DrawDottedLine(tangentPosition, wp.position, 2); EditorGUI.BeginChangeCheck(); Handles.DotHandleCap(0, tangentPosition, Quaternion.identity, size, Event.current.type); Vector3 newTangent = Handles.PositionHandle(tangentPosition, Quaternion.identity); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Modify tangent"); wp.SetInTangentEndpoint(newTangent); path.points[i] = wp; path.FlushEvents(); repaint = true; } } if (!(i == path.ControlPointCount - 1 && !path.Closed)) { Vector3 tangentPosition = wp.outTangentEndpoint; if (Event.current.type == EventType.Repaint) Handles.DrawDottedLine(tangentPosition, wp.position, 2); EditorGUI.BeginChangeCheck(); Handles.DotHandleCap(0, tangentPosition, Quaternion.identity, size, Event.current.type); Vector3 newTangent = Handles.PositionHandle(tangentPosition, Quaternion.identity); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Modify tangent"); wp.SetOutTangentEndpoint(newTangent); path.points[i] = wp; path.FlushEvents(); repaint = true; } } } if (Event.current.type == EventType.Repaint) { Handles.color = selectedStatus[i] ? handleColor : Color.white; Vector3 pos = wp.position; if (currentTool == PathEditorTool.OrientPoints) { Handles.ArrowHandleCap(0, pos, Quaternion.LookRotation(path.normals[i]), HandleUtility.GetHandleSize(pos), EventType.Repaint); } Handles.SphereHandleCap(0, pos, Quaternion.identity, size * 3, EventType.Repaint); } return repaint; } protected Vector3 GetControlPointAverage(out int lastSelected, out int selectedCount) { lastSelected = -1; selectedCount = 0; Vector3 averagePos = Vector3.zero; // Find center of all selected control points: for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { averagePos += path.points[i].position; selectedCount++; lastSelected = i; } } if (selectedCount > 0) averagePos /= selectedCount; return averagePos; } protected bool SplineCPTools(Matrix4x4 matrix) { bool repaint = false; // Calculate handle rotation, for local or world pivot modes. Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Local ? Quaternion.identity : Quaternion.Inverse(matrix.rotation); // Reset initial handle rotation/orientation after using a tool: if (GUIUtility.hotControl == 0) { prevRot = handleRotation; prevScale = Vector3.one; if (selectedCount == 1 && Tools.pivotRotation == PivotRotation.Local && currentTool == PathEditorTool.OrientPoints) { //prevRot = Quaternion.LookRotation(GetNormal(lastSelected)); } } // Transform handles: if (selectedCount > 0) { if (useOrientation && currentTool == PathEditorTool.OrientPoints) { repaint |= OrientTool(selectionAverage, handleRotation); } else { switch (currentTool) { case PathEditorTool.TranslatePoints: { repaint |= MoveTool(selectionAverage, handleRotation); } break; case PathEditorTool.ScalePoints: { repaint |= ScaleTool(selectionAverage, handleRotation); } break; case PathEditorTool.RotatePoints: { repaint |= RotateTool(selectionAverage, handleRotation); } break; } } } return repaint; } protected bool MoveTool(Vector3 handlePosition, Quaternion handleRotation) { EditorGUI.BeginChangeCheck(); Vector3 newPos = Handles.PositionHandle(handlePosition, handleRotation); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Move control point"); Vector3 delta = newPos - handlePosition; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; wp.Transform(delta, Quaternion.identity, Vector3.one); path.points[i] = wp; } } path.FlushEvents(); return true; } return false; } protected bool ScaleTool(Vector3 handlePosition, Quaternion handleRotation) { EditorGUI.BeginChangeCheck(); Vector3 scale = Handles.ScaleHandle(prevScale, handlePosition, handleRotation, HandleUtility.GetHandleSize(handlePosition)); if (EditorGUI.EndChangeCheck()) { Vector3 deltaScale = new Vector3(scale.x / prevScale.x, scale.y / prevScale.y, scale.z / prevScale.z); prevScale = scale; Undo.RecordObject(blueprint, "Scale control point"); if (Tools.pivotMode == PivotMode.Center && selectedCount > 1) { for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; Vector3 newPos = handlePosition + Vector3.Scale(wp.position - handlePosition, deltaScale); wp.Transform(newPos - wp.position, Quaternion.identity, Vector3.one); path.points[i] = wp; } } } else { // Scale all handles of selected control points relative to their control point: for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; wp.Transform(Vector3.zero, Quaternion.identity, deltaScale); path.points[i] = wp; } } } path.FlushEvents(); return true; } return false; } protected bool RotateTool(Vector3 handlePosition, Quaternion handleRotation) { EditorGUI.BeginChangeCheck(); // TODO: investigate weird rotation gizmo: Quaternion newRotation = Handles.RotationHandle(prevRot, handlePosition); if (EditorGUI.EndChangeCheck()) { Quaternion delta = newRotation * Quaternion.Inverse(prevRot); prevRot = newRotation; Undo.RecordObject(blueprint, "Rotate control point"); if (Tools.pivotMode == PivotMode.Center && selectedCount > 1) { // Rotate all selected control points around their average: for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; Vector3 newPos = handlePosition + delta * (wp.position - handlePosition); wp.Transform(newPos - wp.position, Quaternion.identity, Vector3.one); path.points[i] = wp; } } } else { // Rotate all handles of selected control points around their control point: for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; wp.Transform(Vector3.zero, delta, Vector3.one); path.points[i] = wp; } } } path.FlushEvents(); return true; } return false; } protected bool OrientTool(Vector3 averagePos, Quaternion pivotRotation) { EditorGUI.BeginChangeCheck(); Quaternion newRotation = Handles.RotationHandle(prevRot, averagePos); if (EditorGUI.EndChangeCheck()) { Quaternion delta = newRotation * Quaternion.Inverse(prevRot); prevRot = newRotation; Undo.RecordObject(blueprint, "Orient control point"); // Rotate all selected control points around their average: for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { path.normals[i] = delta * path.normals[i]; } } path.FlushEvents(); return true; } return false; } protected bool DoThicknessHandles(float scale) { Color oldColor = Handles.color; Handles.color = handleColor; EditorGUI.BeginChangeCheck(); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { Vector3 position = path.points[i].position; var tangent = path.points.GetTangent(i); if (!tangent.Equals(Vector3.zero)) { Quaternion orientation = Quaternion.LookRotation(tangent); float offset = 0.05f; float thickness = (path.thicknesses[i] * scale) + offset; EditorGUI.BeginChangeCheck(); thickness = DoRadiusHandle(orientation, position, thickness); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point thickness"); path.thicknesses[i] = Mathf.Max(0, (thickness - offset) / scale); path.FlushEvents(); return true; } } } } Handles.color = oldColor; return false; } public void DrawToolPanel() { DrawToolButtons(); DrawControlPointInspector(); } private void DrawToolButtons() { GUILayout.BeginHorizontal(); EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.TranslatePoints, new GUIContent(Resources.Load("TranslateControlPoint"), "Translate CPs"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.TranslatePoints; } EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.RotatePoints, new GUIContent(Resources.Load("RotateControlPoint"), "Rotate CPs"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.RotatePoints; } EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.ScalePoints, new GUIContent(Resources.Load("ScaleControlPoint"), "Scale CPs"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.ScalePoints; } EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.InsertPoints, new GUIContent(Resources.Load("AddControlPoint"), "Add CPs"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.InsertPoints; } EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.RemovePoints, new GUIContent(Resources.Load("RemoveControlPoint"), "Remove CPs"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.RemovePoints; } EditorGUI.BeginChangeCheck(); bool closed = GUILayout.Toggle(path.Closed, new GUIContent(Resources.Load("OpenCloseCurve"), "Open/Close the path"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Open/close path"); path.Closed = closed; path.FlushEvents(); needsRepaint = true; } if (useOrientation) { EditorGUI.BeginChangeCheck(); GUILayout.Toggle(currentTool == PathEditorTool.OrientPoints, new GUIContent(Resources.Load("OrientControlPoint"), "Orientation tool"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); if (EditorGUI.EndChangeCheck()) { currentTool = PathEditorTool.OrientPoints; } } showTangentHandles = GUILayout.Toggle(showTangentHandles, new GUIContent(Resources.Load("ShowTangentHandles"), "Show tangent handles"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); showThicknessHandles = GUILayout.Toggle(showThicknessHandles, new GUIContent(Resources.Load("ShowThicknessHandles"), "Show thickness handles"), "Button", GUILayout.MaxHeight(24), GUILayout.Width(38)); GUILayout.EndHorizontal(); } private void DrawPositionField(Rect rect, string label, int index) { EditorGUI.showMixedValue = false; float pos = 0; bool firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { pos = path.points[i].position[index]; firstSelected = false; } else if (!Mathf.Approximately(pos,path.points[i].position[index])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); float oldLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 10; pos = EditorGUI.FloatField(rect, label, pos); EditorGUIUtility.labelWidth = oldLabelWidth; EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control points position"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; wp.position[index] = pos; path.points[i] = wp; } } path.FlushEvents(); needsRepaint = true; } } private void DrawInTangentField(Rect rect, string label, int index) { EditorGUI.showMixedValue = false; float pos = 0; bool firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { pos = path.points[i].inTangent[index]; firstSelected = false; } else if (!Mathf.Approximately(pos, path.points[i].inTangent[index])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); float oldLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 10; pos = EditorGUI.FloatField(rect, label, pos); EditorGUIUtility.labelWidth = oldLabelWidth; EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control points tangent"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; var newInTangent = wp.inTangent; newInTangent[index] = pos; wp.SetInTangent(newInTangent); path.points[i] = wp; } } path.FlushEvents(); needsRepaint = true; } } private void DrawOutTangentField(Rect rect, string label, int index) { EditorGUI.showMixedValue = false; float pos = 0; bool firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { pos = path.points[i].outTangent[index]; firstSelected = false; } else if (!Mathf.Approximately(pos, path.points[i].outTangent[index])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); float oldLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 10; pos = EditorGUI.FloatField(rect, label, pos); EditorGUIUtility.labelWidth = oldLabelWidth; EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control points tangent"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; var newOutTangent = wp.outTangent; newOutTangent[index] = pos; wp.SetOutTangent(newOutTangent); path.points[i] = wp; } } path.FlushEvents(); needsRepaint = true; } } private void DrawControlPointInspector() { GUI.enabled = selectedCount > 0; bool wideMode = EditorGUIUtility.wideMode; EditorGUIUtility.wideMode = true; EditorGUIUtility.labelWidth = 100; EditorGUILayout.BeginVertical(); GUILayout.Box("", ObiEditorUtils.GetSeparatorLineStyle()); // position: var rect = EditorGUILayout.GetControlRect(); rect = EditorGUI.PrefixLabel(rect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Position")); rect.width /= 3.0f; DrawPositionField(rect,"X",0); rect.x += rect.width; DrawPositionField(rect,"Y",1); rect.x += rect.width; DrawPositionField(rect,"Z",2); rect.x += rect.width; // in tangent: rect = EditorGUILayout.GetControlRect(); rect = EditorGUI.PrefixLabel(rect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("In Tangent")); rect.width /= 3.0f; DrawInTangentField(rect, "X", 0); rect.x += rect.width; DrawInTangentField(rect, "Y", 1); rect.x += rect.width; DrawInTangentField(rect, "Z", 2); rect.x += rect.width; // out tangent: rect = EditorGUILayout.GetControlRect(); rect = EditorGUI.PrefixLabel(rect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Out Tangent")); rect.width /= 3.0f; DrawOutTangentField(rect, "X", 0); rect.x += rect.width; DrawOutTangentField(rect, "Y", 1); rect.x += rect.width; DrawOutTangentField(rect, "Z", 2); rect.x += rect.width; // tangent mode: EditorGUI.showMixedValue = false; var mode = ObiWingedPoint.TangentMode.Free; bool firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { mode = path.points[i].tangentMode; firstSelected = false; } else if (mode != path.points[i].tangentMode) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); var newMode = (ObiWingedPoint.TangentMode)EditorGUILayout.EnumPopup("Tangent mode", mode, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control points mode"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { var wp = path.points[i]; wp.tangentMode = newMode; path.points[i] = wp; } } path.FlushEvents(); needsRepaint = true; } // thickness: EditorGUI.showMixedValue = false; float thickness = 0; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { thickness = path.thicknesses[i]; firstSelected = false; } else if (!Mathf.Approximately(thickness, path.thicknesses[i])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); thickness = EditorGUILayout.FloatField("Thickness", thickness, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point thickness"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.thicknesses[i] = Mathf.Max(0, thickness); } path.FlushEvents(); needsRepaint = true; } // mass: EditorGUI.showMixedValue = false; float mass = 0; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { mass = path.masses[i]; firstSelected = false; } else if (!Mathf.Approximately(mass, path.masses[i])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); mass = EditorGUILayout.FloatField("Mass", mass, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point mass"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.masses[i] = mass; } path.FlushEvents(); needsRepaint = true; } if (useOrientation) { // rotational mass: EditorGUI.showMixedValue = false; float rotationalMass = 0; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { rotationalMass = path.rotationalMasses[i]; firstSelected = false; } else if (!Mathf.Approximately(rotationalMass, path.rotationalMasses[i])) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); rotationalMass = EditorGUILayout.FloatField("Rotational mass", rotationalMass, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point rotational mass"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.rotationalMasses[i] = rotationalMass; } path.FlushEvents(); needsRepaint = true; } } // category: EditorGUI.showMixedValue = false; int category = 0; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { category = ObiUtils.GetCategoryFromFilter(path.filters[i]); firstSelected = false; } else if (!Mathf.Approximately(category, ObiUtils.GetCategoryFromFilter(path.filters[i]))) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); category = EditorGUILayout.Popup("Category", category, ObiUtils.categoryNames, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point category"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.filters[i] = ObiUtils.MakeFilter(ObiUtils.GetMaskFromFilter(path.filters[i]),category); } path.FlushEvents(); needsRepaint = true; } // mask: EditorGUI.showMixedValue = false; int mask = 0; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { mask = ObiUtils.GetMaskFromFilter(path.filters[i]); firstSelected = false; } else if (!Mathf.Approximately(mask, ObiUtils.GetMaskFromFilter(path.filters[i]))) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); mask = EditorGUILayout.MaskField("Collides with", mask, ObiUtils.categoryNames, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point mask"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.filters[i] = ObiUtils.MakeFilter(mask,ObiUtils.GetCategoryFromFilter(path.filters[i])); } path.FlushEvents(); needsRepaint = true; } // color: EditorGUI.showMixedValue = false; Color color = Color.white; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { color = path.colors[i]; firstSelected = false; } else if (color != path.colors[i]) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); color = EditorGUILayout.ColorField(new GUIContent("Color"), color, true, true, true, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point color"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.colors[i] = color; } path.FlushEvents(); needsRepaint = true; } // name: EditorGUI.showMixedValue = false; string cpname = ""; firstSelected = true; for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) { if (firstSelected) { cpname = path.GetName(i); firstSelected = false; } else if (cpname != path.GetName(i)) { EditorGUI.showMixedValue = true; break; } } } EditorGUI.BeginChangeCheck(); cpname = EditorGUILayout.DelayedTextField("Name", cpname, GUILayout.MinWidth(94)); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(blueprint, "Change control point name"); for (int i = 0; i < path.ControlPointCount; ++i) { if (selectedStatus[i]) path.SetName(i, cpname); } path.FlushEvents(); needsRepaint = true; } EditorGUILayout.EndVertical(); EditorGUIUtility.wideMode = wideMode; GUI.enabled = true; } internal static float DoRadiusHandle(Quaternion rotation, Vector3 position, float radius) { Vector3[] vector3Array; Vector3 camToPosition; if (Camera.current.orthographic) { camToPosition = Camera.current.transform.forward; Handles.DrawWireDisc(position, camToPosition, radius); vector3Array = new Vector3[4] { Camera.current.transform.right, Camera.current.transform.up, -Camera.current.transform.right, -Camera.current.transform.up, }; } else { camToPosition = position - Camera.current.transform.position; Handles.DrawWireDisc(position, rotation * Vector3.forward, radius); vector3Array = new Vector3[4] { rotation * Vector3.right, rotation * Vector3.up, rotation * -Vector3.right, rotation * -Vector3.up, }; } for (int index = 0; index < 4; ++index) { int controlId = GUIUtility.GetControlID("ObiPathThicknessHandle".GetHashCode(), FocusType.Passive); Vector3 position1 = position + radius * vector3Array[index]; bool changed = GUI.changed; GUI.changed = false; Vector3 a = Handles.Slider(controlId, position1, vector3Array[index], HandleUtility.GetHandleSize(position1) * 0.03f, Handles.DotHandleCap, 0.0f); if (GUI.changed) radius = Vector3.Distance(a, position); GUI.changed |= changed; } return radius; } public static float ScreenPointToCurveMu(ObiPath path, Vector2 screenPoint, Matrix4x4 referenceFrame, int samples = 30) { if (path.ControlPointCount >= 2) { samples = Mathf.Max(1, samples); float step = 1 / (float)samples; float closestMu = 0; float minDistance = float.MaxValue; for (int k = 0; k < path.GetSpanCount(); ++k) { int nextCP = (k + 1) % path.ControlPointCount; var wp1 = path.points[k]; var wp2 = path.points[nextCP]; Vector3 _p = referenceFrame.MultiplyPoint3x4(wp1.position); Vector3 p = referenceFrame.MultiplyPoint3x4(wp1.outTangentEndpoint); Vector3 p_ = referenceFrame.MultiplyPoint3x4(wp2.inTangentEndpoint); Vector3 p__ = referenceFrame.MultiplyPoint3x4(wp2.position); Vector2 lastPoint = HandleUtility.WorldToGUIPoint(path.m_Points.Evaluate(_p, p, p_, p__, 0)); for (int i = 1; i <= samples; ++i) { Vector2 currentPoint = HandleUtility.WorldToGUIPoint(path.m_Points.Evaluate(_p, p, p_, p__, i * step)); float mu; float distance = Vector2.SqrMagnitude((Vector2)ObiUtils.ProjectPointLine(lastPoint, currentPoint, screenPoint, out mu) - screenPoint); if (distance < minDistance) { minDistance = distance; closestMu = (k + (i - 1) * step + mu / samples) / (float)path.GetSpanCount(); } lastPoint = currentPoint; } } return closestMu; } else { Debug.LogWarning("Curve needs at least 2 control points to be defined."); } return 0; } } }