#if UNITY_EDITOR using UnityEngine; using UnityEditor; using UnityEngine.UIElements; using System; using System.Collections.Generic; namespace GSpawn { public class ObjectSelection : ScriptableObject { private enum SelectionChangeReason { ClickSelect = 0, MultiSelect, SetSelected, Append, Replaced, SelectPrefabInstances, SelectSimilarPrefabInstances, DeselectPrefabInstances, Delete, FilterOutOfView } // 1-to-1 mapping with ObjectTransformSession.Type private ObjectTransformSession[] _transformSessions = new ObjectTransformSession[Enum.GetValues(typeof(ObjectTransformSessionType)).Length]; private ObjectTransformSession _activeTransformSession = null; // Note: Helps us avoid processing of mouse up events when the mouse is released // after dragging a window. In that case we don't want to update the selection. private bool _receivedMouseDown; private ObjectBounds.QueryConfig _selBoxQConfig = new ObjectBounds.QueryConfig(); private GameObjectType _selBoxDrawTypes = GameObjectType.All & (~GameObjectType.Empty); private ObjectSelectionShape[] _selShapes = new ObjectSelectionShape[] { new ObjectSelectionRect(), new ObjectSelectionSegments(), new ObjectSelectionBox() }; [NonSerialized] private SceneRaycastFilter _replaceRaycastFilter = new SceneRaycastFilter() { objectTypes = GameObjectType.Mesh | GameObjectType.Sprite, raycastGrid = false }; [SerializeField] private ObjectSelectionGizmos _gizmos; [NonSerialized] private ObjectSelectionSettings _settings; [NonSerialized] private ObjectSelectionGrowSettings _growSettings; [NonSerialized] private ObjectProjectionSettings _projectionSettings; [NonSerialized] private ObjectVertexSnapSettings _vertexSnapSettings; [NonSerialized] private ObjectBoxSnapSettings _boxSnapSettings; [NonSerialized] private ObjectSurfaceSnapSettings _surfaceSnapSettings; [NonSerialized] private ObjectModularSnapSettings _modularSnapSettings; [SerializeField] private ObjectSelectionShape.Type _selectionShapeType = ObjectSelectionShape.Type.Rect; [SerializeField] private List _selectedObjects = new List(); [SerializeField] private GameObject _gizmosPivotObject; [NonSerialized] private List _parentsBuffer = new List(); [NonSerialized] private List _parentsTransformBuffer = new List(); [NonSerialized] private List _gameObjectBuffer = new List(); [NonSerialized] private List _prefabInstanceBuffer = new List(); [NonSerialized] private ObjectOutline _selectionHighlight = new ObjectOutline(); [NonSerialized] private List _prefabAssetBuffer = new List(); [NonSerialized] private HashSet _prefabAssetSet = new HashSet(); [NonSerialized] private HashSet _growSet = new HashSet(); [NonSerialized] private List _growSetAddBuffer = new List(); [NonSerialized] private List _growSetRemoveBuffer = new List(); [NonSerialized] private List _objectOverlapBuffer = new List(); private bool appendEnabled { get; set; } private bool multiDeselectEnabled { get; set; } public IEnumerable objectCollection { get { return _selectedObjects; } } public int numSelectedObjects { get { return _selectedObjects.Count; } } public ObjectSelectionGizmos gizmos { get { if (_gizmos == null) _gizmos = ScriptableObject.CreateInstance(); return _gizmos; } } public ObjectSelectionShape.Type selectionShapeType { get { return _selectionShapeType; } set { getSelectionShape().cancel(); _selectionShapeType = value; PluginInspectorUI.instance.refresh(); } } public bool multiSelecting { get { return getSelectionShape().selecting; } } public ObjectSelectionSettings settings { get { if (_settings == null) _settings = AssetDbEx.loadScriptableObject(PluginFolders.settings); return _settings; } } public ObjectSelectionGrowSettings growSettings { get { if (_growSettings == null) _growSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings); return _growSettings; } } public ObjectProjectionSettings projectionSettings { get { if (_projectionSettings == null) _projectionSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings, typeof(ObjectSelection).Name + "_" + typeof(ObjectProjectionSettings).Name); return _projectionSettings; } } public ObjectVertexSnapSettings vertexSnapSettings { get { if (_vertexSnapSettings == null) _vertexSnapSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings, typeof(ObjectSelection).Name + "_" + typeof(ObjectVertexSnapSettings).Name); return _vertexSnapSettings; } } public ObjectBoxSnapSettings boxSnapSettings { get { if (_boxSnapSettings == null) _boxSnapSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings, typeof(ObjectSelection).Name + "_" + typeof(ObjectBoxSnapSettings).Name); return _boxSnapSettings; } } public ObjectSurfaceSnapSettings surfaceSnapSettings { get { if (_surfaceSnapSettings == null) { _surfaceSnapSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings, typeof(ObjectSelection).Name + "_" + typeof(ObjectSurfaceSnapSettings).Name); _surfaceSnapSettings.snapSingleTargetToCursor = false; } return _surfaceSnapSettings; } } public ObjectModularSnapSettings modularSnapSettings { get { if (_modularSnapSettings == null) _modularSnapSettings = AssetDbEx.loadScriptableObject(PluginFolders.settings, typeof(ObjectSelection).Name + "_" + typeof(ObjectModularSnapSettings).Name); return _modularSnapSettings; } } public bool isAnyTransformSessionActive { get { return _activeTransformSession != null; } } public bool clickSelectEnabled { get; set; } public bool multiSelectEnabled { get; set; } public bool gizmosEnabled { get; set; } public static ObjectSelection instance { get { return GSpawn.active.objectSelection; } } public ObjectSelection() { _selBoxQConfig.objectTypes = _selBoxDrawTypes; _selBoxQConfig.volumelessSize = Vector3.one; } public void onSceneGUI() { Event e = Event.current; appendEnabled = false; multiDeselectEnabled = false; if (_activeTransformSession != null) { if (e.type == EventType.KeyDown && FixedShortcuts.cancelAction(e)) endTransformSession(); else if (e.type == EventType.MouseUp && e.button == 0) { if (isTransformSessionActive(ObjectTransformSessionType.SurfaceSnap) || isTransformSessionActive(ObjectTransformSessionType.ModularSnap)) endTransformSession(); } if (_activeTransformSession != null) _activeTransformSession.onSceneGUI(); } else { if (e.type == EventType.MouseDown) { if (FixedShortcuts.selection_ReplaceOnClick(e)) { replaceWithPickedObject(); } } if (FixedShortcuts.selection_EnableAppend(e)) appendEnabled = true; else if (FixedShortcuts.selection_EnableMultiDeselect(e)) multiDeselectEnabled = true; // Draw selection handles here to avoid overwriting the gizmo pixels. // Note: Drawing here means that we are still drawing the old selection (i.e. we're one frame behind) // but it doesn't seem to produce any artifacts. drawSelectionHandles(); // Note: Let the gizmos eat the current GUI event first. Otherwise, there // will be conflicts between event handling (e.g. on MouseUp, the // selection is updated and this can result in the gizmos being // hidden because objects may have been deselected, when in fact the // mouse was released over the gizmo at the end of a drag session). if (gizmosEnabled && (selectionShapeType == ObjectSelectionShape.Type.Rect || !multiSelecting)) gizmos.onSceneGUI(); bool wasMultiSelecting = getSelectionShape().selecting; getSelectionShape().onSceneGUI(); if (wasMultiSelecting && !getSelectionShape().selecting) refreshObjectSelectionUI(); if (e.type == EventType.MouseUp) { // Note: Only proceed if the ALT key is not pressed. If we are orbiting, we // don't want to alter the selection. if (e.button == (int)MouseButton.LeftMouse && !getSelectionShape().selecting && !e.alt && _receivedMouseDown) { clickSelect(); refreshObjectSelectionUI(); } _receivedMouseDown = false; } else if (e.type == EventType.MouseDown) _receivedMouseDown = true; } } public bool canSelectPrefabInstances() { return !isAnyTransformSessionActive; } public void getSelectedPrefabs(List prefabAssets) { prefabAssets.Clear(); _prefabAssetSet.Clear(); foreach(var go in _selectedObjects) { var prefab = go.getPrefabAsset(); if (prefab != null && !_prefabAssetSet.Contains(prefab)) { prefabAssets.Add(prefab); _prefabAssetSet.Add(prefab); } } _prefabAssetSet.Clear(); } public void filterOutOfView() { if (numSelectedObjects == 0) return; UndoEx.record(this); GameObjectEx.getParents(_selectedObjects, _parentsBuffer); var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(PluginCamera.camera); ObjectBounds.QueryConfig boundsQConfig = ObjectBounds.QueryConfig.defaultConfig; foreach (var parent in _parentsBuffer) { var outerMostInstance = parent.getOutermostPrefabInstanceRoot(); if (outerMostInstance != null) { AABB aabb = ObjectBounds.calcHierarchyWorldAABB(outerMostInstance, boundsQConfig); if (aabb.isValid) { if (!GeometryUtility.TestPlanesAABB(frustumPlanes, aabb.toBounds())) { parent.getAllChildrenAndSelf(true, true, _gameObjectBuffer); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); } } } else { AABB aabb = ObjectBounds.calcWorldAABB(parent, boundsQConfig); if (aabb.isValid) { if (!GeometryUtility.TestPlanesAABB(frustumPlanes, aabb.toBounds())) { parent.getAllChildrenAndSelf(true, true, _gameObjectBuffer); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); } } } } onSelectionChanged(SelectionChangeReason.FilterOutOfView); refreshObjectSelectionUI(); } public void selectSimilarPrefabInstances() { if (numSelectedObjects == 0) return; getSelectedPrefabs(_prefabAssetBuffer); UndoEx.record(this); _selectedObjects.Clear(); PluginScene.instance.findPrefabInstances(_prefabAssetBuffer, _prefabInstanceBuffer); _selectedObjects.AddRange(_prefabInstanceBuffer); onSelectionChanged(SelectionChangeReason.SelectSimilarPrefabInstances); refreshObjectSelectionUI(); } public void selectPrefabInstances(List prefabAssets) { if (!canSelectPrefabInstances()) return; UndoEx.record(this); if (!appendEnabled) _selectedObjects.Clear(); PluginScene.instance.findPrefabInstances(prefabAssets, _prefabInstanceBuffer); _selectedObjects.AddRange(_prefabInstanceBuffer); onSelectionChanged(SelectionChangeReason.SelectPrefabInstances); refreshObjectSelectionUI(); } public bool canDeselectPrefabInstances() { return !isAnyTransformSessionActive; } public void deselectPrefabInstances(List prefabAssets) { if (!canDeselectPrefabInstances()) return; UndoEx.record(this); GameObjectEx.getOutermostPrefabInstanceRoots(_selectedObjects, prefabAssets, _gameObjectBuffer, null); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); onSelectionChanged(SelectionChangeReason.DeselectPrefabInstances); refreshObjectSelectionUI(); } public bool canCreatePrefabFromSelection() { return numSelectedObjects != 0 && !isAnyTransformSessionActive; } public GameObject createPrefabFromSelectedObjects(PrefabFromSelectedObjectsCreationSettings prefabCreationSettings) { if (!canCreatePrefabFromSelection()) return null; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); return PrefabFactory.create(_parentsBuffer, prefabCreationSettings); } public bool canReplace() { return numSelectedObjects != 0 && !isAnyTransformSessionActive; } public void replaceWithPickedObject() { if (!canReplace()) return; SceneRayHit rayHit = PluginScene.instance.raycastClosest(PluginCamera.camera.getCursorRay(), _replaceRaycastFilter, ObjectRaycastConfig.defaultConfig); if (rayHit.wasObjectHit) { var prefabInstanceRoot = rayHit.objectHit.hitObject.getOutermostPrefabInstanceRoot(); if (prefabInstanceRoot != null) { var prefab = prefabInstanceRoot.getPrefabAsset(); if (prefab != null) replaceSelection(prefab); } else replaceSelection(rayHit.objectHit.hitObject); } } public void replaceSelection(GameObject replacement) { if (!canReplace()) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); GameObjectEx.getAllObjectsInHierarchies(_parentsBuffer, true, true, _gameObjectBuffer); UndoEx.record(this); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); _gameObjectBuffer.Clear(); if (replacement.isSceneObject()) { foreach (var selectedParent in _parentsBuffer) { var prefabInstanceRoot = selectedParent.getOutermostPrefabInstanceRoot(); if (prefabInstanceRoot != null) { GameObject newObject = GameObject.Instantiate(replacement, prefabInstanceRoot.transform.position, prefabInstanceRoot.transform.rotation); UndoEx.registerCreatedObject(newObject); newObject.transform.localScale = prefabInstanceRoot.transform.lossyScale; newObject.transform.parent = prefabInstanceRoot.transform.parent; _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(prefabInstanceRoot); } else { GameObject newObject = GameObject.Instantiate(replacement, selectedParent.transform.position, selectedParent.transform.rotation); UndoEx.registerCreatedObject(newObject); newObject.transform.localScale = selectedParent.transform.lossyScale; newObject.transform.parent = selectedParent.transform.parent; _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(selectedParent); } } } else { // Note: Currently ignored; causes floating point rounding errors to creep in. // Quaternion baseRotation = replacement.transform.rotation; foreach (var selectedParent in _parentsBuffer) { var prefabInstanceRoot = selectedParent.getOutermostPrefabInstanceRoot(); if (prefabInstanceRoot != null) { GameObject newObject = replacement.instantiatePrefab(); UndoEx.registerCreatedObject(newObject); newObject.transform.position = prefabInstanceRoot.transform.position; newObject.transform.rotation = prefabInstanceRoot.transform.rotation;// * baseRotation; newObject.transform.localScale = prefabInstanceRoot.transform.lossyScale; newObject.transform.parent = prefabInstanceRoot.transform.parent; _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(prefabInstanceRoot); } else { GameObject newObject = replacement.instantiatePrefab(); UndoEx.registerCreatedObject(newObject); newObject.transform.position = selectedParent.transform.position; newObject.transform.rotation = selectedParent.transform.rotation;// * baseRotation; newObject.transform.localScale = selectedParent.transform.lossyScale; newObject.transform.parent = selectedParent.transform.parent; _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(selectedParent); } } } foreach (var go in _gameObjectBuffer) _selectedObjects.Add(go); onSelectionChanged(SelectionChangeReason.Replaced); _gameObjectBuffer.Clear(); _parentsBuffer.Clear(); refreshObjectSelectionUI(); } public void replaceSelection(PluginPrefab prefab) { if (!canReplace()) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); GameObjectEx.getAllObjectsInHierarchies(_parentsBuffer, true, true, _gameObjectBuffer); UndoEx.record(this); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); _gameObjectBuffer.Clear(); // Note: Currently ignored; causes floating point rounding errors to creep in. // Quaternion baseRotation = prefab.prefabAsset.transform.rotation; foreach (var parent in _parentsBuffer) { var prefabInstanceRoot = parent.getOutermostPrefabInstanceRoot(); if (prefabInstanceRoot != null) { GameObject newObject = prefab.spawn(prefabInstanceRoot.transform.position, (prefabInstanceRoot.transform.rotation),// * baseRotation).normalized, prefabInstanceRoot.transform.lossyScale); UndoEx.registerCreatedObject(newObject); _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(prefabInstanceRoot); } else { GameObject newObject = prefab.spawn(parent.transform.position, (parent.transform.rotation),// * baseRotation).normalized, parent.transform.lossyScale); UndoEx.registerCreatedObject(newObject); _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(parent); } } foreach (var go in _gameObjectBuffer) _selectedObjects.Add(go); onSelectionChanged(SelectionChangeReason.Replaced); _gameObjectBuffer.Clear(); _parentsBuffer.Clear(); refreshObjectSelectionUI(); } [NonSerialized] private CumulativeProbabilityTable _replacementPrefabTable = new CumulativeProbabilityTable(); public void replaceSelection(List prefabs) { if (!canReplace()) return; if (prefabs.Count == 0) return; _replacementPrefabTable.clear(); foreach (var prefab in prefabs) _replacementPrefabTable.addEntity(prefab, 1.0f); _replacementPrefabTable.refresh(); GameObjectEx.getParents(_selectedObjects, _parentsBuffer); GameObjectEx.getAllObjectsInHierarchies(_parentsBuffer, true, true, _gameObjectBuffer); UndoEx.record(this); foreach (var go in _gameObjectBuffer) _selectedObjects.Remove(go); _gameObjectBuffer.Clear(); foreach (var parent in _parentsBuffer) { PluginPrefab pickedPrefab = _replacementPrefabTable.pickEntity(); var prefabInstanceRoot = parent.getOutermostPrefabInstanceRoot(); if (prefabInstanceRoot != null) { GameObject newObject = pickedPrefab.spawn(prefabInstanceRoot.transform.position, (prefabInstanceRoot.transform.rotation),// * baseRotation).normalized, prefabInstanceRoot.transform.lossyScale); UndoEx.registerCreatedObject(newObject); _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(prefabInstanceRoot); } else { GameObject newObject = pickedPrefab.spawn(parent.transform.position, (parent.transform.rotation),// * baseRotation).normalized, parent.transform.lossyScale); UndoEx.registerCreatedObject(newObject); _gameObjectBuffer.Add(newObject); UndoEx.destroyGameObjectImmediate(parent); } } foreach (var go in _gameObjectBuffer) _selectedObjects.Add(go); onSelectionChanged(SelectionChangeReason.Replaced); _gameObjectBuffer.Clear(); _parentsBuffer.Clear(); refreshObjectSelectionUI(); } public void frameSelected() { // Note: Selection framing depends on the currently active pivot mode. So make sure // that the Unity Editor pivot mode is synced with the gizmos pivot before // framing the selection. var oldPivotMode = Tools.pivotMode; Tools.pivotMode = gizmos.transformPivot == ObjectGizmoTransformPivot.Center ? PivotMode.Center : PivotMode.Pivot; UnityEditorCommands.frameSelected(_selectedObjects); Tools.pivotMode = oldPivotMode; } public void duplicateSelected() { UnityEditorCommands.duplicate(_selectedObjects); } public void deleteSelected() { if (numSelectedObjects == 0) return; endActiveTransformSession(); GameObjectEx.getParents(_selectedObjects, _parentsBuffer); _parentsBuffer.RemoveAll(item => { var outerPrefabInstance = item.getOutermostPrefabInstanceRoot(); return outerPrefabInstance != null && outerPrefabInstance != item; }); UndoEx.record(this); _gameObjectBuffer.Clear(); _selectedObjects.RemoveAll(item => { foreach (var parent in _parentsBuffer) { if (item.transform.IsChildOf(parent.transform)) { _gameObjectBuffer.Add(item); return true; } } return false; }); onSelectionChanged(SelectionChangeReason.Delete); PluginScene.instance.deleteObjects(_parentsBuffer); refreshObjectSelectionUI(); } public bool isTransformSessionActive(ObjectTransformSessionType sessionType) { if (_activeTransformSession == null) return false; return _activeTransformSession.sessionType == sessionType; } public void beginTransformSession(ObjectTransformSessionType sessionType) { if (isTransformSessionActive(sessionType)) return; getSelectionShape().cancel(); endTransformSession(); UndoEx.recordGameObjectTransforms(_selectedObjects); _activeTransformSession = getTransformSession(sessionType); if (!_activeTransformSession.begin()) { _activeTransformSession = null; } SceneView.RepaintAll(); if (_activeTransformSession != null) ObjectSelectionUI.instance.setObjectTransformUIEnabled(false); } public void onLevelDesignToolChanged() { if (GSpawn.active.levelDesignToolId != LevelDesignToolId.ObjectSelection) endActiveTransformSession(); //onSelectedObjectsMightHaveBeenDeleted(Plugin.active.levelDesignToolId == LevelDesignToolId.ObjectSelection); } public void endActiveTransformSession() { endTransformSession(); } public void endActiveTransformSession(ObjectTransformSessionType sessionType) { if (!isTransformSessionActive(sessionType)) return; endTransformSession(); } public void executeSurfaceSnapSessionCommand(ObjectSurfaceSnapSessionCommand command) { if (isTransformSessionActive(ObjectTransformSessionType.SurfaceSnap)) { var session = (getTransformSession(ObjectTransformSessionType.SurfaceSnap) as ObjectSurfaceSnapSession); session.executeCommand(command); } } public void executeModularSnapSessionCommand(ObjectModularSnapSessionCommand command) { if (isTransformSessionActive(ObjectTransformSessionType.ModularSnap)) { var session = (getTransformSession(ObjectTransformSessionType.ModularSnap) as ObjectModularSnapSession); session.executeCommand(command); } } public void drawGizmos() { if (isTransformSessionActive(ObjectTransformSessionType.BoxSnap) || isTransformSessionActive(ObjectTransformSessionType.SurfaceSnap) || isTransformSessionActive(ObjectTransformSessionType.ModularSnap)) return; drawSelectionGizmos(); } public bool isObjectSelected(GameObject gameObject) { return _selectedObjects.Contains(gameObject); } public bool anyChildrenSelected(GameObject gameObject) { gameObject.getAllChildren(false, false, _gameObjectBuffer); foreach (var child in _gameObjectBuffer) if (isObjectSelected(child)) return true; return false; } public bool canClickSelect() { return clickSelectEnabled && !multiSelecting; } public bool canMultiSelect() { return multiSelectEnabled; } public bool isObjectSelectable(GameObject gameObject) { if (gameObject == null || PluginInstanceData.instance.isPlugin(gameObject) || gameObject.couldBePooled()) return false; if (SceneVisibilityManager.instance.IsHidden(gameObject, false) || LayerEx.isLayerHidden(gameObject.layer) || LayerEx.isPickingDisabled(gameObject.layer) || SceneVisibilityManager.instance.IsPickingDisabled(gameObject, false)) return false; return settings.isGameObjectSelectable(gameObject, GameObjectDataDb.instance.getGameObjectType(gameObject)); } public Vector3 calcSelectionCenter() { if (numSelectedObjects == 0) return Vector3.zero; ObjectBounds.QueryConfig boundsQConfig = ObjectBounds.QueryConfig.defaultConfig; boundsQConfig.objectTypes = GameObjectType.Mesh | GameObjectType.Sprite | GameObjectType.Terrain; Vector3 center = Vector3.zero; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); foreach (var parent in _parentsBuffer) { OBB obb = ObjectBounds.calcHierarchyWorldOBB(parent, boundsQConfig); if (!obb.isValid) continue; center += obb.center; } center *= (1.0f / (float)_parentsBuffer.Count); return center; } public void resetRotationToOriginal() { if (numSelectedObjects == 0) return; if (_activeTransformSession != null && !_activeTransformSession.clientCanUpdateTargetTransforms) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); UndoEx.recordGameObjectTransforms(_parentsBuffer); foreach (var parent in _parentsBuffer) parent.resetRotationToOriginal(); onObjectTransformsChanged(); ObjectEvents.onObjectsTransformed(); } public void setRotation(Quaternion rotation) { if (numSelectedObjects == 0) return; if (_activeTransformSession != null && !_activeTransformSession.clientCanUpdateTargetTransforms) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); UndoEx.recordGameObjectTransforms(_parentsBuffer); foreach (var parent in _parentsBuffer) parent.transform.rotation = rotation; onObjectTransformsChanged(); ObjectEvents.onObjectsTransformed(); } public void resetScaleToOriginal() { if (numSelectedObjects == 0) return; if (_activeTransformSession != null && !_activeTransformSession.clientCanUpdateTargetTransforms) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); UndoEx.recordGameObjectTransforms(_parentsBuffer); foreach (var parent in _parentsBuffer) parent.resetScaleToOriginal(); onObjectTransformsChanged(); ObjectEvents.onObjectsTransformed(); } public void rotate(Vector3 axis, float degrees) { if (numSelectedObjects == 0) return; if (_activeTransformSession != null && !_activeTransformSession.clientCanUpdateTargetTransforms) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); UndoEx.recordGameObjectTransforms(_parentsBuffer); foreach (var parent in _parentsBuffer) parent.transform.Rotate(axis, degrees, Space.World); onObjectTransformsChanged(); ObjectEvents.onObjectsTransformed(); } public void rotate(Vector3 point, Vector3 axis, float degrees) { if (numSelectedObjects == 0) return; if (_activeTransformSession != null && !_activeTransformSession.clientCanUpdateTargetTransforms) return; GameObjectEx.getParents(_selectedObjects, _parentsBuffer); UndoEx.recordGameObjectTransforms(_parentsBuffer); foreach (var parent in _parentsBuffer) parent.transform.RotateAround(point, axis, degrees); onObjectTransformsChanged(); ObjectEvents.onObjectsTransformed(); } public void onObjectTransformsChanged() { foreach (var session in _transformSessions) session.onTargetTransformsChanged(); gizmos.onTargetObjectTransformsChanged(); refreshObjectSelectionUI(); } public void getSelectedObjects(List selectedObjects) { selectedObjects.Clear(); selectedObjects.AddRange(_selectedObjects); } public GameObject getSelectedObject(int index) { return numSelectedObjects != 0 ? _selectedObjects[index] : null; } public void setSelectedObject(GameObject gameObject) { endTransformSession(); UndoEx.record(this); _selectedObjects.Clear(); _gameObjectBuffer.Clear(); _gameObjectBuffer.Add(gameObject); selectObjects(_gameObjectBuffer, true, true); onSelectionChanged(SelectionChangeReason.SetSelected); refreshObjectSelectionUI(); } public void setSelectedObjects(List gameObjects) { endTransformSession(); UndoEx.record(this); _selectedObjects.Clear(); selectObjects(gameObjects, true, true); onSelectionChanged(SelectionChangeReason.SetSelected); refreshObjectSelectionUI(); } public void appendObjects(List gameObjects) { endTransformSession(); UndoEx.record(this); selectObjects(gameObjects, true, true); onSelectionChanged(SelectionChangeReason.Append); refreshObjectSelectionUI(); } public void setMultiSelectedObjects(List gameObjects, ObjectSelectionShape.Type shapeType, UndoConfig undoConfig) { if (!canMultiSelect()) return; if (undoConfig.allowUndoRedo) UndoEx.record(this); if (multiDeselectEnabled && shapeType == ObjectSelectionShape.Type.Rect) { deselectObjects(gameObjects); onSelectionChanged(SelectionChangeReason.MultiSelect); } else { if (appendEnabled) { selectObjects(gameObjects, true, true); onSelectionChanged(SelectionChangeReason.MultiSelect); } else { _selectedObjects.Clear(); selectObjects(gameObjects, false, true); onSelectionChanged(SelectionChangeReason.MultiSelect); } } if (undoConfig.allowUndoRedo && undoConfig.collapseToGroup) Undo.CollapseUndoOperations(undoConfig.groupIndex); } public void snapAllAxes(PluginGrid grid) { GameObjectEx.getParents(_selectedObjects, _parentsBuffer); TransformEx.getTransforms(_parentsBuffer, _parentsTransformBuffer); UndoEx.recordTransforms(_parentsTransformBuffer); grid.snapTransformsAllAxes(_parentsTransformBuffer); gizmos.onTargetObjectsUpdated(_gizmosPivotObject); ObjectEvents.onObjectsTransformed(); } public void projectOnGrid(PluginGrid grid) { endTransformSession(); UndoEx.recordGameObjectTransforms(_selectedObjects); GameObjectEx.getParents(_selectedObjects, _parentsBuffer); ObjectProjection.projectHierarchiesOnPlane(_parentsBuffer, grid.plane, projectionSettings, null); ObjectEvents.onObjectsTransformed(); } public void onObjectsWillBeDeleted(List gameObjects, bool refreshUI) { UndoEx.record(this); if (_selectedObjects.RemoveAll(gameObject => gameObjects.Contains(gameObject)) != 0) { if (_gizmosPivotObject == null) _gizmosPivotObject = numSelectedObjects != 0 ? _selectedObjects[0] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); if (refreshUI) refreshObjectSelectionUI(); } } public void onSelectedObjectsMightHaveBeenDeleted(bool refreshUI) { _selectedObjects.RemoveAll(item => item == null); if (_gizmosPivotObject == null) _gizmosPivotObject = numSelectedObjects != 0 ? _selectedObjects[0] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); if (refreshUI) refreshObjectSelectionUI(); } public void onObjectsWillBeDestroyed(List gameObjects, bool refreshUI) { UndoEx.record(this); if (_selectedObjects.RemoveAll(item => gameObjects.Contains(item)) != 0) { if (_gizmosPivotObject == null) _gizmosPivotObject = numSelectedObjects != 0 ? _selectedObjects[0] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); if (refreshUI) refreshObjectSelectionUI(); } } public void refreshObjectSelectionUI() { ObjectSelectionUI.instance.refreshObjectTransformUI(); PluginInspectorUI.instance.refresh(); } private List _originalGrowPositions = new List(); public void grow() { if (isAnyTransformSessionActive) return; _growSet.Clear(); _prefabAssetSet.Clear(); var boundsQConfig = ObjectBounds.QueryConfig.defaultConfig; boundsQConfig.objectTypes = GameObjectType.Mesh | GameObjectType.Sprite; var objectOverlapFilter = new ObjectOverlapFilter(); objectOverlapFilter.objectTypes = GameObjectType.Mesh | GameObjectType.Sprite; var objectOverlapConfig = ObjectOverlapConfig.defaultConfig; objectOverlapConfig.prefabMode = ObjectOverlapPrefabMode.OnlyPrefabInstanceRoot; var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(PluginCamera.camera); UndoEx.record(this); GameObjectEx.getOutermostPrefabInstanceRoots(_selectedObjects, _prefabInstanceBuffer, null); bool checkPosConstraints = growSettings.xPositionConstraint | growSettings.yPositionConstraint | growSettings.zPositionConstraint; // Note: Start with a clean slate. _selectedObjects.Clear(); _originalGrowPositions.Clear(); foreach (var go in _prefabInstanceBuffer) { _growSet.Add(go); go.getAllChildrenAndSelf(true, true, _gameObjectBuffer); objectOverlapFilter.addIgnoredObjects(_gameObjectBuffer); if (growSettings.usePrefabConstraint) _prefabAssetSet.Add(go.getOutermostPrefabAsset()); // Select the prefab instance. This ensures that when growing, selection will // always contain the top most prefab instance for each object hierarchy, even // though the user may have selected children. _selectedObjects.Add(go); if (checkPosConstraints) { OBB obb = ObjectBounds.calcHierarchyWorldOBB(go, boundsQConfig); if (obb.isValid) _originalGrowPositions.Add(obb.center); } } Vector3 gridRight = PluginScene.instance.grid.right; Vector3 gridUp = PluginScene.instance.grid.up; Vector3 gridLook = PluginScene.instance.grid.look; objectOverlapFilter.customFilter = (go) => { if (growSettings.usePrefabConstraint && !_prefabAssetSet.Contains(go.getOutermostPrefabAsset())) return false; if (growSettings.ignoreOutOfView) { AABB aabb = ObjectBounds.calcHierarchyWorldAABB(go, boundsQConfig); if (aabb.isValid) { if (!GeometryUtility.TestPlanesAABB(frustumPlanes, aabb.toBounds())) return false; } } if (checkPosConstraints) { OBB obb = ObjectBounds.calcHierarchyWorldOBB(go, boundsQConfig); Vector3 obbCenter = obb.center; int numConditionsToSatisfy = 0; if (growSettings.xPositionConstraint) ++numConditionsToSatisfy; if (growSettings.yPositionConstraint) ++numConditionsToSatisfy; if (growSettings.zPositionConstraint) ++numConditionsToSatisfy; float dot; float xErr = growSettings.xPositionThreshold; float yErr = growSettings.yPositionThreshold; float zErr = growSettings.zPositionThreshold; bool satisfiesAll = false; foreach (var originalPos in _originalGrowPositions) { int numSatisfied = 0; Vector3 vec = obbCenter - originalPos; if (growSettings.xPositionConstraint) { dot = Vector3.Dot(gridRight, vec); if (dot < 0.0f && !growSettings.growLeft) continue; if (dot > 0.0f && !growSettings.growRight) continue; if (!growSettings.useXPositionThreshold || Mathf.Abs(dot) <= xErr) ++numSatisfied; } if (growSettings.yPositionConstraint) { dot = Vector3.Dot(gridUp, vec); if (dot < 0.0f && !growSettings.growDown) continue; if (dot > 0.0f && !growSettings.growUp) continue; if (!growSettings.useYPositionThreshold || Mathf.Abs(dot) <= yErr) ++numSatisfied; } if (growSettings.zPositionConstraint) { dot = Vector3.Dot(gridLook, vec); if (dot < 0.0f && !growSettings.growBackward) continue; if (dot > 0.0f && !growSettings.growForward) continue; if (!growSettings.useZPositionThreshold || Mathf.Abs(dot) <= zErr) ++numSatisfied; } if (numSatisfied == numConditionsToSatisfy) { satisfiesAll = true; break; } } if (!satisfiesAll) return false; } return true; }; _growSetRemoveBuffer.Clear(); _growSetAddBuffer.Clear(); float obbInflate = growSettings.distanceThreshold * 2.0f; // Note: Multiply by 2 because when inflating it will inflate by half in all directions. int numSelected = 0; while (_growSet.Count != 0) { foreach (var go in _growSet) { if (canBeIncludedInGrow(go)) { // Build the overlap box Vector3 eulerAngles = go.transform.rotation.eulerAngles; OBB overlapOBB = ObjectBounds.calcHierarchyWorldOBB(go, boundsQConfig); overlapOBB.inflate(obbInflate); // Gather surrounding objects if (PluginScene.instance.overlapBox(overlapOBB, objectOverlapFilter, objectOverlapConfig, _objectOverlapBuffer)) { // Extract the prefab instances GameObjectEx.getOutermostPrefabInstanceRoots(_objectOverlapBuffer, _prefabInstanceBuffer, null); // Add the instances to the grow add buffer and select them foreach (var gameObj in _prefabInstanceBuffer) { if (growSettings.rotationConstraintMode != ObjectSelectionGrowRotationConstraintMode.None) { if (growSettings.rotationConstraintMode == ObjectSelectionGrowRotationConstraintMode.Flexible) { if (!gameObj.transform.checkCoordSystemAxesAlignment(overlapOBB.rotation, growSettings.angleThreshold)) continue; } else if (growSettings.rotationConstraintMode == ObjectSelectionGrowRotationConstraintMode.Exact) { if (gameObj.transform.rotation.eulerAngles != eulerAngles) continue; } } _growSetAddBuffer.Add(gameObj); _selectedObjects.Add(gameObj); gameObj.getAllChildrenAndSelf(true, true, _gameObjectBuffer); objectOverlapFilter.addIgnoredObjects(_gameObjectBuffer); ++numSelected; if (growSettings.useMaxCountConstraint && numSelected == growSettings.maxCount) break; } } } // Mark this object for removal _growSetRemoveBuffer.Add(go); if (growSettings.useMaxCountConstraint && numSelected == growSettings.maxCount) break; } // Remove objects from grow foreach (var go in _growSetRemoveBuffer) _growSet.Remove(go); // Add more objects for grow foreach (var go in _growSetAddBuffer) _growSet.Add(go); _growSetRemoveBuffer.Clear(); _growSetAddBuffer.Clear(); if (growSettings.useMaxCountConstraint && numSelected == growSettings.maxCount) break; } _growSet.Clear(); SceneView.RepaintAll(); } private bool canBeIncludedInGrow(GameObject gameObject) { GameObjectType objectType = GameObjectDataDb.instance.getGameObjectType(gameObject); if (objectType == GameObjectType.Terrain) return false; if (objectType == GameObjectType.Mesh) { if (gameObject.isSphericalMesh()) return false; if (gameObject.isTerrainMesh()) return false; } return true; } private void clickSelect() { if (!canClickSelect()) return; GameObject pickedObject = HandleUtility.PickGameObject(Mouse.instance.position, true); if (appendEnabled) { if (pickedObject != null && isObjectSelectable(pickedObject)) { /* if (pickedObject != null && pickedObject.isPartOfPrefabInstance() && (isObjectSelected(pickedObject) || anyChildrenSelected(pickedObject))) { GameObject pickedChild = HandleUtility.PickGameObject(Mouse.instance.position, false); if (pickedChild != null && isObjectSelectable(pickedChild)) pickedObject = pickedChild; } */ UndoEx.record(this); if (isObjectSelected(pickedObject)) _selectedObjects.Remove(pickedObject); else _selectedObjects.Add(pickedObject); onSelectionChanged(SelectionChangeReason.ClickSelect); } } else { if (ObjectSelectionPrefs.instance.clickSelectAllowChildSelect) { if (pickedObject != null && pickedObject.isPartOfPrefabInstance() && isObjectSelected(pickedObject)) { GameObject pickedChild = HandleUtility.PickGameObject(Mouse.instance.position, false); if (pickedChild != null && isObjectSelectable(pickedChild)) pickedObject = pickedChild; } } UndoEx.record(this); _selectedObjects.Clear(); if (pickedObject != null && isObjectSelectable(pickedObject)) _selectedObjects.Add(pickedObject); onSelectionChanged(SelectionChangeReason.ClickSelect); } } private void selectObjects(IEnumerable gameObjects, bool filterAlreadySelected, bool applyRestrictions) { if (filterAlreadySelected && applyRestrictions) { foreach (var go in gameObjects) { if (isObjectSelectable(go) && !isObjectSelected(go)) _selectedObjects.Add(go); } } else if (filterAlreadySelected && !applyRestrictions) { foreach (var go in gameObjects) { if (!isObjectSelected(go)) _selectedObjects.Add(go); } } else if (!filterAlreadySelected && applyRestrictions) { foreach (var go in gameObjects) { if (isObjectSelectable(go)) _selectedObjects.Add(go); } } else _selectedObjects.AddRange(gameObjects); } private void deselectObjects(IEnumerable gameObjects) { foreach (var go in gameObjects) _selectedObjects.Remove(go); } private void drawSelectionHandles() { if (numSelectedObjects == 0 || Event.current.type != EventType.Repaint) return; if (isTransformSessionActive(ObjectTransformSessionType.BoxSnap) || isTransformSessionActive(ObjectTransformSessionType.SurfaceSnap) || isTransformSessionActive(ObjectTransformSessionType.ModularSnap)) return; _selectionHighlight.setGatherObjects(_selectedObjects); _selectionHighlight.drawHandles(ObjectSelectionPrefs.instance.parentHighlightColor, ObjectSelectionPrefs.instance.childHighlightColor, ObjectSelectionPrefs.instance.highlightOpacity); } private void drawSelectionGizmos() { foreach (var selectedObject in _selectedObjects) { // Note: Should not happen but it seems that sometimes, drawSelectionGizmos is called before // _selectedObjects has a chance to be cleaned up of null objects (e.g. after objects have // been deleted in the scene). if (selectedObject == null) return; if (LayerEx.isLayerHidden(selectedObject.layer) || !selectedObject.activeInHierarchy) return; Camera camera = selectedObject.getCamera(); if (camera != null && camera.enabled) { GizmosEx.saveColor(); Gizmos.color = ObjectSelectionPrefs.instance.cameraGizmoColor; if (!camera.orthographic) Gizmos.DrawFrustum(camera.transform.position, camera.fieldOfView, camera.farClipPlane, camera.nearClipPlane, camera.aspect); else { GizmosEx.saveMatrix(); Vector3 scale = new Vector3(camera.orthographicSize * 2.0f * camera.aspect, camera.orthographicSize * 2.0f, camera.farClipPlane - camera.nearClipPlane); Gizmos.matrix = Matrix4x4.TRS(camera.transform.position + camera.transform.forward * (camera.nearClipPlane + (camera.farClipPlane - camera.nearClipPlane) * 0.5f), camera.transform.rotation, scale); Gizmos.DrawWireCube(Vector3.zero, Vector3.one); GizmosEx.restoreMatrix(); } GizmosEx.restoreColor(); return; } } } private ObjectSelectionShape getSelectionShape() { return _selShapes[(int)_selectionShapeType]; } private void onSelectionChanged(SelectionChangeReason changeReason) { if (changeReason == SelectionChangeReason.MultiSelect && !multiDeselectEnabled && !appendEnabled) { if (_gizmosPivotObject == null || !isObjectSelected(_gizmosPivotObject)) _gizmosPivotObject = numSelectedObjects > 0 ? _selectedObjects[0] : null; } else _gizmosPivotObject = numSelectedObjects > 0 ? _selectedObjects[numSelectedObjects - 1] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); SceneView.RepaintAll(); } private void endTransformSession() { if (_activeTransformSession != null) _activeTransformSession.end(); _activeTransformSession = null; SceneView.RepaintAll(); gizmos.onTargetObjectsUpdated(_gizmosPivotObject); ObjectSelectionUI.instance.setObjectTransformUIEnabled(true); } private ObjectTransformSession getTransformSession(ObjectTransformSessionType sessionType) { return _transformSessions[(int)sessionType]; } private void OnEnable() { if (!FileSystem.folderExists(PluginFolders.settings)) return; EditorApplication.hierarchyChanged += onHierarchyChanged; Selection.selectionChanged += onUnitySelectionChanged; Undo.undoRedoPerformed += onUndoRedo; clickSelectEnabled = true; multiSelectEnabled = true; gizmosEnabled = true; gizmos.bindTargetObjects(_selectedObjects); int numSessions = _transformSessions.Length; for (int i = 0; i < numSessions; ++i) { _transformSessions[i] = ObjectTransformSession.create((ObjectTransformSessionType)i); _transformSessions[i].bindTargetObjects(_selectedObjects); } var projectionSession = (getTransformSession(ObjectTransformSessionType.Projection) as ObjectProjectionSession); projectionSession.sharedSettings = projectionSettings; projectionSession.projected += onProjected; var vertexSnapSession = (getTransformSession(ObjectTransformSessionType.VertexSnap)) as ObjectVertexSnapSession; vertexSnapSession.sharedSettings = vertexSnapSettings; var boxSnapSession = (getTransformSession(ObjectTransformSessionType.BoxSnap)) as ObjectBoxSnapSession; boxSnapSession.sharedSettings = boxSnapSettings; var surfaceSnapSession = (getTransformSession(ObjectTransformSessionType.SurfaceSnap)) as ObjectSurfaceSnapSession; surfaceSnapSession.sharedSettings = surfaceSnapSettings; var modularSnapSession = (getTransformSession(ObjectTransformSessionType.ModularSnap)) as ObjectModularSnapSession; modularSnapSession.sharedSettings = modularSnapSettings; modularSnapSettings.snapSingleTargetToCursor = true; } private void OnDisable() { EditorApplication.hierarchyChanged -= onHierarchyChanged; Selection.selectionChanged -= onUnitySelectionChanged; Undo.undoRedoPerformed -= onUndoRedo; if (!FileSystem.folderExists(PluginFolders.settings)) return; var projectionSession = (getTransformSession(ObjectTransformSessionType.Projection) as ObjectProjectionSession); projectionSession.projected -= onProjected; } private void OnDestroy() { int numSessions = _transformSessions.Length; for (int i = 0; i < numSessions; ++i) { ScriptableObjectEx.destroyImmediate(_transformSessions[i]); _transformSessions[i] = null; } ScriptableObjectEx.destroyImmediate(_gizmos); } private void onHierarchyChanged() { _selectedObjects.RemoveAll(item => item == null); if (_gizmosPivotObject == null) _gizmosPivotObject = numSelectedObjects != 0 ? _selectedObjects[0] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); } private void onUnitySelectionChanged() { if (GSpawn.active == null) return; // Note: We need to refresh the gizmos in case the selected objects // have been transformed with Unity's gizmos. Also, update the // pivot object in case the selection change event was fired // because objects were deleted from the scene via the Unity // interface. _selectedObjects.RemoveAll(item => item == null); if (_gizmosPivotObject == null) _gizmosPivotObject = numSelectedObjects != 0 ? _selectedObjects[0] : null; gizmos.onTargetObjectsUpdated(_gizmosPivotObject); endActiveTransformSession(); } private void onUndoRedo() { gizmos.onTargetObjectsUpdated(_gizmosPivotObject); foreach (var session in _transformSessions) session.onUndoRedo(); refreshObjectSelectionUI(); } private void onProjected() { endTransformSession(); } } } #endif