修改水

This commit is contained in:
2026-01-01 22:00:33 +08:00
parent 040a222bd6
commit 9ceffccd39
1800 changed files with 103929 additions and 139495 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1db68a6c1808a4d4d9099ac4c9f9812c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abe8bdec24030408b8a545ae03e751f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8b444bc1fb43438aadcfff26a9342e3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Shared.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebc89e55d7e224f58b73430f7f836895
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
{
"reference": "GUID:3eae0364be2026648bf74846acb8a731"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5373b95f6a6234bd9acf598c2856b1c9
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9efee046059b1440a932e76cb96a30d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 645345341492f4e7daaf6a332bf6d944
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89ed50bd0e0cc4e0593b1eb86b22e426
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
{
"reference": "GUID:c579267770062bf448e75eb160330b7f"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b4e50428db6234431b8ab6d9fe9af9ad
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ab8bbb10bd06a476aa3c33abddbdd42d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abd60842ea94549cbbde56358927ae18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Shared.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88af6858f7fbe4851ae792f703289573
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
{
"reference": "GUID:be0903cd8e1546f498710afdc59db5eb"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d1d2e8dc2707d4ea38f4e65cafa7ab83
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2df0bac542844fa0a8e4ad57f8e16a0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Examples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Paint")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Paint.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.ShallowWater")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.ShallowWater.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.ShiftingOrigin")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.ShiftingOrigin.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Splines")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Splines.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Watercraft")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Watercraft.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Whirlpool")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Whirlpool.Editor")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 959b22cf6debd4b5398062eb810543b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 70a73c2fbd8a142b3956c49e19e17c31
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4feed62e65f34662aea25208d1ce0bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Attributes;
using WaveHarmonic.Crest.Editor;
namespace WaveHarmonic.Crest
{
sealed class Embedded : DecoratedProperty
{
internal EmbeddedAssetEditor _Editor;
readonly int _BottomMargin;
public Embedded(int margin = 0)
{
_Editor = new();
_BottomMargin = margin;
}
internal override void OnGUI(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
_Editor.DrawEditorCombo(label, drawer, property, "asset", _BottomMargin);
}
internal override bool NeedsControlRectangle(SerializedProperty property) => false;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c024c6def66044f1cb4783f23a050bec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Attributes;
using WaveHarmonic.Crest.Editor;
namespace WaveHarmonic.Crest
{
sealed class HelpBox : Decorator
{
// Define our own as Unity's won't be available in builds.
public enum MessageType
{
Info,
Warning,
Error,
}
public string _Message;
public MessageType _MessageType;
public Visibility _Visibility;
public enum Visibility
{
Always,
PropertyEnabled,
PropertyDisabled,
}
readonly GUIContent _GuiContent;
public override bool AlwaysVisible => false;
public HelpBox(string message, MessageType messageType = MessageType.Info, Visibility visibility = Visibility.Always)
{
_Message = message;
_MessageType = messageType;
_Visibility = visibility;
_GuiContent = new(message);
}
internal override void Decorate(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
if (_Visibility == Visibility.PropertyEnabled && !GUI.enabled || _Visibility == Visibility.PropertyDisabled && GUI.enabled)
{
return;
}
// Enable rich text in help boxes. Store original so we can revert since this might be a "hack".
var style = GUI.skin.GetStyle("HelpBox");
var styleRichText = style.richText;
style.richText = true;
var height = style.CalcHeight(_GuiContent, EditorGUIUtility.currentViewWidth);
if (height <= EditorGUIUtility.singleLineHeight)
{
// This gets internal layout of the help box right but breaks down if multiline.
height += style.padding.horizontal + style.lineHeight;
}
// Always get a new control rect so we don't have to deal with positions and offsets.
position = EditorGUILayout.GetControlRect(true, height, style);
// + 1 maps our MessageType to Unity's.
EditorGUI.HelpBox(position, _Message, (UnityEditor.MessageType)_MessageType + 1);
// Revert skin since it persists.
style.richText = styleRichText;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49c68d73fb5ce4690a444ab84a0abc8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System;
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Constructs a custom link to Crest's documentation for the help URL button.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum, AllowMultiple = false)]
sealed class HelpURL : HelpURLAttribute
{
public HelpURL(string path = "") : base(GetPageLink(path))
{
// Blank.
}
public static string GetPageLink(string path)
{
return "https://docs.crest.waveharmonic.com/" + path;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f113b211b4a014cf19cf7828018f49d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Attributes;
using WaveHarmonic.Crest.Editor;
namespace WaveHarmonic.Crest
{
sealed class Layer : DecoratedProperty
{
internal override void OnGUI(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
property.intValue = EditorGUI.LayerField(position, label, property.intValue);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a25b1272591fc4fd6a34265f20064746
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System;
namespace WaveHarmonic.Crest
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
sealed class OnChange : Attribute
{
public Type Type { get; }
public bool SkipIfInactive { get; }
/// <summary>
/// Register an instance method as an OnChange handler.
/// </summary>
public OnChange(bool skipIfInactive = true)
{
SkipIfInactive = skipIfInactive;
}
/// <summary>
/// Register a static method as an OnChange handler.
/// </summary>
/// <param name="type">The type to target.</param>
/// <param name="skipIfInactive">Skip this handler if component is inactive.</param>
public OnChange(Type type, bool skipIfInactive = true)
{
Type = type;
SkipIfInactive = skipIfInactive;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75d2e7be9abf446048de686f69cb485c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,213 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Attributes;
using WaveHarmonic.Crest.Editor;
namespace WaveHarmonic.Crest
{
sealed class Predicated : Decorator
{
enum Mode
{
Property,
Member,
Type,
RenderPipeline,
}
readonly Mode _Mode;
readonly bool _Inverted;
readonly bool _Hide;
readonly string _PropertyName;
readonly object _DisableValue;
readonly Type _Type;
readonly MemberInfo _Member;
readonly RenderPipeline _RenderPipeline;
/// <summary>
/// The field with this attribute will be drawn enabled/disabled based on return of method.
/// </summary>
/// <param name="type">The type to call the method on. Must be either a static type or the type the field is defined on.</param>
/// <param name="member">Member name. Method must match signature: bool MethodName(Component component). Can be any visibility and static or instance.</param>
/// <param name="disableValue"></param>
/// <param name="inverted">Flip behaviour - for example disable if a bool field is set to true (instead of false).</param>
/// <param name="hide">Hide this component in the inspector.</param>
public Predicated(Type type, string member, object disableValue, bool inverted = false, bool hide = false)
{
_Mode = Mode.Member;
_Inverted = inverted;
_Hide = hide;
_Type = type;
_DisableValue = disableValue;
_Member = _Type.GetMember(member, Helpers.s_AnyMethod)[0];
}
/// <summary>
/// Enable/Disable field depending on the current type of the component.
/// </summary>
/// <param name="type">If a component of this type is not attached to this GameObject, disable the GUI (or enable if inverted is true).</param>
/// <param name="inverted">Flip behaviour - for example disable if a bool field is set to true (instead of false).</param>
/// <param name="hide">Hide this component in the inspector.</param>
public Predicated(Type type, bool inverted = false, bool hide = false)
{
_Mode = Mode.Type;
_Inverted = inverted;
_Hide = hide;
_Type = type;
}
/// <summary>
/// The field with this attribute will be drawn enabled/disabled based on another field. For example can be used
/// to disable a field if a toggle is false.
/// </summary>
/// <param name="property">The name of the other property whose value dictates whether this field is enabled or not.</param>
/// <param name="inverted">Flip behaviour - for example disable if a bool field is set to true (instead of false).</param>
/// <param name="disableValue">If the field has this value, disable the GUI (or enable if inverted is true).</param>
/// <param name="hide">Hide this component in the inspector.</param>
public Predicated(string property, bool inverted = false, object disableValue = null, bool hide = false)
{
_Mode = Mode.Property;
_Inverted = inverted;
_Hide = hide;
_PropertyName = property;
_DisableValue = disableValue;
}
/// <summary>
/// Field is predicated (enabled/shown) on which render pipeline is active.
/// </summary>
/// <param name="rp">Enable if this render pipeline is active.</param>
/// <param name="inverted">Invert behaviour.</param>
/// <param name="hide">Hide instead of disable.</param>
public Predicated(RenderPipeline rp, bool inverted = false, bool hide = false)
{
_Mode = Mode.RenderPipeline;
_Inverted = inverted;
_Hide = hide;
_RenderPipeline = rp;
}
public override bool AlwaysVisible => true;
public bool GUIEnabled(SerializedProperty property)
{
return _Inverted != property.propertyType switch
{
// Enable GUI if int value of field is not equal to 0, or whatever the disable-value is set to
SerializedPropertyType.Integer => property.intValue != ((int?)_DisableValue ?? 0),
// Enable GUI if disable-value is 0 and field is true, or disable-value is not 0 and field is false
SerializedPropertyType.Boolean => property.boolValue ^ (((int?)_DisableValue ?? 0) != 0),
SerializedPropertyType.Float => property.floatValue != ((float?)_DisableValue ?? 0),
SerializedPropertyType.String => property.stringValue != ((string)_DisableValue ?? ""),
// Must pass nameof enum entry as we are comparing names because index != value.
SerializedPropertyType.Enum => property.enumNames[property.enumValueIndex] != ((string)_DisableValue ?? ""),
SerializedPropertyType.ObjectReference => property.objectReferenceValue != null,
SerializedPropertyType.ManagedReference => property.managedReferenceValue != null,
_ => throw new ArgumentException($"Crest.Predicated: property type on <i>{property.serializedObject.targetObject}</i> not implemented yet: {property.propertyType}."),
};
}
internal override void Decorate(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer)
{
var enabled = true;
if (_Mode == Mode.Property)
{
var propertyPath = _PropertyName;
if (property.depth > 0)
{
// Get the property path so we can find it from the serialized object.
propertyPath = $"{string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1])}.{propertyPath}";
}
// Get the other property to be the predicate for the enabled/disabled state of this property.
// Do not use property.FindPropertyRelative as it does not work with nested properties.
// Try and find the nested property first and then fallback to the root object.
var otherProperty = property.serializedObject.FindProperty(propertyPath) ?? property.serializedObject.FindProperty(_PropertyName);
Debug.AssertFormat(otherProperty != null, "Crest.Predicated: {0} or {1} property on {2} ({3}) could not be found!", propertyPath, _PropertyName, property.serializedObject.targetObject.GetType(), property.name);
if (otherProperty != null)
{
enabled = GUIEnabled(otherProperty);
}
}
else if (_Mode == Mode.Member)
{
// Static is both abstract and sealed: https://stackoverflow.com/a/1175950
object @object = _Type.IsAbstract && _Type.IsSealed ? null : property.serializedObject.targetObject;
// If this is a nested type, grab that type. This may not be suitable in all cases.
if (property.depth > 0)
{
// Get the property path so we can find it from the serialized object.
var propertyPath = string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1]);
var otherProperty = property.serializedObject.FindProperty(propertyPath);
@object = otherProperty.propertyType switch
{
SerializedPropertyType.ManagedReference => otherProperty.managedReferenceValue,
SerializedPropertyType.Generic => otherProperty.boxedValue,
_ => @object,
};
}
if (_Member is PropertyInfo autoProperty)
{
// == operator does not work.
enabled = autoProperty.GetValue(@object).Equals(_DisableValue);
}
else if (_Member is MethodInfo method)
{
enabled = method.Invoke(@object, new object[] { }).Equals(_DisableValue);
}
if (_Inverted) enabled = !enabled;
}
else if (_Mode == Mode.Type)
{
var type = property.serializedObject.targetObject.GetType();
// If this is a nested type, grab that type. This may not be suitable in all cases.
if (property.depth > 0)
{
// Get the property path so we can find it from the serialized object.
var propertyPath = string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1]);
var otherProperty = property.serializedObject.FindProperty(propertyPath);
type = otherProperty.propertyType switch
{
SerializedPropertyType.ManagedReference => otherProperty.managedReferenceValue.GetType(),
SerializedPropertyType.Generic => otherProperty.boxedValue.GetType(),
_ => type,
};
}
var enabledByTypeCheck = _Type.IsAssignableFrom(type);
if (_Inverted) enabledByTypeCheck = !enabledByTypeCheck;
enabled = enabledByTypeCheck && enabled;
}
else if (_Mode == Mode.RenderPipeline)
{
enabled = RenderPipelineHelper.RenderPipeline == _RenderPipeline != _Inverted;
}
// Keep current disabled state.
GUI.enabled &= enabled;
// Keep previous hidden state.
DecoratedDrawer.s_HideInInspector = DecoratedDrawer.s_HideInInspector || (_Hide && !enabled);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a36938141700f4f47b31f7565311ace0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,217 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// Adapted from:
// https://forum.unity.com/threads/drawing-a-field-using-multiple-property-drawers.479377/
// This class draws all the attributes which inherit from DecoratedProperty. This class may need to be
// extended to handle reseting GUI states as we need them.
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using WaveHarmonic.Crest.Attributes;
namespace WaveHarmonic.Crest.Editor
{
[CustomPropertyDrawer(typeof(DecoratedProperty), true)]
sealed class DecoratedDrawer : PropertyDrawer
{
internal static bool s_HideInInspector = false;
public static bool s_IsFoldout = false;
public static bool s_IsFoldoutOpen = false;
public static bool s_TemporaryColor;
public static Color s_PreviousColor;
List<object> _Decorators = null;
List<object> Decorators
{
get
{
// Populate list with decorators.
_Decorators ??= fieldInfo
.GetCustomAttributes(typeof(Decorator), false)
.ToList();
return _Decorators;
}
}
List<Attributes.Validator> _Validators = null;
List<Attributes.Validator> Validators => _Validators ??= fieldInfo
.GetCustomAttributes(typeof(Attributes.Validator), false)
.Cast<Attributes.Validator>()
.ToList();
static List<(MethodInfo, OnChange)> s_OnChangeHandlers;
static List<(MethodInfo, OnChange)> OnChangeHandlers => s_OnChangeHandlers ??= TypeCache
.GetMethodsWithAttribute<OnChange>()
.Select(x => (x, x.GetCustomAttribute<OnChange>()))
.ToList();
[InitializeOnLoadMethod]
static void OnDomainReload()
{
s_OnChangeHandlers = null;
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// Make original control rectangle be invisible because we always create our own. Zero still adds a little
// height which becomes noticeable once multiple properties are hidden. This could be some GUI style
// property but could not find which one.
return -2f;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Store the original GUI state so it can be reset later.
var originalColor = GUI.color;
var originalEnabled = GUI.enabled;
if (s_TemporaryColor) GUI.color = s_PreviousColor;
// Execute all non visual attributes like Predicated. If these change any IMGUI
// properties they might be overriden with EditorGUI.GetPropertyHeight call.
for (var index = 0; index < Decorators.Count; index++)
{
var attribute = (Decorator)Decorators[index];
if (!attribute.AlwaysVisible) continue;
attribute.Decorate(position, property, label, this);
}
if (!s_HideInInspector && (!s_IsFoldout || s_IsFoldoutOpen))
{
// Execute all visual attributes.
for (var index = 0; index < Decorators.Count; index++)
{
var attribute = (Decorator)Decorators[index];
if (attribute.AlwaysVisible) continue;
label = attribute.BuildLabel(label);
attribute.Decorate(position, property, label, this);
}
var a = (DecoratedProperty)attribute;
position = a.NeedsControlRectangle(property)
? EditorGUILayout.GetControlRect(true, EditorGUI.GetPropertyHeight(property, label, true))
: position;
// Call for labels again as EditorGUI.GetPropertyHeight will revert them.
for (var index = 0; index < Decorators.Count; index++)
{
var attribute = (Decorator)Decorators[index];
if (attribute.AlwaysVisible) continue;
label = attribute.BuildLabel(label);
}
var skipChange = fieldInfo.FieldType == typeof(UnityEvent);
if (!skipChange)
{
EditorGUI.BeginChangeCheck();
}
var oldValue = skipChange ? null : property.boxedValue;
a.OnGUI(position, property, label, this);
for (var index = 0; index < Validators.Count; index++)
{
Validators[index].Validate(position, property, label, this, oldValue);
}
// Guard against foldouts triggering change check due to changing isExpanded.
if (!skipChange && EditorGUI.EndChangeCheck() && oldValue != property.boxedValue)
{
// Apply any changes.
property.serializedObject.ApplyModifiedProperties();
OnChange(property, oldValue);
}
for (var index = 0; index < Decorators.Count; index++)
{
var attribute = (Decorator)Decorators[index];
if (attribute.AlwaysVisible) continue;
attribute.DecorateAfter(position, property, label, this);
}
}
if (!s_IsFoldout || s_IsFoldoutOpen)
{
for (var index = 0; index < Decorators.Count; index++)
{
var attribute = (Decorator)Decorators[index];
if (!attribute.AlwaysVisible) continue;
attribute.DecorateAfter(position, property, label, this);
}
}
// Handle resetting the GUI state.
s_HideInInspector = false;
GUI.color = originalColor;
GUI.enabled = originalEnabled;
}
public static void OnChange(SerializedProperty property, object oldValue)
{
var target = property.serializedObject.targetObject;
// This is the type the field is declared on so it will work for nested objects.
var targetType = target.GetType();
var isActive = true;
if (property.serializedObject.targetObject is Internal.EditorBehaviour c && !c.isActiveAndEnabled)
{
isActive = false;
}
// Send event to target.
foreach (var (method, attribute) in OnChangeHandlers)
{
if (attribute.SkipIfInactive && !isActive) continue;
var type = attribute.Type ?? method.DeclaringType;
if (!type.IsAssignableFrom(targetType)) continue;
if (attribute.Type == null)
{
method.Invoke(target, new object[] { property.propertyPath, oldValue });
}
else
{
method.Invoke(null, new object[] { target, property.propertyPath, oldValue, property });
}
}
// Propagate event to nested classes.
for (var i = 0; i < property.depth; i++)
{
var chunks = property.propertyPath.Split(".");
var nestedProperty = property.serializedObject.FindProperty(string.Join(".", chunks[..(i + 1)]));
if (nestedProperty.propertyType != SerializedPropertyType.ManagedReference) continue;
var relativePath = string.Join(".", chunks[(i + 1)..]);
var nestedTarget = nestedProperty.managedReferenceValue;
var nestedTargetType = nestedTarget.GetType();
foreach (var (method, attribute) in OnChangeHandlers)
{
if (attribute.SkipIfInactive && !isActive) continue;
var type = attribute.Type ?? method.DeclaringType;
if (!type.IsAssignableFrom(nestedTargetType)) continue;
if (attribute.Type == null)
{
method.Invoke(nestedTarget, new object[] { relativePath, oldValue });
}
else
{
method.Invoke(null, new object[] { nestedTarget, relativePath, oldValue, property });
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb1d9ebb9335a411586a683ac6f78acd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,246 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// This file is subject to the Unity Companion License:
// https://github.com/Unity-Technologies/com.unity.cinemachine/blob/593fa283bee378322337e5d9f5a7b91331a45799/LICENSE.md
// Lovingly adapted from Cinemachine:
// https://github.com/Unity-Technologies/com.unity.cinemachine/blob/593fa283bee378322337e5d9f5a7b91331a45799/Editor/Utility/EmbeddedAssetHelpers.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.VersionControl;
namespace WaveHarmonic.Crest.Editor
{
/// <summary>
/// Interface for editors that receive an argument
/// </summary>
interface IEmbeddableEditor
{
void SetTypeOfHostComponent(System.Type hostType);
}
/// <summary>
/// Helper for drawing embedded asset editors
/// </summary>
sealed class EmbeddedAssetEditor
{
/// <summary>
/// Create in OnEnable()
/// </summary>
public EmbeddedAssetEditor()
{
_CreateButtonGUIContent = new("Create Asset", "Create a new shared settings asset");
}
/// <summary>
/// Called after the asset editor is created, in case it needs
/// to be customized
/// </summary>
public OnCreateEditorDelegate _OnCreateEditor;
public delegate void OnCreateEditorDelegate(UnityEditor.Editor editor);
/// <summary>
/// Called when the asset being edited was changed by the user.
/// </summary>
public OnChangedDelegate _OnChanged;
public delegate void OnChangedDelegate(System.Type type, Object obj);
/// <summary>
/// Free the resources in OnDisable()
/// </summary>
public void OnDisable()
{
DestroyEditor();
Helpers.Destroy(_DefaultTarget);
}
/// <summary>
/// Customize this after creation if you want
/// </summary>
public GUIContent _CreateButtonGUIContent;
UnityEditor.Editor _Editor = null;
System.Type _Type;
Object _DefaultTarget;
const int k_IndentOffset = 3;
public void DrawEditorCombo(GUIContent label, PropertyDrawer drawer, SerializedProperty property, string extension, int bottomMargin = 0)
{
_Type = drawer.fieldInfo.FieldType;
DrawEditorCombo
(
label,
$"Create {property.displayName} Asset",
$"{property.displayName.Replace(' ', '_')}",
extension,
string.Empty,
false,
property,
bottomMargin
);
}
/// <summary>
/// Call this from OnInspectorGUI. Will draw the asset reference field, and
/// the embedded editor, or a Create Asset button, if no asset is set.
/// </summary>
public void DrawEditorCombo
(
GUIContent label,
string title,
string defaultName,
string extension,
string message,
bool indent,
SerializedProperty property,
int bottomMargin
)
{
UpdateEditor(property);
EditorGUI.BeginChangeCheck();
var rect = AssetField(label, property, title, defaultName, extension, message);
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
UpdateEditor(property);
}
// Display embedded editor.
if (_Editor != null)
{
var foldoutRect = new Rect(rect.x - k_IndentOffset, rect.y, rect.width + k_IndentOffset, EditorGUIUtility.singleLineHeight);
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, GUIContent.none, true);
var canEditAsset = AssetDatabase.IsOpenForEdit(_Editor.target, StatusQueryOptions.UseCachedIfPossible);
// We take the current GUI state into account to support attribute stacking.
var guiEnabled = GUI.enabled;
GUI.enabled = guiEnabled && canEditAsset;
if (property.isExpanded)
{
var level = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUILayout.BeginHorizontal();
// NOTE: Tweaked for current usage but probably will not work everywhere.
if (level > 0) GUILayout.Space(8 * (level + 2));
EditorGUILayout.BeginVertical(GUI.skin.box);
if ((_Editor.target.hideFlags & HideFlags.NotEditable) == 0)
{
EditorGUILayout.HelpBox("This is a shared asset. Changes made here will apply to all users of this asset.", MessageType.Info);
}
EditorGUI.BeginChangeCheck();
_Editor.OnInspectorGUI();
if (EditorGUI.EndChangeCheck() && (_OnChanged != null))
_OnChanged(_Type, property.objectReferenceValue);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
if (bottomMargin > 0)
{
EditorGUILayout.Space(bottomMargin);
}
}
// Enable GUI so the checkout button works.
GUI.enabled = true;
if (_Editor.target != null)
{
if (!canEditAsset && GUILayout.Button("Check out"))
{
var task = Provider.Checkout(AssetDatabase.GetAssetPath(_Editor.target), CheckoutMode.Asset);
task.Wait();
}
}
// Restore stacked GUI enabled state.
GUI.enabled = guiEnabled;
}
}
Rect AssetField
(
GUIContent label,
SerializedProperty property,
string title,
string defaultName,
string extension,
string message
)
{
return EditorHelpers.AssetField
(
_Type,
label,
property,
EditorGUILayout.GetControlRect(true),
title,
defaultName,
extension,
message,
x => ScriptableObject.CreateInstance(_Type)
);
}
public void DestroyEditor()
{
if (_Editor != null)
{
Object.DestroyImmediate(_Editor);
_Editor = null;
}
}
public void UpdateEditor(SerializedProperty property)
{
var target = property.objectReferenceValue;
if (target == null && _DefaultTarget == null)
{
_DefaultTarget = ScriptableObject.CreateInstance(_Type);
_DefaultTarget.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
}
if (target == null)
{
target = _DefaultTarget;
}
// Destroy the editor if target has changed.
if (_Editor != null && _Editor.target != target)
{
DestroyEditor();
}
if (_Editor != null)
{
return;
}
// NOTE: This is triggered twice on asset switch for some reason.
// Create editor if need one.
if (target != null)
{
_Editor = UnityEditor.Editor.CreateEditor(target);
// Pass through argument for editors that receive it
if (property.serializedObject.targetObject != null)
{
(_Editor as IEmbeddableEditor)?.SetTypeOfHostComponent(property.serializedObject.targetObject.GetType());
}
_OnCreateEditor?.Invoke(_Editor);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13b989c062ddf461a9afd71ab6da8921
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,272 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest.Editor
{
/// <summary>
/// Provides general helper functions for the editor.
/// </summary>
static class EditorHelpers
{
internal static ComputeShader s_VisualizeNegativeValuesShader;
internal static ComputeShader VisualizeNegativeValuesShader
{
get
{
if (s_VisualizeNegativeValuesShader == null)
{
s_VisualizeNegativeValuesShader = AssetDatabase.LoadAssetAtPath<ComputeShader>("Packages/com.waveharmonic.crest/Editor/Shaders/VisualizeNegativeValues.compute");
}
return s_VisualizeNegativeValuesShader;
}
}
public static LayerMask LayerMaskField(string label, LayerMask layerMask)
{
// Adapted from: http://answers.unity.com/answers/1387522/view.html
var temporary = EditorGUILayout.MaskField(
label,
UnityEditorInternal.InternalEditorUtility.LayerMaskToConcatenatedLayersMask(layerMask),
UnityEditorInternal.InternalEditorUtility.layers);
return UnityEditorInternal.InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(temporary);
}
/// <summary>Attempts to get the scene view this camera is rendering.</summary>
/// <returns>The scene view or null if not found.</returns>
public static SceneView GetSceneViewFromSceneCamera(Camera camera)
{
foreach (SceneView sceneView in SceneView.sceneViews)
{
if (sceneView.camera == camera)
{
return sceneView;
}
}
return null;
}
/// <summary>Get time passed to animated materials.</summary>
public static float GetShaderTime()
{
// When "Always Refresh" is disabled, Unity passes zero. Also uses realtimeSinceStartup:
// https://github.com/Unity-Technologies/Graphics/blob/5743e39cdf0795cf7cbeb7ba8ffbbcc7ca200709/Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariablesGlobal.cs#L116
return !Application.isPlaying && SceneView.lastActiveSceneView != null &&
!SceneView.lastActiveSceneView.sceneViewState.alwaysRefresh ? 0f : Time.realtimeSinceStartup;
}
public static GameObject GetGameObject(SerializedObject serializedObject)
{
// We will either get the component or the GameObject it is attached to.
return serializedObject.targetObject is GameObject
? serializedObject.targetObject as GameObject
: (serializedObject.targetObject as Component).gameObject;
}
public static Material CreateSerializedMaterial(string shaderPath, string message)
{
var shader = Shader.Find(shaderPath);
Debug.Assert(shader != null, "Crest: Cannot create required material because shader is null");
var material = new Material(shader);
// Record the material and any subsequent changes.
Undo.RegisterCreatedObjectUndo(material, message);
Undo.RegisterCompleteObjectUndo(material, message);
return material;
}
public static Material CreateSerializedMaterial(string shaderPath)
{
return CreateSerializedMaterial(shaderPath, Undo.GetCurrentGroupName());
}
public static Object GetDefaultReference(this SerializedObject self, string property)
{
var path = AssetDatabase.GetAssetPath(MonoScript.FromMonoBehaviour(self.targetObject as MonoBehaviour));
var importer = AssetImporter.GetAtPath(path) as MonoImporter;
return importer.GetDefaultReference(property);
}
public static object GetDefiningBoxedObject(this SerializedProperty property)
{
object target = property.serializedObject.targetObject;
if (property.depth > 0)
{
// Get the property path so we can find it from the serialized object.
var path = string.Join(".", property.propertyPath.Split(".", System.StringSplitOptions.None)[0..^1]);
var other = property.serializedObject.FindProperty(path);
// Boxed value can handle both managed and generic with caveats:
// https://docs.unity3d.com/ScriptReference/SerializedProperty-boxedValue.html
// Not sure if it will be a new or same instance as in the scene.
target = other.boxedValue;
}
return target;
}
internal delegate Object CreateInstance(SerializedProperty property);
internal static Rect AssetField
(
System.Type type,
GUIContent label,
SerializedProperty property,
Rect rect,
string title,
string defaultName,
string extension,
string message,
CreateInstance create
)
{
var hSpace = 5;
var buttonWidth = 45;
var buttonCount = 2;
rect.width -= buttonWidth * buttonCount + hSpace;
EditorGUI.PropertyField(rect, property, label);
var r = new Rect(rect);
r.x += r.width + hSpace;
r.width = buttonWidth;
if (GUI.Button(r, "New", EditorStyles.miniButtonLeft))
{
var path = EditorUtility.SaveFilePanelInProject(title, defaultName, extension, message);
if (!string.IsNullOrEmpty(path))
{
var asset = create(property);
if (asset != null)
{
if (extension == "prefab")
{
PrefabUtility.SaveAsPrefabAsset(asset as GameObject, path);
}
else
{
AssetDatabase.CreateAsset(asset, path);
}
property.objectReferenceValue = AssetDatabase.LoadAssetAtPath<Object>(path);
property.serializedObject.ApplyModifiedProperties();
}
else
{
Debug.LogError($"Crest: Could not create file");
}
}
}
// Only allow cloning if extensions match. Guards against cloning Shader Graph if
// using its embedded material.
var cloneable = property.objectReferenceValue != null;
cloneable = cloneable && Path.GetExtension(AssetDatabase.GetAssetPath(property.objectReferenceValue)) == $".{extension}";
EditorGUI.BeginDisabledGroup(!cloneable);
r.x += r.width;
if (GUI.Button(r, "Clone", EditorStyles.miniButtonRight))
{
var oldPath = AssetDatabase.GetAssetPath(property.objectReferenceValue);
var newPath = oldPath;
if (!newPath.StartsWithNoAlloc("Assets")) newPath = Path.Join("Assets", Path.GetFileName(newPath));
newPath = AssetDatabase.GenerateUniqueAssetPath(newPath);
AssetDatabase.CopyAsset(oldPath, newPath);
property.objectReferenceValue = AssetDatabase.LoadAssetAtPath<Object>(newPath);
}
EditorGUI.EndDisabledGroup();
return rect;
}
internal static void RichTextHelpBox(string message, MessageType type)
{
var styleRichText = GUI.skin.GetStyle("HelpBox").richText;
GUI.skin.GetStyle("HelpBox").richText = true;
EditorGUILayout.HelpBox(message, type);
// Revert skin since it persists.
GUI.skin.GetStyle("HelpBox").richText = styleRichText;
}
// Prettify nameof.
internal static string Pretty(this string text)
{
// Regular expression to split on transitions from lower to upper case and keep acronyms together
return Regex.Replace(text, @"([a-z])([A-Z])|([A-Z])([A-Z][a-z])", "$1$3 $2$4").Replace("_", "");
}
internal static string Italic(this string text)
{
return $"<i>{text}</i>";
}
public static void MarkCurrentStageAsDirty()
{
var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
if (stage != null)
{
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(stage.scene);
}
else
{
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
}
}
static readonly MethodInfo s_ButtonWithDropdownList = typeof(EditorGUI).GetMethod
(
"ButtonWithDropdownList",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
null,
new System.Type[] { typeof(GUIContent), typeof(string[]), typeof(GenericMenu.MenuFunction2), typeof(GUILayoutOption[]) },
null
);
static readonly GUILayoutOption[] s_GUILayoutOptionsZero;
public static bool ButtonWithDropdownList(GUIContent name, string[] names, GenericMenu.MenuFunction2 callback)
{
if (names == null)
{
return GUILayout.Button(name);
}
return (bool)s_ButtonWithDropdownList.Invoke(null, new object[] { name, names, callback, s_GUILayoutOptionsZero });
}
}
static partial class Extensions
{
internal static string GetSubShaderTag([DisallowNull] this Shader shader, ShaderSnippetData snippet, ShaderTagId id)
{
var data = ShaderUtil.GetShaderData(shader);
if (data == null) return null;
var index = (int)snippet.pass.SubshaderIndex;
if (index < 0 || index >= shader.subshaderCount) return null;
var subShader = data.GetSerializedSubshader(index);
if (subShader == null) return null;
var tag = subShader.FindTagValue(id);
if (string.IsNullOrEmpty(tag.name)) return null;
return tag.name;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19e5156bd1ea44af484895930b902df7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,158 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace WaveHarmonic.Crest.Editor
{
partial class Inspector
{
static readonly bool s_GroupMessages = false;
static GUIContent s_JumpButtonContent = null;
static GUIContent s_FixButtonContent = null;
protected virtual void RenderValidationMessages()
{
// Enable rich text in help boxes. Store original so we can revert since this might be a "hack".
var styleRichText = GUI.skin.GetStyle("HelpBox").richText;
GUI.skin.GetStyle("HelpBox").richText = true;
// This is a static list so we need to clear it before use. Not sure if this will ever be a threaded
// operation which would be an issue.
foreach (var messages in ValidatedHelper.s_Messages)
{
messages.Clear();
}
ValidatedHelper.ExecuteValidators(target, ValidatedHelper.HelpBox);
// We only want space before and after the list of help boxes. We don't want space between.
var needsSpaceAbove = true;
var needsSpaceBelow = false;
// We loop through in reverse order so errors appears at the top.
for (var messageTypeIndex = 0; messageTypeIndex < ValidatedHelper.s_Messages.Length; messageTypeIndex++)
{
var messages = ValidatedHelper.s_Messages[messageTypeIndex];
if (messages.Count > 0)
{
if (needsSpaceAbove)
{
// Double space looks good at top.
EditorGUILayout.Space();
// EditorGUILayout.Space();
needsSpaceAbove = false;
}
needsSpaceBelow = true;
// Map Validated.MessageType to HelpBox.MessageType.
var messageType = (MessageType)ValidatedHelper.s_Messages.Length - messageTypeIndex;
if (s_GroupMessages)
{
// We join the messages together to reduce vertical space since HelpBox has padding, borders etc.
var joinedMessage = messages[0]._Message;
// Format as list if we have more than one message.
if (messages.Count > 1) joinedMessage = $"- {joinedMessage}";
for (var messageIndex = 1; messageIndex < messages.Count; messageIndex++)
{
joinedMessage += $"\n- {messages[messageIndex]}";
}
EditorGUILayout.HelpBox(joinedMessage, messageType);
}
else
{
foreach (var message in messages)
{
EditorGUILayout.BeginHorizontal();
var fixDescription = message._FixDescription;
var originalGUIEnabled = GUI.enabled;
if (message._Action != null)
{
fixDescription += " Click the fix/repair button on the right to fix.";
if ((message._Action == ValidatedHelper.FixAddMissingMathPackage || message._Action == ValidatedHelper.FixAddMissingBurstPackage) && PackageManagerHelpers.IsBusy)
{
GUI.enabled = false;
}
}
EditorGUILayout.HelpBox($"{message._Message} {fixDescription}", messageType);
// Jump to object button.
if (message._Object != null)
{
// Selection.activeObject can be message._object.gameObject instead of the component
// itself. We soft cast to MonoBehaviour to get the gameObject for comparison.
// Alternatively, we could always pass gameObject instead of "this".
var casted = message._Object as MonoBehaviour;
if (Selection.activeObject != message._Object && (casted == null || casted.gameObject != Selection.activeObject))
{
s_JumpButtonContent ??= new(EditorGUIUtility.FindTexture("scenepicking_pickable_hover@2x"), "Jump to object to resolve issue");
if (GUILayout.Button(s_JumpButtonContent, GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)))
{
Selection.activeObject = message._Object;
}
}
}
// Fix the issue button.
if (message._Action != null)
{
s_FixButtonContent ??= new(EditorGUIUtility.FindTexture("SceneViewTools"));
if (message._FixDescription != null)
{
var sanitisedFixDescr = Regex.Replace(message._FixDescription, @"<[^<>]*>", "'");
s_FixButtonContent.tooltip = $"Apply fix: {sanitisedFixDescr}";
}
else
{
s_FixButtonContent.tooltip = "Fix issue";
}
if (GUILayout.Button(s_FixButtonContent, GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)))
{
// Run fix function
var serializedObject = new SerializedObject(message._Object);
// Property is optional.
var property = message._PropertyPath != null ? serializedObject?.FindProperty(message._PropertyPath) : null;
var oldValue = property?.boxedValue;
message._Action.Invoke(serializedObject, property);
if (serializedObject.ApplyModifiedProperties())
{
// SerializedObject does this for us, but gives the history item a nicer label.
Undo.RecordObject(message._Object, s_FixButtonContent.tooltip);
DecoratedDrawer.OnChange(property, oldValue);
}
}
}
GUI.enabled = originalGUIEnabled;
EditorGUILayout.EndHorizontal();
}
}
}
}
if (needsSpaceBelow)
{
// EditorGUILayout.Space();
}
// Revert skin since it persists.
GUI.skin.GetStyle("HelpBox").richText = styleRichText;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2125841df4154bd89cf5af6847a0d40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,218 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest.Editor
{
/// <summary>
/// Base editor. Needed as custom drawers require a custom editor to work.
/// </summary>
[CustomEditor(typeof(EditorBehaviour), editorForChildClasses: true)]
partial class Inspector : UnityEditor.Editor
{
public const int k_FieldGroupOrder = 1000;
const string k_NoPrefabModeSupportWarning = "Prefab mode is not supported. Changes made in prefab mode will not be reflected in the scene view. Save this prefab to see changes.";
internal static Inspector Current { get; private set; }
readonly Dictionary<Material, MaterialEditor> _MaterialEditors = new();
public override bool RequiresConstantRepaint() => TexturePreview.AnyActivePreviews;
// Set this from a decorator to enable the material editor.
internal List<Material> _Materials = new();
readonly Utility.SortedList<int, SerializedProperty> _Properties = new(Helpers.DuplicateComparison);
public override void OnInspectorGUI()
{
// Reset foldout values.
DecoratedDrawer.s_IsFoldout = false;
DecoratedDrawer.s_IsFoldoutOpen = false;
// Make sure we adhere to flags.
var enabled = GUI.enabled;
GUI.enabled = (target.hideFlags & HideFlags.NotEditable) == 0;
RenderBeforeInspectorGUI();
RenderInspectorGUI();
RenderValidationMessages();
EditorGUI.BeginDisabledGroup(target is Behaviour component && !component.isActiveAndEnabled);
RenderBottomButtons();
EditorGUI.EndDisabledGroup();
RenderAfterInspectorGUI();
GUI.enabled = enabled;
}
protected virtual void OnDisable()
{
foreach (var (_, editor) in _MaterialEditors)
{
Helpers.Destroy(editor);
}
}
protected void RenderBeforeInspectorGUI()
{
if (this.target is EditorBehaviour target && target._IsPrefabStageInstance)
{
EditorGUILayout.Space();
EditorGUILayout.HelpBox(k_NoPrefabModeSupportWarning, MessageType.Warning);
EditorGUILayout.Space();
}
}
protected virtual void RenderInspectorGUI()
{
var previous = Current;
Current = this;
_Properties.Clear();
serializedObject.Update();
using var iterator = serializedObject.GetIterator();
if (iterator.NextVisible(true))
{
var index = 0;
var group = 0;
Type type = null;
do
{
var property = serializedObject.FindProperty(iterator.name);
if (iterator.name == "m_Script")
{
#if CREST_DEBUG
_Properties.Add(index++, property);
#endif
continue;
}
var field = property.GetFieldInfo(out _);
// If field is on a new type (inheritance) then reset the group to prevent group leakage.
if (field.DeclaringType != type)
{
group = 0;
type = field.DeclaringType;
}
// Null checking but there should always be one DecoratedProperty.
var order = field.GetCustomAttribute<Attributes.DecoratedProperty>()?.order ?? 0;
group = field.GetCustomAttribute<Group>()?.order * k_FieldGroupOrder ?? group;
_Properties.Add(order + group + index++, property);
}
while (iterator.NextVisible(false));
}
foreach (var (_, property) in _Properties)
{
#if CREST_DEBUG
using (new EditorGUI.DisabledGroupScope(property.name == "m_Script"))
#endif
{
// Only support top level ordering for now.
EditorGUILayout.PropertyField(property, includeChildren: true);
}
}
// Need to call just in case there is no decorated property.
serializedObject.ApplyModifiedProperties();
// Restore previous in case this is a nested editor.
Current = previous;
// Fixes indented validation etc.
EditorGUI.indentLevel = 0;
}
protected virtual void RenderBottomButtons()
{
}
protected virtual void RenderAfterInspectorGUI()
{
foreach (var material in _Materials)
{
if (material == null) continue;
DrawMaterialEditor(material);
}
_Materials.Clear();
}
// Adapted from: http://answers.unity.com/answers/975894/view.html
void DrawMaterialEditor(Material material)
{
MaterialEditor editor;
if (!_MaterialEditors.ContainsKey(material))
{
editor = (MaterialEditor)CreateEditor(material);
_MaterialEditors[material] = editor;
}
else
{
editor = _MaterialEditors[material];
}
// Check material again as sometimes null.
if (editor != null && material != null)
{
EditorGUILayout.Space();
// Draw the material's foldout and the material shader field. Required to call OnInspectorGUI.
editor.DrawHeader();
var path = AssetDatabase.GetAssetPath(material);
// We need to prevent the user from editing Unity's default materials.
// Check Editor.IsEnabled in Editor.cs for further filtering.
var isEditable = (material.hideFlags & HideFlags.NotEditable) == 0;
#if !CREST_DEBUG
// Do not allow editing of our assets.
isEditable &= !path.StartsWithNoAlloc("Packages/com.waveharmonic");
#endif
using (new EditorGUI.DisabledGroupScope(!isEditable))
{
// Draw the material properties. Works only if the foldout of DrawHeader is open.
editor.OnInspectorGUI();
}
}
}
}
// Adapted from:
// https://gist.github.com/thebeardphantom/1ad9aee0ef8de6271fff39f1a6a3d66d
static partial class Extensions
{
static readonly MethodInfo s_GetFieldInfoFromProperty;
static Extensions()
{
var utility = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.ScriptAttributeUtility");
Debug.Assert(utility != null, "Crest: ScriptAttributeUtility not found!");
s_GetFieldInfoFromProperty = utility.GetMethod("GetFieldInfoFromProperty", BindingFlags.NonPublic | BindingFlags.Static);
Debug.Assert(s_GetFieldInfoFromProperty != null, "Crest: GetFieldInfoFromProperty not found!");
}
public static FieldInfo GetFieldInfo(this SerializedProperty property, out Type type)
{
type = null;
return (FieldInfo)s_GetFieldInfoFromProperty.Invoke(null, new object[] { property, type });
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db8aed30f95c447c3b9491865a600372
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEditor;
using UnityEngine;
namespace WaveHarmonic.Crest.Editor
{
/// <summary>
/// Adds a deprecated message to shaders.
///
/// USAGE
/// Add to bottom of Shader block:
/// CustomEditor "Crest.ObsoleteShaderGUI"
/// Optionally add to Properties block:
/// [HideInInspector] _ObsoleteMessage("The additional message.", Float) = 0
/// </summary>
sealed class ObsoleteShaderGUI : ShaderGUI
{
public override void OnGUI(MaterialEditor editor, MaterialProperty[] properties)
{
// Enable rich text in help boxes. Store original so we can revert since this might be a "hack".
var styleRichText = GUI.skin.GetStyle("HelpBox").richText;
GUI.skin.GetStyle("HelpBox").richText = true;
var message = "";
{
var property = FindProperty("_Message", properties, propertyIsMandatory: false);
if (property != null)
{
message += property.displayName;
}
}
{
var property = FindProperty("_ObsoleteMessage", properties, propertyIsMandatory: false);
if (property != null)
{
message += "This shader is deprecated and will be removed in a future version. " + property.displayName;
}
}
EditorGUILayout.HelpBox(message, MessageType.Warning);
EditorGUILayout.Space(3f);
// Revert skin since it persists.
GUI.skin.GetStyle("HelpBox").richText = styleRichText;
// Render the default GUI.
base.OnGUI(editor, properties);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a7b7bf49879b47f5b5e4fe887760848
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// Helpers for using the Unity Package Manager.
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
namespace WaveHarmonic.Crest.Editor
{
static class PackageManagerHelpers
{
static AddRequest s_Request;
public static bool IsBusy => s_Request?.IsCompleted == false;
public static void AddMissingPackage(string packageName)
{
s_Request = Client.Add(packageName);
EditorApplication.update += AddMissingPackageProgress;
}
static void AddMissingPackageProgress()
{
if (s_Request.IsCompleted)
{
if (s_Request.Status == StatusCode.Success)
{
Debug.Log("Installed: " + s_Request.Result.packageId);
}
else if (s_Request.Status >= StatusCode.Failure)
{
Debug.Log(s_Request.Error.message);
}
EditorApplication.update -= AddMissingPackageProgress;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5138ba66b6604da09f58ac99ee035f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// Exposes a customized version of Unity's shader generator task to only generate
// our source files.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest.Editor
{
static class ShaderGeneratorUtility
{
static MethodInfo s_GenerateAsync;
static MethodInfo GenerateAsync => s_GenerateAsync ??= typeof(CSharpToHLSL)
.GetMethod("GenerateAsync", BindingFlags.Static | BindingFlags.NonPublic);
static async Task InvokeAsync(this MethodInfo methodInfo, object obj, params object[] parameters)
{
dynamic awaitable = methodInfo.Invoke(obj, parameters);
await awaitable;
}
// Adapted from:
// https://github.com/Unity-Technologies/Graphics/blob/96ba978a240e96adcb2abceb21e90b24caa484a3/Packages/com.unity.render-pipelines.core/Editor/ShaderGenerator/CSharpToHLSL.cs#L18L53
internal static async Task GenerateAll()
{
Dictionary<string, List<ShaderTypeGenerator>> sourceGenerators = null;
try
{
// Store per source file path the generator definitions
sourceGenerators = DictionaryPool<string, List<ShaderTypeGenerator>>.Get();
// Extract all types with the GenerateHLSL tag
foreach (var type in TypeCache.GetTypesWithAttribute<GenerateHLSL>())
{
// Only generate our sources as Unity's will trigger a package refresh.
if (!type.FullName.StartsWith("WaveHarmonic.Crest")) continue;
var attr = type.GetCustomAttributes(typeof(GenerateHLSL), false).First() as GenerateHLSL;
if (!sourceGenerators.TryGetValue(attr.sourcePath, out var generators))
{
generators = ListPool<ShaderTypeGenerator>.Get();
sourceGenerators.Add(attr.sourcePath, generators);
}
generators.Add(new(type, attr));
}
// We need to force the culture to invariant, otherwise generated code can replace characters.
// For example, Turkish will replace "I" with "İ". This is a bug with GenerateAsync.
var culture = System.Globalization.CultureInfo.CurrentCulture;
System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
// Generate all files
await Task.WhenAll(sourceGenerators.Select(async it => await GenerateAsync
.InvokeAsync(null, new object[] { $"{it.Key}.hlsl", $"{Path.ChangeExtension(it.Key, "custom")}.hlsl", it.Value })));
System.Globalization.CultureInfo.CurrentCulture = culture;
}
finally
{
// Make sure we always release pooled resources
if (sourceGenerators != null)
{
foreach (var pair in sourceGenerators)
ListPool<ShaderTypeGenerator>.Release(pair.Value);
DictionaryPool<string, List<ShaderTypeGenerator>>.Release(sourceGenerators);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bf974bd66ba04a1aa65908c4308b1b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,199 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#pragma warning disable
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEditor;
using UnityEditor.ShaderGraph;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Editor")]
namespace WaveHarmonic.Crest.Editor
{
internal static class ShaderGraphPropertyDrawers
{
static Dictionary<GraphInputData, bool> s_CompoundPropertyFoldoutStates = new();
static Dictionary<string, string> s_Tooltips;
static readonly GUIContent s_Label = new();
public static void DrawShaderGraphGUI(MaterialEditor materialEditor, IEnumerable<MaterialProperty> properties, Dictionary<string, string> tooltips)
{
s_Tooltips = tooltips;
Material m = materialEditor.target as Material;
Shader s = m.shader;
string path = AssetDatabase.GetAssetPath(s);
ShaderGraphMetadata metadata = null;
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(path))
{
if (obj is ShaderGraphMetadata meta)
{
metadata = meta;
break;
}
}
if (metadata != null)
DrawShaderGraphGUI(materialEditor, properties, metadata.categoryDatas);
else
PropertiesDefaultGUI(materialEditor, properties);
}
static void PropertiesDefaultGUI(MaterialEditor materialEditor, IEnumerable<MaterialProperty> properties)
{
foreach (var property in properties)
{
if ((property.flags & (MaterialProperty.PropFlags.HideInInspector | MaterialProperty.PropFlags.PerRendererData)) != 0)
continue;
float h = materialEditor.GetPropertyHeight(property, property.displayName);
Rect r = EditorGUILayout.GetControlRect(true, h, EditorStyles.layerMaskField);
s_Label.text = property.displayName;
s_Label.tooltip = s_Tooltips.GetValueOrDefault(property.name, null);
materialEditor.ShaderProperty(r, property, s_Label);
}
}
static Rect GetRect(MaterialProperty prop)
{
return EditorGUILayout.GetControlRect(true, MaterialEditor.GetDefaultPropertyHeight(prop));
}
static MaterialProperty FindProperty(string propertyName, IEnumerable<MaterialProperty> properties)
{
foreach (var prop in properties)
{
if (prop.name == propertyName)
{
return prop;
}
}
return null;
}
public static void DrawShaderGraphGUI(MaterialEditor materialEditor, IEnumerable<MaterialProperty> properties, IEnumerable<MinimalCategoryData> categoryDatas)
{
foreach (MinimalCategoryData mcd in categoryDatas)
{
DrawCategory(materialEditor, properties, mcd);
}
}
static void DrawCategory(MaterialEditor materialEditor, IEnumerable<MaterialProperty> properties, MinimalCategoryData minimalCategoryData)
{
if (minimalCategoryData.categoryName.Length > 0)
{
minimalCategoryData.expanded = EditorGUILayout.BeginFoldoutHeaderGroup(minimalCategoryData.expanded, minimalCategoryData.categoryName);
}
else
{
// force draw if no category name to do foldout on
minimalCategoryData.expanded = true;
}
if (minimalCategoryData.expanded)
{
foreach (var propData in minimalCategoryData.propertyDatas)
{
if (propData.isCompoundProperty == false)
{
MaterialProperty prop = FindProperty(propData.referenceName, properties);
if (prop == null) continue;
DrawMaterialProperty(materialEditor, prop, propData.propertyType, propData.isKeyword, propData.keywordType);
}
else
{
DrawCompoundProperty(materialEditor, properties, propData);
}
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
static void DrawCompoundProperty(MaterialEditor materialEditor, IEnumerable<MaterialProperty> properties, GraphInputData compoundPropertyData)
{
EditorGUI.indentLevel++;
bool foldoutState = true;
var exists = s_CompoundPropertyFoldoutStates.ContainsKey(compoundPropertyData);
if (!exists)
s_CompoundPropertyFoldoutStates.Add(compoundPropertyData, true);
else
foldoutState = s_CompoundPropertyFoldoutStates[compoundPropertyData];
foldoutState = EditorGUILayout.Foldout(foldoutState, compoundPropertyData.referenceName);
if (foldoutState)
{
EditorGUI.indentLevel++;
foreach (var subProperty in compoundPropertyData.subProperties)
{
var property = FindProperty(subProperty.referenceName, properties);
if (property == null) continue;
DrawMaterialProperty(materialEditor, property, subProperty.propertyType);
}
EditorGUI.indentLevel--;
}
if (exists)
s_CompoundPropertyFoldoutStates[compoundPropertyData] = foldoutState;
EditorGUI.indentLevel--;
}
static void DrawMaterialProperty(MaterialEditor materialEditor, MaterialProperty property, PropertyType propertyType, bool isKeyword = false, KeywordType keywordType = KeywordType.Boolean)
{
if (!isKeyword)
{
switch (propertyType)
{
case PropertyType.SamplerState:
case PropertyType.Matrix4:
case PropertyType.Matrix3:
case PropertyType.Matrix2:
case PropertyType.VirtualTexture:
case PropertyType.Gradient:
return;
case PropertyType.Vector3:
DrawVector3Property(materialEditor, property);
return;
case PropertyType.Vector2:
DrawVector2Property(materialEditor, property);
return;
}
}
s_Label.text = property.displayName;
s_Label.tooltip = s_Tooltips.GetValueOrDefault(property.name, null);
materialEditor.ShaderProperty(property, s_Label);
}
static void DrawVector2Property(MaterialEditor materialEditor, MaterialProperty property)
{
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = property.hasMixedValue;
Vector2 newValue = EditorGUI.Vector2Field(GetRect(property), property.displayName, new Vector2(property.vectorValue.x, property.vectorValue.y));
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
property.vectorValue = newValue;
}
}
static void DrawVector3Property(MaterialEditor materialEditor, MaterialProperty property)
{
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = property.hasMixedValue;
Vector3 newValue = EditorGUI.Vector3Field(GetRect(property), property.displayName, new Vector3(property.vectorValue.x, property.vectorValue.y, property.vectorValue.z));
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
property.vectorValue = newValue;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ee5c95ce93114c1c909f5b1346d8bb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest.Editor
{
abstract class TexturePreview : ObjectPreview
{
public static bool AnyActivePreviews { get; private set; }
UnityEditor.Editor _Editor;
RenderTexture _RenderTexture;
RenderTextureDescriptor _OriginalDescriptor = new();
Texture _Current;
protected int _Slice;
bool _First = true;
protected abstract Texture OriginalTexture { get; }
protected virtual Texture ModifiedTexture { get; }
Texture Texture => ModifiedTexture != null ? ModifiedTexture : OriginalTexture;
protected virtual bool Flipped => false;
// Preview complains if not a certain set of formats.
bool Incompatible => !(GraphicsFormatUtility.IsIEEE754Format(Texture.graphicsFormat)
|| GraphicsFormatUtility.IsNormFormat(Texture.graphicsFormat));
public TexturePreview() { }
public override bool HasPreviewGUI()
{
AnyActivePreviews = false;
return OriginalTexture;
}
public override void Cleanup()
{
base.Cleanup();
Object.DestroyImmediate(_Editor);
if (_RenderTexture != null) _RenderTexture.Release();
Object.DestroyImmediate(_RenderTexture);
}
public override void OnPreviewSettings()
{
if (_First && Event.current.type == EventType.Repaint && !Application.isPlaying)
{
// Solves on enter edit mode:
// ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
_First = false;
return;
}
if (_Editor != null) _Editor.OnPreviewSettings();
}
public override void OnPreviewGUI(Rect rect, GUIStyle background)
{
AnyActivePreviews = true;
// This check is in original.
if (Event.current.type == EventType.Repaint)
{
background.Draw(rect, false, false, false, false);
}
var descriptor = Texture.GetDescriptor();
if (Helpers.RenderTextureNeedsUpdating(descriptor, _OriginalDescriptor))
{
_OriginalDescriptor = descriptor;
if (Incompatible)
{
descriptor.graphicsFormat = GraphicsFormat.R32G32B32A32_SFloat;
}
Helpers.SafeCreateRenderTexture(ref _RenderTexture, descriptor);
_RenderTexture.Create();
Object.DestroyImmediate(_Editor);
_Editor = UnityEditor.Editor.CreateEditor(_RenderTexture);
// Reset for incompatible copy.
descriptor = _OriginalDescriptor;
}
// Name may change without texture changing (see SWS).
_RenderTexture.name = Texture.name + " (Preview)";
if (Incompatible)
{
var temporary = RenderTexture.GetTemporary(descriptor);
Graphics.CopyTexture(Texture, temporary);
Helpers.Blit(temporary, _RenderTexture);
RenderTexture.ReleaseTemporary(temporary);
}
else
{
Graphics.CopyTexture(Texture, _RenderTexture);
}
_Editor.DrawPreview(rect);
}
#if CREST_DEBUG
public override void OnInteractivePreviewGUI(Rect rect, GUIStyle background)
{
OnPreviewGUI(rect, background);
// Show pixel value in preview.
_Slice = Development.Utility.GetPreviewSlice(_Editor, Texture);
var color = Development.Utility.InspectPixel(rect, OriginalTexture, Flipped, _Slice);
var text = Development.Utility.GetInspectPixelString(color, OriginalTexture);
EditorGUI.DropShadowLabel(new Rect(rect.x, rect.y, rect.width, 40), text);
}
#endif
void Allocate(Texture texture)
{
// LOD with buffered data like foam will recreate every frame freezing controls.
if (_Editor != null && _Current == Texture) return;
_Current = texture;
Object.DestroyImmediate(_Editor);
_Editor = UnityEditor.Editor.CreateEditor(texture);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c817389e2ce240f69487a0e78fc7091
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,302 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// How to use:
// Use or inherit from Crest.Editor.Inspector to support validation messages.
// Then create a static method with Validator attribute.
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest.Editor
{
using FixValidation = System.Action<SerializedObject, SerializedProperty>;
[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
sealed class Validator : System.Attribute
{
public readonly System.Type _Type;
public Validator(System.Type type)
{
_Type = type;
}
}
// Holds the shared list for messages
static class ValidatedHelper
{
public enum MessageType
{
Error,
Warning,
Info,
}
public struct HelpBoxMessage
{
public string _Message;
public string _FixDescription;
public Object _Object;
public FixValidation _Action;
public string _PropertyPath;
}
// This is a shared resource. It will be cleared before use. It is only used by the HelpBox delegate since we
// want to group them by severity (MessageType). Make sure length matches MessageType length.
public static readonly List<HelpBoxMessage>[] s_Messages = new List<HelpBoxMessage>[]
{
new(),
new(),
new(),
};
public delegate void ShowMessage(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null);
public static void DebugLog(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null)
{
// Never log info validation to console.
if (type == MessageType.Info)
{
return;
}
message = $"Crest Validation: {message} {fixDescription} Click this message to highlight the problem object.";
switch (type)
{
case MessageType.Error: Debug.LogError(message, @object); break;
case MessageType.Warning: Debug.LogWarning(message, @object); break;
default: Debug.Log(message, @object); break;
}
}
public static void HelpBox(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null)
{
s_Messages[(int)type].Add(new() { _Message = message, _FixDescription = fixDescription, _Object = @object, _Action = action, _PropertyPath = property });
}
public static void Suppressed(string message, string fixDescription, MessageType type, Object @object = null, FixValidation action = null, string property = null)
{
}
public static T FixAttachComponent<T>(SerializedObject componentOrGameObject)
where T : Component
{
return Undo.AddComponent<T>(EditorHelpers.GetGameObject(componentOrGameObject));
}
internal static void FixSetMaterialOptionEnabled(SerializedObject material, string keyword, string floatParam, bool enabled)
{
var mat = material.targetObject as Material;
Undo.RecordObject(mat, $"Enable keyword {keyword}");
mat.SetBoolean(Shader.PropertyToID(floatParam), enabled);
if (ArrayUtility.Contains(mat.shader.keywordSpace.keywordNames, keyword))
{
mat.SetKeyword(keyword, enabled);
}
}
internal static void FixSetMaterialIntProperty(SerializedObject material, string label, string intParam, int value)
{
var mat = material.targetObject as Material;
Undo.RecordObject(mat, $"change {label}");
mat.SetInteger(intParam, value);
}
public static void FixAddMissingMathPackage(SerializedObject _0, SerializedProperty _1)
{
PackageManagerHelpers.AddMissingPackage("com.unity.mathematics");
}
public static void FixAddMissingBurstPackage(SerializedObject _0, SerializedProperty _1)
{
PackageManagerHelpers.AddMissingPackage("com.unity.burst");
}
public static bool ValidateNoScale(Object @object, Transform transform, ShowMessage showMessage)
{
if (transform.lossyScale != Vector3.one)
{
showMessage
(
$"There must be no scale on the <i>{@object.GetType().Name}</i> Transform or any of its parents." +
$"The current scale is <i>{transform.lossyScale}</i>.",
"Reset the scale on this Transform and all parents to one.",
MessageType.Error, @object
);
return false;
}
return true;
}
public static bool ValidateNoRotation(Object @object, Transform transform, ShowMessage showMessage)
{
if (transform.eulerAngles.magnitude > 0.0001f)
{
showMessage
(
$"There must be no rotation on the <i>{@object.GetType().Name}</i> Transform or any of its parents." +
$"The current rotation is <i>{transform.eulerAngles}.</i>",
"Reset the rotation on this Transform and all parents to zero.",
MessageType.Error, @object
);
return false;
}
return true;
}
public static bool ValidateRenderer<T>
(
Component component,
Renderer renderer,
ShowMessage showMessage,
bool checkShaderPasses,
string shaderPrefix = null
)
where T : Renderer
{
if (renderer == null)
{
var type = typeof(T);
var name = type.Name;
// Give users a hint as to what "Renderer" really means.
if (type == typeof(Renderer))
{
name += " (Mesh, Trail etc)";
}
showMessage
(
$"A <i>{name}</i> component is required but none is assigned.",
"Provide a renderer.",
MessageType.Error, component
);
return false;
}
var materials = renderer.sharedMaterials;
for (var i = 0; i < materials.Length; i++)
{
// Empty material slots is a user error. Unity complains about it so we should too.
if (materials[i] == null)
{
showMessage
(
$"<i>{renderer.GetType().Name}</i> used by this input (<i>{component.GetType().Name}</i>) has empty material slots.",
"Remove these slots or fill them with a material.",
MessageType.Error, renderer
);
}
}
if (renderer is MeshRenderer)
{
renderer.gameObject.TryGetComponent<MeshFilter>(out var mf);
if (mf == null)
{
showMessage
(
$"A <i>MeshRenderer</i> component is being used by this input but no <i>MeshFilter</i> component was found so there may not be any valid geometry to render.",
"Attach a <i>MeshFilter</i> component.",
MessageType.Error, renderer.gameObject,
(_, _) => Undo.AddComponent<MeshFilter>(renderer.gameObject)
);
return false;
}
else if (mf.sharedMesh == null)
{
showMessage
(
$"A <i>MeshRenderer</i> component is being used by this input but no mesh is assigned to the <i>MeshFilter</i> component.",
"Assign the geometry to be rendered to the <i>MeshFilter</i> component.",
MessageType.Error, renderer.gameObject
);
return false;
}
}
if (!ValidateMaterial(renderer.gameObject, showMessage, renderer.sharedMaterial, shaderPrefix, checkShaderPasses))
{
return false;
}
return true;
}
public static bool ValidateMaterial(GameObject gameObject, ShowMessage showMessage, Material material, string shaderPrefix, bool checkShaderPasses)
{
if (shaderPrefix == null && material == null)
{
showMessage
(
$"<i>Mesh Renderer</i> requires a material.",
"Assign a material.",
MessageType.Error, gameObject
);
return false;
}
if (!material || material.shader && (!material.shader.name.StartsWithNoAlloc(shaderPrefix) && !material.shader.name.StartsWithNoAlloc($"Hidden/{shaderPrefix}") && !material.shader.name.Contains("/All/")))
{
showMessage
(
$"Shader assigned to water input expected to be of type <i>{shaderPrefix}</i>.",
"Assign a material that uses a shader of this type.",
MessageType.Error, gameObject
);
return false;
}
if (checkShaderPasses && material.passCount > 1)
{
showMessage
(
$"The shader <i>{material.shader.name}</i> for material <i>{material.name}</i> has multiple passes which might not work as expected as only the first pass is executed. " +
"This can be ignored in most cases, like Shader Graph, as only one pass is often required.",
"To have all passes execute then set <i>Shader Pass Index</i> to <i>-1</i>.",
MessageType.Info, gameObject
);
}
return true;
}
public static bool ExecuteValidators(object target, ShowMessage messenger)
{
var isValid = true;
var type = target.GetType();
var validators = TypeCache.GetMethodsWithAttribute<Validator>();
foreach (var validator in validators)
{
var attribute = validator.GetCustomAttribute<Validator>();
if (attribute._Type.IsAssignableFrom(type))
{
isValid = (bool)validator.Invoke(null, new object[] { target, messenger }) && isValid;
}
}
return isValid;
}
public static bool ExecuteValidators(Object target)
{
return ExecuteValidators(target, DebugLog);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8e9a7f4ffb1bb41999902cd4310bf2d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
{
"name": "WaveHarmonic.Crest.Shared.Editor",
"rootNamespace": "",
"references": [
"GUID:056ff2a5b2f124d468c6655552acdca5",
"GUID:3eae0364be2026648bf74846acb8a731",
"GUID:df380645f10b7bc4b97d4f5eb6303d95",
"GUID:be0903cd8e1546f498710afdc59db5eb"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [
"UNITY_2022_3_OR_NEWER"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1ab2a6c2a51cd4b43867788dbaee1a55
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: