You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1180 lines
41 KiB
C#
1180 lines
41 KiB
C#
//#define POLYBRUSH_DEBUG
|
|
|
|
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine.Polybrush;
|
|
using UnityEditor.SettingsManagement;
|
|
using System;
|
|
using UnityEngine.Profiling;
|
|
|
|
namespace UnityEditor.Polybrush
|
|
{
|
|
/// <summary>
|
|
/// Interface and settings for Polybrush
|
|
/// </summary>
|
|
internal class PolybrushEditor : ConfigurableWindow
|
|
{
|
|
static PolybrushEditor s_Instance = null;
|
|
internal static PolybrushEditor instance { get { return s_Instance; } }
|
|
|
|
const string k_BrushSettingsAssetPref = "Polybrush::Editor.brushSettingsAsset";
|
|
const string k_BrushSettingsPref = "Polybrush::Editor.brushSettings";
|
|
const string k_BrushSettingsName = "Polybrush::Editor.brushSettingsName";
|
|
|
|
const double k_EditorTargetFrameLow = .016667;
|
|
const double k_EditorTargetFramerateHigh = .03;
|
|
|
|
static readonly Vector2 k_EditorWindowMinimumSize = new Vector2(320, 180);
|
|
|
|
static List<Ray> s_Rays = new List<Ray>();
|
|
|
|
/// <summary>
|
|
/// Set true to have Polybrush window in floating mode (no dockable).
|
|
/// </summary>
|
|
[UserSetting]
|
|
internal static Pref<bool> s_FloatingWindow = new Pref<bool>("Editor.FloatingWindow", false, SettingsScope.Project);
|
|
|
|
[SerializeField]
|
|
List<BrushMode> modes = new List<BrushMode>();
|
|
|
|
// A reference to the saved preset brush settings.
|
|
[SerializeField]
|
|
BrushSettings brushSettingsAsset;
|
|
|
|
// Editor for the current brush settings.
|
|
BrushSettingsEditor m_BrushEditor = null;
|
|
double m_LastBrushUpdate = 0.0;
|
|
|
|
/// <summary>
|
|
/// Editor for the brush mirror settings
|
|
/// </summary>
|
|
MirrorSettingsEditor m_BrushMirrorEditor = null;
|
|
|
|
|
|
#if !UNITY_2021_1_OR_NEWER
|
|
// gameobjects that are temporarily ignored by HandleUtility.PickGameObject.
|
|
List<GameObject> m_IgnoreDrag = new List<GameObject>(8);
|
|
#endif
|
|
// All objects that have been hovered by the mouse
|
|
Dictionary<GameObject, BrushTarget> m_Hovering = new Dictionary<GameObject, BrushTarget>();
|
|
GameObject m_LastHoveredGameObject = null;
|
|
int m_CurrentBrushIndex = 0;
|
|
BrushSettings[] m_AvailableBrushes = null;
|
|
string[] m_AvailableBrushesStrings = null;
|
|
|
|
// Keep track of the objects that have been registered for undo, allowing the editor to
|
|
// restrict undo calls to only the necessary meshes when applying a brush swath.
|
|
List<GameObject> m_UndoQueue = new List<GameObject>();
|
|
bool m_WantsRepaint = false;
|
|
bool m_ApplyingBrush = false;
|
|
Vector2 m_Scroll = Vector2.zero;
|
|
|
|
// The current editing mode (RaiseLower, Smooth, Color, etc).
|
|
internal BrushTool tool = BrushTool.None;
|
|
|
|
// The current brush status
|
|
internal BrushTarget brushTarget = null;
|
|
|
|
// The current secondary brushes
|
|
internal List<BrushTarget> m_SecondaryBrushTargets = new List<BrushTarget>();
|
|
|
|
// The secondary brushes detected at the last frame
|
|
internal List<BrushTarget> m_LastSecondaryBrushTargets = new List<BrushTarget>();
|
|
|
|
// The first GameObject being stroked
|
|
internal GameObject firstGameObject = null;
|
|
|
|
// The current brush settings
|
|
internal BrushSettings brushSettings;
|
|
|
|
GUIContent[] m_GCToolmodeIcons = null;
|
|
|
|
GUIContent m_GCSaveBrushSettings = new GUIContent("Save", "Save the brush settings as a preset");
|
|
|
|
// The current editing mode (RaiseLower, Smooth, Color, etc).
|
|
internal BrushMode mode
|
|
{
|
|
get
|
|
{
|
|
return modes.Count > 0 ? modes[0] : null;
|
|
}
|
|
set
|
|
{
|
|
if (modes.Contains(value))
|
|
modes.Remove(value);
|
|
modes.Insert(0, value);
|
|
}
|
|
}
|
|
|
|
internal BrushSettingsEditor brushEditor
|
|
{
|
|
get
|
|
{
|
|
if (m_BrushEditor == null && brushSettings != null)
|
|
{
|
|
m_BrushEditor = (BrushSettingsEditor)Editor.CreateEditor(brushSettings);
|
|
}
|
|
else if (m_BrushEditor.target != brushSettings)
|
|
{
|
|
GameObject.DestroyImmediate(m_BrushEditor);
|
|
|
|
if (brushSettings != null)
|
|
m_BrushEditor = (BrushSettingsEditor)Editor.CreateEditor(brushSettings);
|
|
}
|
|
|
|
return m_BrushEditor;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch between floating window and dockable window
|
|
/// </summary>
|
|
/// <param name="floating">should reopen the window as floating or not ?</param>
|
|
void SetWindowFloating(bool floating)
|
|
{
|
|
s_FloatingWindow.value = floating;
|
|
|
|
BrushTool tool = s_Instance.tool;
|
|
|
|
GetWindow<PolybrushEditor>().Close();
|
|
MenuItems.MenuInitEditorWindow();
|
|
|
|
s_Instance.SetTool(tool);
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
if (!PrefUtility.VersionCheck())
|
|
PrefUtility.ClearPrefs();
|
|
|
|
PolybrushEditor.s_Instance = this;
|
|
|
|
// Editor window setup
|
|
titleContent = new GUIContent("Polybrush");
|
|
wantsMouseMove = true;
|
|
minSize = k_EditorWindowMinimumSize;
|
|
|
|
m_BrushMirrorEditor = new MirrorSettingsEditor();
|
|
|
|
if (ProBuilderBridge.ProBuilderExists())
|
|
ProBuilderBridge.SubscribeToSelectModeChanged(OnProBuilderSelectModeChanged);
|
|
|
|
m_GCToolmodeIcons = new GUIContent[]
|
|
{
|
|
EditorGUIUtility.TrIconContent(IconUtility.GetIcon("Toolbar/Sculpt"), "Sculpt on meshes"),
|
|
EditorGUIUtility.TrIconContent(IconUtility.GetIcon("Toolbar/Smooth"), "Smooth mesh geometry"),
|
|
EditorGUIUtility.TrIconContent(IconUtility.GetIcon("Toolbar/PaintVertexColors"), "Paint vertex colors on meshes"),
|
|
EditorGUIUtility.TrIconContent(IconUtility.GetIcon("Toolbar/PaintPrefabs"), "Scatter Prefabs on meshes"),
|
|
EditorGUIUtility.TrIconContent(IconUtility.GetIcon("Toolbar/PaintTextures"), "Paint textures on meshes"),
|
|
};
|
|
|
|
#if UNITY_2019_1_OR_NEWER
|
|
SceneView.duringSceneGui += OnSceneGUI;
|
|
#else
|
|
SceneView.onSceneGUIDelegate += OnSceneGUI;
|
|
#endif
|
|
Undo.undoRedoPerformed += UndoRedoPerformed;
|
|
|
|
// force update the preview
|
|
m_LastHoveredGameObject = null;
|
|
|
|
RefreshAvailableBrushList();
|
|
EnsureBrushSettingsListIsValid();
|
|
|
|
SetTool(BrushTool.RaiseLower, false);
|
|
|
|
Selection.selectionChanged -= OnSelectionChanged;
|
|
Selection.selectionChanged += OnSelectionChanged;
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
Selection.selectionChanged -= OnSelectionChanged;
|
|
#if UNITY_2019_1_OR_NEWER
|
|
SceneView.duringSceneGui -= OnSceneGUI;
|
|
#else
|
|
SceneView.onSceneGUIDelegate -= OnSceneGUI;
|
|
#endif
|
|
Undo.undoRedoPerformed -= UndoRedoPerformed;
|
|
//EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyWindowItemChanged;
|
|
|
|
if (ProBuilderBridge.ProBuilderExists())
|
|
ProBuilderBridge.UnsubscribeToSelectModeChanged(OnProBuilderSelectModeChanged);
|
|
|
|
// store local changes to brushSettings
|
|
if (brushSettings != null)
|
|
{
|
|
var js = JsonUtility.ToJson(brushSettings, true);
|
|
EditorPrefs.SetString(k_BrushSettingsPref, js);
|
|
}
|
|
|
|
// don't iterate here! FinalizeAndReset does that
|
|
OnBrushExit( m_LastHoveredGameObject );
|
|
FinalizeAndResetHovering();
|
|
|
|
PreviewsDatabase.UnloadCache();
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
SetTool(BrushTool.None);
|
|
|
|
if (ProBuilderBridge.ProBuilderExists())
|
|
ProBuilderBridge.SetSelectMode(ProBuilderBridge.SelectMode.Object);
|
|
|
|
foreach (BrushMode m in modes)
|
|
GameObject.DestroyImmediate(m);
|
|
|
|
if (brushSettings != null)
|
|
GameObject.DestroyImmediate(brushSettings);
|
|
|
|
if (m_BrushEditor != null)
|
|
GameObject.DestroyImmediate(m_BrushEditor);
|
|
}
|
|
|
|
internal static void DoRepaint()
|
|
{
|
|
if(PolybrushEditor.instance != null)
|
|
PolybrushEditor.instance.m_WantsRepaint = true;
|
|
}
|
|
|
|
void OnGUI()
|
|
{
|
|
Profiler.BeginSample("Polybrush GUI");
|
|
Event e = Event.current;
|
|
GUILayout.Space(8);
|
|
|
|
DoContextMenu();
|
|
DrawToolbar();
|
|
CheckForEscapeKey(e);
|
|
|
|
// Call current mode GUI
|
|
if (mode != null)
|
|
{
|
|
m_Scroll = EditorGUILayout.BeginScrollView(m_Scroll);
|
|
|
|
DrawBrushSettings();
|
|
m_BrushMirrorEditor.OnGUI();
|
|
if (tool != BrushTool.None)
|
|
DrawActiveToolmodeSettings();
|
|
|
|
EditorGUILayout.Space();
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
mode.OnBrushSettingsChanged(brushTarget, brushSettings);
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
|
|
#if POLYBRUSH_DEBUG
|
|
GUILayout.Label("DEBUG", EditorStyles.boldLabel);
|
|
|
|
GUILayout.Label("target: " + (Util.IsValid(brushTarget) ? brushTarget.editableObject.gameObjectAttached.name : "null"));
|
|
GUILayout.Label("vertex: " + (Util.IsValid(brushTarget) ? brushTarget.editableObject.vertexCount : 0));
|
|
GUILayout.Label("applying: " + m_ApplyingBrush);
|
|
GUILayout.Label("lockBrushToFirst: " + s_LockBrushToFirst);
|
|
GUILayout.Label("lastHoveredGameObject: " + m_LastHoveredGameObject);
|
|
|
|
GUILayout.Space(6);
|
|
|
|
foreach(var kvp in m_Hovering)
|
|
{
|
|
BrushTarget t = kvp.Value;
|
|
EditableObject dbg_editable = t.editableObject;
|
|
GUILayout.Label("Vertex Streams: " + dbg_editable.usingVertexStreams);
|
|
GUILayout.Label("Original: " + (dbg_editable.originalMesh == null ? "null" : dbg_editable.originalMesh.name));
|
|
GUILayout.Label("Active: " + (dbg_editable.editMesh == null ? "null" : dbg_editable.editMesh.name));
|
|
GUILayout.Label("Graphics: " + (dbg_editable.graphicsMesh == null ? "null" : dbg_editable.graphicsMesh.name));
|
|
}
|
|
#endif
|
|
|
|
if(m_WantsRepaint)
|
|
{
|
|
m_WantsRepaint = false;
|
|
Repaint();
|
|
}
|
|
Profiler.EndSample();
|
|
}
|
|
|
|
void DrawToolbar()
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
int toolbarIndex = (int)tool - 1;
|
|
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
toolbarIndex = GUILayout.Toolbar(toolbarIndex, m_GCToolmodeIcons, GUILayout.Width(s_Instance.position.width - 6), GUILayout.Height(23));
|
|
}
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
BrushTool newTool = (BrushTool)(toolbarIndex + 1);
|
|
SetTool(newTool == tool ? BrushTool.None : (BrushTool)toolbarIndex + 1);
|
|
}
|
|
}
|
|
|
|
void DrawBrushSettings()
|
|
{
|
|
EnsureBrushSettingsListIsValid();
|
|
|
|
// Brush preset selector
|
|
using (new GUILayout.VerticalScope("box"))
|
|
{
|
|
// Show the settings header in PolyEditor so that the preset selector can be included in the block.
|
|
// Can't move preset selector to BrushSettingsEditor because it's a CustomEditor for BrushSettings,
|
|
// along with other issues.
|
|
if (PolyGUILayout.HeaderWithDocsLink(PolyGUI.TempContent("Brush Settings")))
|
|
Application.OpenURL(PrefUtility.documentationBrushSettingsLink);
|
|
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
m_CurrentBrushIndex = EditorGUILayout.Popup(m_CurrentBrushIndex, m_AvailableBrushesStrings);
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
if(m_CurrentBrushIndex >= m_AvailableBrushes.Length)
|
|
SetBrushSettings(BrushSettingsEditor.AddNew(brushSettings));
|
|
else
|
|
SetBrushSettings(m_AvailableBrushes.ElementAt<BrushSettings>(m_CurrentBrushIndex));
|
|
}
|
|
|
|
if (GUILayout.Button(m_GCSaveBrushSettings, GUILayout.Width(50)))
|
|
{
|
|
if (brushSettings != null && brushSettingsAsset != null)
|
|
{
|
|
// integer 0, 1 or 2 corresponding to ok, cancel and alt buttons
|
|
int res = EditorUtility.DisplayDialogComplex("Save Brush Settings", "Overwrite brush preset, or Create a New brush preset? ", "Overwrite", "Create New", "Cancel");
|
|
|
|
if (res == 0)
|
|
{
|
|
brushSettings.CopyTo(brushSettingsAsset);
|
|
EditorGUIUtility.PingObject(brushSettingsAsset);
|
|
}
|
|
else if (res == 1)
|
|
{
|
|
BrushSettings dup = BrushSettingsEditor.AddNew(brushSettings);
|
|
SetBrushSettings(dup);
|
|
EditorGUIUtility.PingObject(brushSettingsAsset);
|
|
}
|
|
|
|
GUIUtility.ExitGUI();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("Something went wrong saving brush settings.");
|
|
}
|
|
}
|
|
}
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
brushEditor.OnInspectorGUI();
|
|
}
|
|
}
|
|
|
|
void DrawActiveToolmodeSettings()
|
|
{
|
|
// Toolmode section
|
|
using (new GUILayout.VerticalScope("box"))
|
|
{
|
|
mode.DrawGUI(brushSettings);
|
|
}
|
|
}
|
|
|
|
private void CheckForEscapeKey(Event e)
|
|
{
|
|
if (e.type == EventType.KeyDown)
|
|
if (e.keyCode == KeyCode.Escape)
|
|
{
|
|
SetTool(BrushTool.None);
|
|
e.Use();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate called when ProBuilder changes select mode.
|
|
/// </summary>
|
|
void OnProBuilderSelectModeChanged(int mode)
|
|
{
|
|
// Top = 0,
|
|
// Geometry = 1,
|
|
// Texture = 2,
|
|
// Plugin = 4
|
|
|
|
if(mode > 0 && tool != BrushTool.None)
|
|
SetTool(BrushTool.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch Polybrush to the given tool.
|
|
/// </summary>
|
|
/// <param name="brushTool">Tool to show in Polybrush window.</param>
|
|
/// <param name="enableTool">If true, will activate the given tool automatically. Default: true.</param>
|
|
internal void SetTool(BrushTool brushTool, bool enableTool = true)
|
|
{
|
|
if(brushTool == tool && mode != null)
|
|
return;
|
|
|
|
if (ProBuilderBridge.ProBuilderExists())
|
|
ProBuilderBridge.SetSelectMode(ProBuilderBridge.SelectMode.Object);
|
|
|
|
if(mode != null)
|
|
{
|
|
// Exiting edit mode
|
|
if(m_LastHoveredGameObject != null)
|
|
{
|
|
OnBrushExit( m_LastHoveredGameObject );
|
|
FinalizeAndResetHovering();
|
|
}
|
|
|
|
mode.OnDisable();
|
|
}
|
|
|
|
m_LastHoveredGameObject = null;
|
|
|
|
System.Type modeType = brushTool.GetModeType();
|
|
|
|
if(modeType != null)
|
|
{
|
|
mode = modes.FirstOrDefault(x => x != null && x.GetType() == modeType);
|
|
|
|
if(mode == null)
|
|
mode = (BrushMode) ScriptableObject.CreateInstance( modeType );
|
|
}
|
|
|
|
// Handle tool auto activation/deactivation.
|
|
tool = enableTool? brushTool : BrushTool.None;
|
|
|
|
if(tool != BrushTool.None)
|
|
{
|
|
Tools.current = Tool.None;
|
|
mode.OnEnable();
|
|
}
|
|
|
|
EnsureBrushSettingsListIsValid();
|
|
DoRepaint();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the current available brushes to update the dropdown list.
|
|
/// </summary>
|
|
void RefreshAvailableBrushList()
|
|
{
|
|
m_AvailableBrushes = BrushSettingsEditor.GetAvailableBrushes();
|
|
m_AvailableBrushesStrings = m_AvailableBrushes.Select(x => x.name).ToArray();
|
|
var brushSettingsName = brushSettings != null ? brushSettings.name :
|
|
(EditorPrefs.HasKey(k_BrushSettingsName) ?
|
|
EditorPrefs.GetString(k_BrushSettingsName) :
|
|
String.Empty);
|
|
m_CurrentBrushIndex = System.Math.Max(Array.FindIndex<string>(m_AvailableBrushesStrings,
|
|
x => x == brushSettingsName),0);
|
|
ArrayUtility.Add<string>(ref m_AvailableBrushesStrings, string.Empty);
|
|
ArrayUtility.Add<string>(ref m_AvailableBrushesStrings, "Add Brush...");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes sure we always have a valid BrushSettings selected in Polybrush then refresh the available list for the EditorWindow.
|
|
/// Will create a new file if it cannot find any.
|
|
/// </summary>
|
|
internal void EnsureBrushSettingsListIsValid()
|
|
{
|
|
VerifyLoadedBrushAssetsIntegrity();
|
|
if (brushSettings == null)
|
|
{
|
|
if (brushSettingsAsset == null)
|
|
brushSettingsAsset = BrushSettingsEditor.LoadBrushSettingsAssets(EditorPrefs.GetString(k_BrushSettingsAssetPref, ""));
|
|
|
|
if (EditorPrefs.HasKey(k_BrushSettingsPref) && brushSettingsAsset != null)
|
|
{
|
|
brushSettings = ScriptableObject.CreateInstance<BrushSettings>();
|
|
JsonUtility.FromJsonOverwrite(EditorPrefs.GetString(k_BrushSettingsPref), brushSettings);
|
|
if (EditorPrefs.HasKey(k_BrushSettingsName))
|
|
brushSettings.name = EditorPrefs.GetString(k_BrushSettingsName);
|
|
}
|
|
else
|
|
{
|
|
SetBrushSettings(brushSettingsAsset != null ? brushSettingsAsset : PolyEditorUtility.GetFirstOrNew<BrushSettings>());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify if all loaded assets haven't been touched by users.
|
|
/// If one or multiples assets are missing, refresh the Palettes list and loadouts.
|
|
/// </summary>
|
|
void VerifyLoadedBrushAssetsIntegrity()
|
|
{
|
|
if (m_AvailableBrushes != null && m_AvailableBrushes.Length > 0 &&
|
|
!System.Array.TrueForAll(m_AvailableBrushes, x => x != null))
|
|
{
|
|
RefreshAvailableBrushList();
|
|
m_CurrentBrushIndex = 0;
|
|
if (m_AvailableBrushes.Length > 0)
|
|
SetBrushSettings(m_AvailableBrushes[m_CurrentBrushIndex]);
|
|
else
|
|
SetBrushSettings(BrushSettingsEditor.AddNew(brushSettings));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the default brush settings
|
|
/// </summary>
|
|
/// <returns>first found brush settings, or a new created one</returns>
|
|
internal BrushSettings GetDefaultSettings()
|
|
{
|
|
return m_AvailableBrushes.FirstOrDefault(x => x.name.Contains("Default"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change brush settings
|
|
/// </summary>
|
|
/// <param name="settings">The new brush settings</param>
|
|
internal void SetBrushSettings(BrushSettings settings)
|
|
{
|
|
if(brushSettings != null && brushSettings != settings)
|
|
DestroyImmediate(brushSettings);
|
|
|
|
EditorPrefs.SetString(k_BrushSettingsAssetPref, AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(settings)));
|
|
EditorPrefs.SetString(k_BrushSettingsName, settings.name);
|
|
|
|
brushSettingsAsset = settings;
|
|
brushSettings = settings.DeepCopy();
|
|
brushSettings.hideFlags = HideFlags.HideAndDontSave;
|
|
|
|
RefreshAvailableBrushList();
|
|
}
|
|
|
|
void OnSceneGUI(SceneView sceneView)
|
|
{
|
|
if(mode == null)
|
|
return;
|
|
|
|
Event e = Event.current;
|
|
|
|
CheckForEscapeKey(e);
|
|
|
|
if (Tools.current != Tool.None)
|
|
SetTool(BrushTool.None);
|
|
|
|
if(brushSettings == null)
|
|
SetBrushSettings(PolyEditorUtility.GetFirstOrNew<BrushSettings>());
|
|
|
|
if (PolySceneUtility.SceneViewInUse(e) || tool == BrushTool.None)
|
|
{
|
|
// Force exit the current brush if user's mouse left
|
|
// the SceneView while a brush was still in use.
|
|
if (m_ApplyingBrush)
|
|
OnFinishApplyingBrush();
|
|
return;
|
|
}
|
|
|
|
int controlID = GUIUtility.GetControlID(FocusType.Passive);
|
|
|
|
if( Util.IsValid(brushTarget) )
|
|
HandleUtility.AddDefaultControl(controlID);
|
|
|
|
switch( e.GetTypeForControl(controlID) )
|
|
{
|
|
case EventType.MouseMove:
|
|
// Handles:
|
|
// OnBrushEnter
|
|
// OnBrushExit
|
|
// OnBrushMove
|
|
if( EditorApplication.timeSinceStartup - m_LastBrushUpdate > GetTargetFramerate(brushTarget) )
|
|
{
|
|
m_LastBrushUpdate = EditorApplication.timeSinceStartup;
|
|
UpdateBrush(e.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
}
|
|
break;
|
|
|
|
case EventType.MouseDown:
|
|
case EventType.MouseDrag:
|
|
// Handles:
|
|
// OnBrushBeginApply
|
|
// OnBrushApply
|
|
// OnBrushFinishApply
|
|
if( EditorApplication.timeSinceStartup - m_LastBrushUpdate > GetTargetFramerate(brushTarget) )
|
|
{
|
|
m_LastBrushUpdate = EditorApplication.timeSinceStartup;
|
|
UpdateBrush(e.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
ApplyBrush(Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
}
|
|
break;
|
|
|
|
case EventType.MouseUp:
|
|
if(m_ApplyingBrush)
|
|
{
|
|
OnFinishApplyingBrush();
|
|
UpdateBrush(e.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
}
|
|
break;
|
|
|
|
case EventType.ScrollWheel:
|
|
ScrollBrushSettings(e);
|
|
break;
|
|
case EventType.KeyDown:
|
|
// Key down event continues as long as the key is held down. However, we only need to update the brush once while the key is down.
|
|
// Check if the key has already been marked as pressed in the brush settings, and don't update if it is.
|
|
if (((e.keyCode == KeyCode.LeftControl || e.keyCode == KeyCode.RightControl) && !brushSettings.isUserHoldingControl) ||
|
|
((e.keyCode == KeyCode.LeftShift || e.keyCode == KeyCode.RightShift) && !brushSettings.isUserHoldingShift))
|
|
{
|
|
m_LastBrushUpdate = EditorApplication.timeSinceStartup;
|
|
UpdateBrush(e.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
}
|
|
break;
|
|
case EventType.KeyUp:
|
|
// Key up only happens once, so don't need to check if we were already holding control/shift
|
|
if ((e.keyCode == KeyCode.LeftControl || e.keyCode == KeyCode.RightControl) ||
|
|
(e.keyCode == KeyCode.LeftShift || e.keyCode == KeyCode.RightShift))
|
|
{
|
|
m_LastBrushUpdate = EditorApplication.timeSinceStartup;
|
|
UpdateBrush(e.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
}
|
|
break;
|
|
|
|
case EventType.Repaint:
|
|
if( Util.IsValid(brushTarget) )
|
|
mode.DrawGizmos(brushTarget, brushSettings);
|
|
break;
|
|
}
|
|
|
|
// foreach(var secondaryBrushTarget in m_SecondaryBrushTargets)
|
|
// {
|
|
// if(Util.IsValid(secondaryBrushTarget))
|
|
// mode.DrawGizmos(secondaryBrushTarget, brushSettings);
|
|
// }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get framerate according to the brush target
|
|
/// </summary>
|
|
/// <param name="target">The brush target</param>
|
|
/// <returns>framerate</returns>
|
|
static double GetTargetFramerate(BrushTarget target)
|
|
{
|
|
if(Util.IsValid(target) && target.vertexCount > 24000)
|
|
return k_EditorTargetFrameLow;
|
|
|
|
return k_EditorTargetFramerateHigh;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a EditableObject matching the GameObject go or create a new one.
|
|
/// </summary>
|
|
/// <param name="go">Gameobject to edit</param>
|
|
/// <returns></returns>
|
|
BrushTarget GetOrCreateBrushTarget(GameObject go)
|
|
{
|
|
BrushTarget target = null;
|
|
|
|
if( !m_Hovering.TryGetValue(go, out target) )
|
|
{
|
|
target = new BrushTarget(EditableObject.Create(go));
|
|
m_Hovering.Add(go, target);
|
|
}
|
|
else if( !target.IsValid() )
|
|
{
|
|
m_Hovering[go] = new BrushTarget(EditableObject.Create(go));
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will display a popup with correct message in case of mismatching method
|
|
/// </summary>
|
|
/// <param name="go">GameObject to check</param>
|
|
/// <result>Will return true if the user decide to convert the GameObject, will return false if the user decide not to convert the GameObject</result>
|
|
bool TryEditingObject(GameObject go)
|
|
{
|
|
bool isObjectUsingAVS = go.GetComponent<PolybrushMesh>() != null;
|
|
|
|
return EditorUtility.DisplayDialog("Mismatched application method", isObjectUsingAVS ?
|
|
"The object you are trying to edit doesn't support instanced mesh editing, do you want to convert it?" :
|
|
"The object you are trying to edit doesn't support additional vertex streams, do you want to convert it?", "Yes", "No");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the current brush object and weights with the current mouse position.
|
|
/// </summary>
|
|
/// <param name="mousePosition">current mouse position (from Event)</param>
|
|
/// <param name="isDrag">optional, is dragging the mouse cursor</param>
|
|
/// <param name="overridenGO">optional, provides an already selected gameobject (used in unit tests only)</param>
|
|
/// <param name="overridenRay"> optional, provides a ray already created (used in unit tests only)</param>
|
|
internal void UpdateBrush(Vector2 mousePosition, bool isUserHoldingControl = false, bool isUserHoldingShift = false, bool isDrag = false, GameObject overridenGO = null, Ray? overridenRay = null)
|
|
{
|
|
MirrorSettings mirrorSettings = m_BrushMirrorEditor.settings;
|
|
|
|
// Must check HandleUtility.PickGameObject only during MouseMoveEvents or errors will rain.
|
|
GameObject go = null;
|
|
brushTarget = null;
|
|
GameObject cur = null;
|
|
|
|
#if UNITY_2021_1_OR_NEWER
|
|
int materialIndex;
|
|
cur = HandleUtility.PickGameObject(mousePosition, false, null, Selection.gameObjects, out materialIndex);
|
|
if(cur != null)
|
|
brushTarget = GetOrCreateBrushTarget(cur);
|
|
|
|
if(brushTarget != null)
|
|
go = cur;
|
|
#else
|
|
int max = 0; // safeguard against unforeseen while loop errors crashing unity
|
|
|
|
do
|
|
{
|
|
int tmp;
|
|
// overloaded PickGameObject ignores array of GameObjects, this is used
|
|
// when there are non-selected gameObjects between the mouse and selected
|
|
// gameObjects.
|
|
cur = overridenGO;
|
|
if (cur == null)
|
|
{
|
|
m_IgnoreDrag.RemoveAll(x => x == null);
|
|
cur = HandleUtility.PickGameObject(mousePosition, m_IgnoreDrag.ToArray(), out tmp);
|
|
}
|
|
|
|
if (cur != null)
|
|
{
|
|
if ( !PolyEditorUtility.InSelection(cur) )
|
|
{
|
|
if(!m_IgnoreDrag.Contains(cur))
|
|
m_IgnoreDrag.Add(cur);
|
|
}
|
|
else
|
|
{
|
|
brushTarget = GetOrCreateBrushTarget(cur);
|
|
|
|
if(brushTarget != null)
|
|
go = cur;
|
|
else
|
|
m_IgnoreDrag.Add(cur);
|
|
}
|
|
}
|
|
} while( go == null && cur != null && max++ < 128);
|
|
#endif
|
|
|
|
bool mouseHoverTargetChanged = false;
|
|
Ray mouseRay = overridenRay != null ? (Ray)overridenRay : HandleUtility.GUIPointToWorldRay(mousePosition);
|
|
// if the mouse hover picked up a valid editable, raycast against that. otherwise
|
|
// raycast all meshes in selection
|
|
if (go == null)
|
|
{
|
|
foreach(var kvp in m_Hovering)
|
|
{
|
|
BrushTarget t = kvp.Value;
|
|
|
|
if (Util.IsValid(t) && DoMeshRaycast(mouseRay, t, mirrorSettings))
|
|
{
|
|
brushTarget = t;
|
|
go = t.gameObject;
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
if (!DoMeshRaycast(mouseRay, brushTarget, mirrorSettings))
|
|
{
|
|
brushTarget = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// if m_Hovering off another gameobject, call OnBrushExit on that last one and mark the
|
|
// target as having been changed
|
|
if( go != m_LastHoveredGameObject)
|
|
{
|
|
if(m_LastHoveredGameObject)
|
|
OnBrushExit(m_LastHoveredGameObject);
|
|
|
|
if (m_ApplyingBrush)
|
|
mode.OnBrushFinishApply(brushTarget, brushSettings);
|
|
|
|
mouseHoverTargetChanged = true;
|
|
m_LastHoveredGameObject = go;
|
|
|
|
foreach (var secondaryTarget in m_LastSecondaryBrushTargets)
|
|
{
|
|
if (!m_SecondaryBrushTargets.Contains(secondaryTarget))
|
|
{
|
|
OnBrushExit(secondaryTarget.gameObject);
|
|
if (m_ApplyingBrush)
|
|
mode.OnBrushFinishApply(brushTarget, brushSettings);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(brushTarget == null)
|
|
{
|
|
SceneView.RepaintAll();
|
|
DoRepaint();
|
|
|
|
m_LastSecondaryBrushTargets.Clear();
|
|
m_SecondaryBrushTargets.Clear();
|
|
|
|
return;
|
|
}
|
|
|
|
brushSettings.isUserHoldingControl = isUserHoldingControl;
|
|
brushSettings.isUserHoldingShift = isUserHoldingShift;
|
|
|
|
if (mouseHoverTargetChanged)
|
|
{
|
|
foreach(var secondaryTarget in m_SecondaryBrushTargets)
|
|
{
|
|
if(!m_LastSecondaryBrushTargets.Contains(secondaryTarget))
|
|
{
|
|
OnBrushEnter(secondaryTarget, brushSettings);
|
|
if(m_ApplyingBrush)
|
|
mode.OnBrushBeginApply(secondaryTarget, brushSettings);
|
|
}
|
|
}
|
|
|
|
//The active brushtarget is the last one to notify the brush
|
|
OnBrushEnter(brushTarget, brushSettings);
|
|
|
|
// brush is in use, adding a new object to the undo
|
|
if(m_ApplyingBrush)
|
|
{
|
|
if(!m_UndoQueue.Contains(go))
|
|
{
|
|
int curGroup = Undo.GetCurrentGroup();
|
|
brushTarget.editableObject.isDirty = true;
|
|
OnBrushBeginApply(brushTarget, brushSettings);
|
|
Undo.CollapseUndoOperations(curGroup);
|
|
}
|
|
else
|
|
mode.OnBrushBeginApply(brushTarget, brushSettings);
|
|
}
|
|
}
|
|
|
|
m_LastSecondaryBrushTargets.Clear();
|
|
m_LastSecondaryBrushTargets.AddRange(m_SecondaryBrushTargets);
|
|
|
|
OnBrushMove();
|
|
|
|
SceneView.RepaintAll();
|
|
DoRepaint();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the weights for this ray.
|
|
/// </summary>
|
|
/// <param name="mouseRay">The ray used to calculate weights</param>
|
|
/// <param name="target">The object on which to calculate the weights</param>
|
|
/// <returns>true if mouseRay hits the target, false otherwise</returns>
|
|
bool DoMeshRaycast(Ray mouseRay, BrushTarget target, MirrorSettings mirrorSettings)
|
|
{
|
|
m_SecondaryBrushTargets.Clear();
|
|
if( !Util.IsValid(target) )
|
|
return false;
|
|
|
|
target.ClearRaycasts();
|
|
|
|
EditableObject editable = target.editableObject;
|
|
|
|
s_Rays.Clear();
|
|
s_Rays.Add(mouseRay);
|
|
|
|
if(mirrorSettings.Axes != BrushMirror.None)
|
|
{
|
|
for(int i = 0; i < 3; i++)
|
|
{
|
|
if( ((uint)mirrorSettings.Axes & (1u << i)) < 1 )
|
|
continue;
|
|
|
|
int len = s_Rays.Count;
|
|
|
|
for(int n = 0; n < len; n++)
|
|
{
|
|
Vector3 flipVec = ((BrushMirror)(1u << i)).ToVector3();
|
|
|
|
if(mirrorSettings.Space == MirrorCoordinateSpace.World)
|
|
{
|
|
Vector3 cen = editable.gameObjectAttached.GetComponent<Renderer>().bounds.center;
|
|
s_Rays.Add( new Ray( Vector3.Scale(s_Rays[n].origin - cen, flipVec) + cen,
|
|
Vector3.Scale(s_Rays[n].direction, flipVec)));
|
|
}
|
|
else
|
|
{
|
|
Transform t = SceneView.lastActiveSceneView.camera.transform;
|
|
Vector3 o = t.InverseTransformPoint(s_Rays[n].origin);
|
|
Vector3 d = t.InverseTransformDirection(s_Rays[n].direction);
|
|
s_Rays.Add(new Ray( t.TransformPoint(Vector3.Scale(o, flipVec)),
|
|
t.TransformDirection(Vector3.Scale(d, flipVec))));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool hitMesh = false;
|
|
|
|
int[] triangles = editable.editMesh.GetTriangles();
|
|
foreach(Ray ray in s_Rays)
|
|
{
|
|
PolyRaycastHit hit;
|
|
|
|
if(PolySceneUtility.WorldRaycast(ray, editable.transform, editable.visualMesh.vertices, triangles,
|
|
out hit))
|
|
{
|
|
target.raycastHits.Add(hit);
|
|
hitMesh = true;
|
|
}
|
|
}
|
|
|
|
PolySceneUtility.CalculateWeightedVertices(target, brushSettings, tool, mode);
|
|
|
|
if(hitMesh)
|
|
{
|
|
Transform[] trs = Selection.GetTransforms(SelectionMode.Unfiltered);
|
|
var hits = target.raycastHits;
|
|
foreach(var selectedTransform in trs)
|
|
{
|
|
bool isValid = false;
|
|
if(selectedTransform != editable.transform)
|
|
{
|
|
BrushTarget secondaryTarget = GetOrCreateBrushTarget(selectedTransform.gameObject);
|
|
isValid = Util.IsValid(secondaryTarget);
|
|
if(isValid)
|
|
{
|
|
m_SecondaryBrushTargets.Add(secondaryTarget);
|
|
secondaryTarget.ClearRaycasts();
|
|
|
|
foreach(var hit in hits)
|
|
{
|
|
PolyRaycastHit secondaryHit = new PolyRaycastHit(hit.distance,
|
|
secondaryTarget.transform.InverseTransformPoint(editable.transform.TransformPoint(hit.position)),
|
|
hit.normal,
|
|
-1);
|
|
secondaryTarget.raycastHits.Add(secondaryHit);
|
|
}
|
|
}
|
|
PolySceneUtility.CalculateWeightedVertices(secondaryTarget, brushSettings, tool, mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
return hitMesh;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply brush to current brush target
|
|
/// </summary>
|
|
/// <param name="isUserHoldingControl"></param>
|
|
/// <param name="isUserHoldingShift"></param>
|
|
internal void ApplyBrush(bool isUserHoldingControl, bool isUserHoldingShift)
|
|
{
|
|
if (!brushTarget.IsValid())
|
|
return;
|
|
|
|
brushSettings.isUserHoldingControl = isUserHoldingControl;
|
|
brushSettings.isUserHoldingShift = isUserHoldingShift;
|
|
|
|
if (!m_ApplyingBrush)
|
|
{
|
|
m_UndoQueue.Clear();
|
|
m_ApplyingBrush = true;
|
|
OnBrushBeginApply(brushTarget, brushSettings);
|
|
|
|
foreach(var secondaryBrushTarget in m_SecondaryBrushTargets)
|
|
OnBrushBeginApply( secondaryBrushTarget, brushSettings);
|
|
}
|
|
|
|
mode.OnBrushApply(brushTarget, brushSettings);
|
|
|
|
foreach(var secondaryBrushTarget in m_SecondaryBrushTargets)
|
|
mode.OnBrushApply( secondaryBrushTarget, brushSettings);
|
|
|
|
SceneView.RepaintAll();
|
|
}
|
|
|
|
void OnBrushBeginApply(BrushTarget brushTarget, BrushSettings settings)
|
|
{
|
|
PolySceneUtility.PushGIWorkflowMode();
|
|
firstGameObject = brushTarget.gameObject;
|
|
mode.RegisterUndo(brushTarget);
|
|
m_UndoQueue.Add(brushTarget.gameObject);
|
|
mode.OnBrushBeginApply(brushTarget, brushSettings);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modify the current brush settings depending on which key the user is pressing while scrolling
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
void ScrollBrushSettings(Event e)
|
|
{
|
|
float nrm = 1f;
|
|
|
|
switch(e.modifiers)
|
|
{
|
|
case EventModifiers.Control:
|
|
nrm = Mathf.Sin(Mathf.Max(.001f, brushSettings.normalizedRadius)) * .03f * (brushSettings.brushRadiusMax - brushSettings.brushRadiusMin);
|
|
brushSettings.radius = brushSettings.radius - (e.delta.y * nrm);
|
|
break;
|
|
|
|
case EventModifiers.Shift:
|
|
nrm = Mathf.Sin(Mathf.Max(.001f, brushSettings.falloff)) * .03f;
|
|
brushSettings.falloff = brushSettings.falloff - e.delta.y * nrm;
|
|
break;
|
|
|
|
case EventModifiers.Control | EventModifiers.Shift:
|
|
nrm = Mathf.Sin(Mathf.Max(.001f, brushSettings.strength)) * .03f;
|
|
brushSettings.strength = brushSettings.strength - e.delta.y * nrm;
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
EditorUtility.SetDirty(brushSettings);
|
|
|
|
if(mode != null)
|
|
{
|
|
UpdateBrush(Event.current.mousePosition, Event.current.control, Event.current.shift && Event.current.type != EventType.ScrollWheel);
|
|
mode.OnBrushSettingsChanged(brushTarget, brushSettings);
|
|
}
|
|
|
|
e.Use();
|
|
DoRepaint();
|
|
SceneView.RepaintAll();
|
|
}
|
|
|
|
void OnSelectionChanged()
|
|
{
|
|
// We want to delete deselected gameObjects from this.m_Hovering
|
|
var toDelete = new List<GameObject>();
|
|
var selectionGameObjects = Selection.gameObjects;
|
|
|
|
foreach (var hovering in m_Hovering.Keys)
|
|
if (!selectionGameObjects.Contains(hovering))
|
|
toDelete.Add(hovering);
|
|
|
|
foreach (var go in toDelete)
|
|
m_Hovering.Remove(go);
|
|
|
|
#if !UNITY_2021_1_OR_NEWER
|
|
m_IgnoreDrag.Clear();
|
|
#endif
|
|
}
|
|
|
|
void OnBrushEnter(BrushTarget target, BrushSettings settings)
|
|
{
|
|
mode.OnBrushEnter(target.editableObject, settings);
|
|
}
|
|
|
|
void OnBrushMove()
|
|
{
|
|
foreach(var secondaryBrushTarget in m_SecondaryBrushTargets)
|
|
mode.OnBrushMove( secondaryBrushTarget, brushSettings);
|
|
|
|
mode.OnBrushMove( brushTarget, brushSettings );
|
|
}
|
|
|
|
void OnBrushExit(GameObject go)
|
|
{
|
|
BrushTarget target;
|
|
|
|
if(go == null || !m_Hovering.TryGetValue(go, out target) || !Util.IsValid(target))
|
|
return;
|
|
|
|
mode.OnBrushExit(target.editableObject);
|
|
|
|
if(!m_ApplyingBrush)
|
|
target.editableObject.Revert();
|
|
}
|
|
|
|
internal void OnFinishApplyingBrush()
|
|
{
|
|
PolySceneUtility.PopGIWorkflowMode();
|
|
firstGameObject = null;
|
|
m_ApplyingBrush = false;
|
|
mode.OnBrushFinishApply(brushTarget, brushSettings);
|
|
FinalizeAndResetHovering();
|
|
|
|
#if !UNITY_2021_1_OR_NEWER
|
|
m_IgnoreDrag.Clear();
|
|
#endif
|
|
}
|
|
|
|
void FinalizeAndResetHovering()
|
|
{
|
|
foreach(var kvp in m_Hovering)
|
|
{
|
|
BrushTarget target = kvp.Value;
|
|
|
|
if(!Util.IsValid(target))
|
|
continue;
|
|
|
|
// if mesh hasn't been modified, revert it back
|
|
// to the original mesh so that unnecessary assets
|
|
// aren't allocated. if it has been modified, let
|
|
// the editableObject apply those changes to the
|
|
// pb_Object if necessary.
|
|
if(!target.editableObject.isDirty)
|
|
target.editableObject.Revert();
|
|
else
|
|
target.editableObject.Apply(true, true);
|
|
}
|
|
|
|
m_Hovering.Clear();
|
|
brushTarget = null;
|
|
m_LastHoveredGameObject = null;
|
|
|
|
if (ProBuilderBridge.ProBuilderExists())
|
|
ProBuilderBridge.RefreshEditor(false);
|
|
|
|
DoRepaint();
|
|
}
|
|
|
|
void UndoRedoPerformed()
|
|
{
|
|
mode.UndoRedoPerformed(m_UndoQueue);
|
|
|
|
for (int i = 0; i < m_UndoQueue.Count; ++i)
|
|
{
|
|
if (m_Hovering.ContainsKey(m_UndoQueue[i]))
|
|
{
|
|
m_Hovering[m_UndoQueue[i]].editableObject.RemovePolybrushComponentsIfNecessary();
|
|
}
|
|
}
|
|
|
|
if (EditableObject.s_RebuildCollisions)
|
|
{
|
|
foreach (GameObject go in m_UndoQueue.Where(x => x != null))
|
|
{
|
|
MeshCollider mc = go.GetComponent<MeshCollider>();
|
|
MeshFilter mf = go.GetComponent<MeshFilter>();
|
|
|
|
if (mc == null || mf == null || mf.sharedMesh == null)
|
|
continue;
|
|
|
|
if (PrefabUtility.IsPartOfPrefabInstance(go))
|
|
{
|
|
SerializedObject obj = new SerializedObject(mc);
|
|
SerializedProperty prop = obj.FindProperty("m_Mesh");
|
|
PrefabUtility.RevertPropertyOverride(prop, InteractionMode.AutomatedAction);
|
|
}
|
|
else
|
|
{
|
|
mc.sharedMesh = null;
|
|
mc.sharedMesh = mf.sharedMesh;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_Hovering.Clear();
|
|
brushTarget = null;
|
|
m_LastHoveredGameObject = null;
|
|
m_UndoQueue.Clear();
|
|
|
|
SceneView.RepaintAll();
|
|
DoRepaint();
|
|
}
|
|
}
|
|
}
|