// 27 Slicer
// Copyright 2021 Deftly Games
// https://slicer.deftly.games/
using Slicer.Core;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
namespace Slicer
{
///
/// SlicerControllers are used to manage the slicing of s that are on the same GameObject as itself.
///
///
/// In the below image the SlicerController is managing the which will slice any Meshes that are a decedent of this GameObject.
///
/// The property is set to double the x dimension of the mesh while keeping the other dimensions the same as the original mesh.
///
/// The property defines the where the slices should occur. Ranging from 0 (the center of the object), to 1 being the furthest extents of the object.
///
///
///
///
///
///
[REFERENCE MANUAL](xref:manual\components\slicer_controller)
///
[DisallowMultipleComponent]
[ExecuteAlways]
[AddComponentMenu("Slicer/Slicer Controller")]
[HelpURL(SlicerConfiguration.SiteUrl + SlicerConfiguration.ComponentsManualPath + "slicer_controller.html")]
public class SlicerController : MonoBehaviour
{
///
/// Adjusts (Scales) the final dimensions of the sliced item.
///
///
/// Setting the vector to (x: 2, y: 1.5, z: 0.5)
/// will double the x dimension, add an additional 50% of the size to the y dimension and will halve the z dimension.
///
[Tooltip("Adjusts (Scales) the final dimensions of the sliced item.")]
public Vector3 Size = Vector3.one;
///
/// Adjusts where the slices should occur. Ranging from 0 (the center of the object), to 1 being the furthest extents of the object.
///
///
/// In the case of Mesh getting sliced, any vertices that fall within between the center of the Mesh and the provided values will be stretched (scaled) by the value of the Size property.
/// Any vertices that are greater than the provided value will be moved (translated) linearly.
///
[Tooltip("Adjusts where the slices should occur.\nRanging from 0 (the center of the object),\nto 1 being the furthest extents of the object.")]
public Vector3 Slices = Vector3.one;
///
/// Offsets the SlicedBounds, useful if the sliced items needs to be offset without affecting the where it falls in the slice.
///
[HideInInspector]
public Vector3 Offset = Vector3.zero;
private List slicerComponents;
private List previousSlicerComponents;
///
/// A read only collection of SlicerComponents
///
public ReadOnlyCollection SlicerComponents { get { return slicerComponents?.AsReadOnly(); } }
private List sliceModifiers;
private List previousSliceModifiers;
///
/// A read only collection of SliceModifiers
///
public ReadOnlyCollection SliceModifiers { get { return sliceModifiers?.AsReadOnly(); } }
private List slicerIgnores;
///
/// A read only collection of SlicerIgnores
///
public ReadOnlyCollection SlicerIgnores { get { return slicerIgnores?.AsReadOnly(); } }
// Used by non-alloc GetComponents()
private static readonly List childSliceModifiers = new List();
///
/// The bounding box of all of the of all the items that are being sliced.
///
///
/// It is in Local Object Space (of the GameObject the SlicerController Component is attached to).
///
/// The bounds will be null if it has not been calculated yet.
///
public Bounds? CompleteBounds { get; private set; }
///
/// The dimensions that will be used by the slicers to determine if particular features are to be stretched (scaled).
///
///
/// They will be stretched (scaled) if the point is contained by this bounding box and will moved (translated) linearly if they fall outside of the box.
///
/// It is in Local Object Space (of the GameObject the SlicerController Component is attached to).
///
public Bounds SlicedBounds { get; private set; }
[SerializeField, HideInInspector]
private Hash128 previousHash;
///
/// The hash that was calculated during the previous slice
///
public Hash128 PreviousHash { get { return previousHash; } }
private bool forceSliceUpdate = true;
private void Start()
{
UpdateSlice();
if (SlicerConfiguration.FinalizeOnStart)
{
if (Application.isPlaying)
{
// We never want this to run outside of play mode
FinalizeSlicing(true);
}
}
}
private void Update()
{
if (ShouldUpdateSlice())
{
UpdateSlice();
}
}
private bool ShouldUpdateSlice()
{
#if UNITY_EDITOR // If we are not in the editor, then we are always playing
if (!Application.isPlaying)
{
return true;
}
#endif
if (SlicerConfiguration.RefreshSlicesOnUpdate)
{
// Configuration has been set to update on every frame
return true;
}
if (forceSliceUpdate)
{
// We have been requested to force a slice update
return true;
}
return false;
}
///
/// Refreshes the slices managed by this controller on the next frame update.
///
///
/// It is recommended to only call this when there have been changes made during runtime that requires recalculation of the slices.
///
public void RefreshSlice()
{
forceSliceUpdate = true;
}
///
/// Refreshes the slices managed by this controller, this slice happens immediately.
///
/// Generally you will want to use instead.
///
///
/// It is recommended to only call this when there have been changes made during runtime that requires recalculation of the slices.
///
public void RefreshSliceImmediate()
{
UpdateSlice();
}
private void UpdateSlice()
{
forceSliceUpdate = false; // Reset the forced update, as we are doing it now
var currentHash = GatherDetails();
var sizeHash = HashUtility.CalculateHash(Size);
var boundsHash = HashUtility.CalculateHash(Slices);
HashUtility.AppendHash(sizeHash, boundsHash, ref currentHash);
if (previousHash != currentHash || !SlicerConfiguration.SkipUnmodifiedSlices)
{
Slice();
Modify();
previousHash = currentHash;
}
}
private Hash128 GatherDetails()
{
// Rotate the Slicer Component collection,
// We will use these two collections to determine if a slicer component has been removed
if (slicerComponents == null)
{
slicerComponents = new List();
previousSlicerComponents = new List();
}
else
{
var tempSlicerComponents = previousSlicerComponents;
previousSlicerComponents = slicerComponents;
slicerComponents = tempSlicerComponents;
}
if (sliceModifiers == null)
{
sliceModifiers = new List();
previousSliceModifiers = new List();
}
else
{
var tempSliceModifiers = previousSliceModifiers;
previousSliceModifiers = sliceModifiers;
sliceModifiers = tempSliceModifiers;
sliceModifiers.Clear();
}
if (slicerIgnores == null)
{
slicerIgnores = new List();
}
else
{
slicerIgnores.Clear();
}
for (int i = slicerComponents.Count - 1; i >= 0; i--)
{
if (slicerComponents[i] == null)
{
slicerComponents.RemoveAt(i);
}
}
GetComponents(slicerComponents);
// If this slicer component is removed, disable the slicer so it sets all of the items back to their default
foreach (var previousSlicerComponent in previousSlicerComponents)
{
if (!slicerComponents.Contains(previousSlicerComponent))
{
previousSlicerComponent.DisableSlicing();
}
}
{
var lastElement = slicerComponents.Count - 1;
for (int i = lastElement; i >= 0; i--)
{
SlicerComponent slicerComponent = slicerComponents[i];
if (slicerComponent == null)
{
slicerComponents.RemoveAt(i);
lastElement--;
}
slicerComponent.PreGatherDetails();
// Some slicer components should be run after others
// So we want to make sure that are at the back
if (slicerComponent is ColliderSlicerComponent)
{
if (i != lastElement)
{
slicerComponents[i] = slicerComponents[lastElement];
slicerComponents[lastElement] = slicerComponent;
}
lastElement--;
}
}
}
GetChildDetails(transform);
CalculateBounds();
var hash = new Hash128();
foreach (var slicerComponent in slicerComponents)
{
var tempHash = slicerComponent.PostGatherDetails();
HashUtility.AppendHash(tempHash, ref hash);
}
foreach (var sliceModifier in sliceModifiers)
{
var tempHash = sliceModifier.GatherDetails();
HashUtility.AppendHash(tempHash, ref hash);
}
// If this slice modifier is removed, disable the slicer so it sets all of the items back to their default
foreach (var previousSliceModifier in previousSliceModifiers)
{
if (previousSliceModifier != null && !sliceModifiers.Contains(previousSliceModifier))
{
previousSliceModifier.DisableModifier();
}
}
previousSlicerComponents.Clear();
previousSliceModifiers.Clear();
return hash;
}
private void GetChildDetails(Transform currentTransform)
{
foreach (Transform childTransform in currentTransform)
{
// If we encounter another SlicerController:
// The found controller manages any sibling and decedent SlicerComponents
// This controller manages the sibling SliceModifiers of the found controller
// The found controller manages decedent SliceModifiers
// If we encounter a SlicerIgnore:
// This controller manages any sibling SliceModifiers but stop navigating its decedents
childTransform.GetComponents(childSliceModifiers);
if (childSliceModifiers.Count > 0)
{
// Add the slice modifiers
sliceModifiers.AddRange(childSliceModifiers);
childSliceModifiers.Clear();
}
var childSlicer = childTransform.GetComponent();
if (childSlicer != null)
{
// Don't mess with other slicer controllers
continue;
}
var childSlicerIgnore = childTransform.GetComponent();
if (childSlicerIgnore != null)
{
// Don't continue with GameObjects that have a SlicerIgnore
slicerIgnores.Add(childSlicerIgnore);
continue;
}
foreach (var slicerComponent in slicerComponents)
{
slicerComponent.GatherDetails(childTransform, transform);
}
GetChildDetails(childTransform);
}
}
private void CalculateBounds()
{
CompleteBounds = null;
foreach (var slicerComponent in slicerComponents)
{
var componentBounds = slicerComponent.CalculateBounds();
CompleteBounds = BoundsUtility.Encapsulate(CompleteBounds, componentBounds);
}
if (!CompleteBounds.HasValue)
{
// There are no bounds set
// We have no data to work with
return;
}
SlicedBounds = CalculateSlicedBounds(CompleteBounds.Value);
}
private void Slice()
{
if (!CompleteBounds.HasValue)
{
// There are no bounds set
// We have no data to work with
return;
}
foreach (var slicerComponent in slicerComponents)
{
if (!slicerComponent.isActiveAndEnabled || !slicerComponent.SlicingEnabled)
{
continue;
}
slicerComponent.Slice(Size, transform, CompleteBounds.Value, SlicedBounds, Slices);
}
}
private void Modify()
{
if (!CompleteBounds.HasValue)
{
// There are no bounds set
// We have no data to work with
return;
}
foreach (var sliceModifier in sliceModifiers)
{
if (!sliceModifier.isActiveAndEnabled)
{
continue;
}
sliceModifier.Modify(Size, transform, CompleteBounds.Value, SlicedBounds);
}
}
///
/// Disables slicing for all of the SlicerComponents controlled by this SlicerController.
///
public void DisableSlicing()
{
foreach (var slicerComponent in slicerComponents)
{
slicerComponent.DisableSlicing();
}
foreach (var sliceModifier in sliceModifiers)
{
sliceModifier.DisableModifier();
}
}
///
/// Enables slicing for all of the SlicerComponents controlled by this SlicerController.
///
public void EnableSlicing()
{
foreach (var slicerComponent in slicerComponents)
{
slicerComponent.EnableSlicing();
}
foreach (var sliceModifier in sliceModifiers)
{
sliceModifier.EnableModifier();
}
}
///
/// Finalizes slicing for this Slicer Controller.
///
/// Wait until the end of the frame to destroy this controller.
public void FinalizeSlicing(bool deferDestroy = false)
{
foreach (var slicerComponent in slicerComponents)
{
slicerComponent.FinalizeSlicing();
}
slicerComponents.Clear();
foreach (var sliceModifier in sliceModifiers)
{
sliceModifier.FinalizeSlicing();
}
sliceModifiers.Clear();
foreach (var slicerIgnore in slicerIgnores)
{
slicerIgnore.FinalizeSlicing();
}
slicerIgnores.Clear();
if (deferDestroy)
{
this.StartCoroutine(LateSafeDestroy(this));
}
else
{
SafeDestroy(this);
}
}
private Bounds CalculateSlicedBounds(Bounds defaultBounds)
{
var result = defaultBounds;
var extents = result.extents;
extents.Scale(Slices);
result.extents = extents;
result.center += Offset;
return result;
}
///
/// When in is true do a otherwise do .
///
internal static void SafeDestroy(Object toDestroy)
{
#if UNITY_EDITOR
if (Application.isPlaying)
{
Object.Destroy(toDestroy);
}
else
{
Object.DestroyImmediate(toDestroy, false);
}
#else
GameObject.Destroy(toDestroy);
#endif
}
internal static IEnumerator LateSafeDestroy(Object toDestroy)
{
yield return new WaitForEndOfFrame();
SafeDestroy(toDestroy);
}
}
}