diff --git a/Source/ConformalDecals/ConformalDecals.csproj b/Source/ConformalDecals/ConformalDecals.csproj index d778ae1..6c9eaf5 100644 --- a/Source/ConformalDecals/ConformalDecals.csproj +++ b/Source/ConformalDecals/ConformalDecals.csproj @@ -1,6 +1,6 @@ - + Debug AnyCPU @@ -17,7 +17,7 @@ true full false - bin\Debug\ + bin/Debug/ DEBUG;TRACE prompt 4 @@ -25,11 +25,11 @@ pdbonly true - bin\Release\ + bin/Release/ TRACE prompt 4 - bin\Release\ConformalDecals.xml + bin/Release/ConformalDecals.xml CS1591,CS0649 @@ -41,65 +41,69 @@ - dlls\UnityEngine.dll + dlls/UnityEngine.dll - dlls\UnityEngine.AssetBundleModule.dll + dlls/UnityEngine.AssetBundleModule.dll - dlls\UnityEngine.CoreModule.dll + dlls/UnityEngine.CoreModule.dll - dlls\UnityEngine.PhysicsModule.dll + dlls/UnityEngine.PhysicsModule.dll - dlls\UnityEngine.TextCoreModule.dll + dlls/UnityEngine.TextCoreModule.dll - dlls\UnityEngine.TextRenderingModule.dll + dlls/UnityEngine.TextRenderingModule.dll - dlls\UnityEngine.UI.dll + dlls/UnityEngine.UI.dll - dlls\UnityEngine.UIElementsModule.dll + dlls/UnityEngine.UIElementsModule.dll - dlls\UnityEngine.UIModule.dll + dlls/UnityEngine.UIModule.dll - - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - + sh -e -c "cp -v '$(TargetPath)' '$(SolutionDir)/../GameData/ConformalDecals/Plugins'" diff --git a/Source/ConformalDecals/UI/BoxSlider.cs b/Source/ConformalDecals/UI/BoxSlider.cs index cf637ef..607aff8 100644 --- a/Source/ConformalDecals/UI/BoxSlider.cs +++ b/Source/ConformalDecals/UI/BoxSlider.cs @@ -26,7 +26,6 @@ namespace ConformalDecals.UI { } } - public Vector2 Value { get => _value; set { diff --git a/Source/ConformalDecals/UI/ColorBoxSlider.cs b/Source/ConformalDecals/UI/ColorBoxSlider.cs new file mode 100644 index 0000000..29a63b1 --- /dev/null +++ b/Source/ConformalDecals/UI/ColorBoxSlider.cs @@ -0,0 +1,55 @@ +using ConformalDecals.Util; +using UnityEngine; +using UnityEngine.UI; + +namespace ConformalDecals.UI { + public class ColorBoxSlider : MonoBehaviour { + [SerializeField] private ColorPickerController.ChannelUpdateEvent _onXChannelChanged = new ColorPickerController.ChannelUpdateEvent(); + [SerializeField] private ColorPickerController.ChannelUpdateEvent _onYChannelChanged = new ColorPickerController.ChannelUpdateEvent(); + + [SerializeField] private Vector2 _value; + [SerializeField] private Vector2Int _channel; + [SerializeField] private bool _hsl; + + [SerializeField] private BoxSlider _slider; + [SerializeField] private Image _image; + + private Material _imageMaterial; + + public Vector2 Value { + get => _value; + set { + _value.x = Mathf.Clamp01(value.x); + _value.y = Mathf.Clamp01(value.y); + UpdateSlider(); + OnChannelUpdate(); + } + } + + public void Awake() { + _imageMaterial = _image.material; + } + + public void OnSliderUpdate(Vector2 value) { + _value = value; + OnChannelUpdate(); + } + + public void OnChannelUpdate() { + _onXChannelChanged.Invoke(_value.x, _channel.x, _hsl); + _onYChannelChanged.Invoke(_value.y, _channel.y, _hsl); + } + + public void OnColorUpdate(Color rgb, ColorHSL hsl) { + Vector2 newValue; + _imageMaterial.SetColor(PropertyIDs._Color, rgb); + newValue.x = _hsl ? hsl[_channel.x] : rgb[_channel.x]; + newValue.y = _hsl ? hsl[_channel.y] : rgb[_channel.y]; + Value = newValue; + } + + public void UpdateSlider() { + _slider.Value = _value; + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/UI/ColorChannelSlider.cs b/Source/ConformalDecals/UI/ColorChannelSlider.cs new file mode 100644 index 0000000..ac9362f --- /dev/null +++ b/Source/ConformalDecals/UI/ColorChannelSlider.cs @@ -0,0 +1,81 @@ +using ConformalDecals.Util; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace ConformalDecals.UI { + public class ColorChannelSlider : MonoBehaviour { + [SerializeField] private ColorPickerController.ChannelUpdateEvent _onChannelChanged = new ColorPickerController.ChannelUpdateEvent(); + + [SerializeField] private float _value; + [SerializeField] private int _channel; + [SerializeField] private bool _hsl; + + [SerializeField] private Selectable _textBox; + [SerializeField] private Slider _slider; + [SerializeField] private Image _image; + + private Material _imageMaterial; + + private bool _ignoreUpdates; + + public float Value { + get => _value; + set { + _value = Mathf.Clamp01(value); + UpdateSlider(); + UpdateTextbox(); + OnChannelUpdate(); + } + } + + public void Awake() { + _imageMaterial = _image.material; + } + + public void OnTextBoxUpdate(string text) { + if (_ignoreUpdates) return; + + if (byte.TryParse(text, out byte byteValue)) { + _value = (float) byteValue / 255; + UpdateSlider(); + OnChannelUpdate(); + } + else { + // value is invalid, reset value + UpdateTextbox(); + } + } + + public void OnSliderUpdate(float value) { + if (_ignoreUpdates) return; + + _value = value; + UpdateTextbox(); + OnChannelUpdate(); + } + + public void OnChannelUpdate() { + _onChannelChanged.Invoke(_value, _channel, _hsl); + } + + public void OnColorUpdate(Color rgb, ColorHSL hsl) { + _imageMaterial.SetColor(PropertyIDs._Color, rgb); + Value = _hsl ? hsl[_channel] : rgb[_channel]; + } + + public void UpdateSlider() { + _ignoreUpdates = true; + _slider.value = _value; + _ignoreUpdates = false; + } + + public void UpdateTextbox() { + if (_textBox == null) return; + + _ignoreUpdates = true; + ((TMP_InputField) _textBox).text = ((byte) (255 * _value)).ToString(); + _ignoreUpdates = false; + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/UI/ColorPickerController.cs b/Source/ConformalDecals/UI/ColorPickerController.cs new file mode 100644 index 0000000..2a23917 --- /dev/null +++ b/Source/ConformalDecals/UI/ColorPickerController.cs @@ -0,0 +1,92 @@ +using System; +using ConformalDecals.Util; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +namespace ConformalDecals.UI { + public class ColorPickerController : MonoBehaviour { + [Serializable] + public class ColorUpdateEvent : UnityEvent { } + + [Serializable] + public class ChannelUpdateEvent : UnityEvent { } + + [SerializeField] private ColorUpdateEvent _onColorChanged = new ColorUpdateEvent(); + + [SerializeField] private Color _value; + [SerializeField] private Image _previewImage; + [SerializeField] private Selectable _hexTextBox; + + private bool _ignoreUpdate; + + public Color RGB { + get => _value; + set { + _value = value; + OnColorUpdate(); + } + } + + public ColorHSL HSL { + get => ColorHSL.RGB2HSL(_value); + set { + _value = ColorHSL.HSL2RGB(value); + OnColorUpdate(); + } + } + + + public static ColorPickerController Create(Color rgb, UnityAction colorUpdateCallback) { + var menu = Instantiate(UILoader.ColorPickerPrefab, MainCanvasUtil.MainCanvas.transform, true); + menu.AddComponent(); + MenuNavigation.SpawnMenuNavigation(menu, Navigation.Mode.Automatic, true); + + var controller = menu.GetComponent(); + controller.RGB = rgb; + controller._onColorChanged.AddListener(colorUpdateCallback); + return controller; + } + + public void OnClose() { + Destroy(gameObject); + } + + public void OnColorUpdate() { + _onColorChanged.Invoke(RGB, HSL); + _previewImage.material.SetColor(PropertyIDs._Color, RGB); + } + + public void OnHexColorUpdate(string text) { + if (_ignoreUpdate) return; + + if (ParseUtil.TryParseHexColor(text, out var newRGB)) { + RGB = newRGB; + OnColorUpdate(); + } + else { + UpdateHexColor(); + } + } + + public void UpdateHexColor() { + _ignoreUpdate = true; + ((TMP_InputField) _hexTextBox).text = $"{RGB.r:x2}{RGB.g:x2}{RGB.b:x2}"; + _ignoreUpdate = false; + } + + public void OnChannelUpdate(float value, int channel, bool hsl) { + if (hsl) { + var newHSL = HSL; + newHSL[channel] = value; + HSL = newHSL; + } + else { + var newRGB = RGB; + newRGB[channel] = value; + RGB = newRGB; + } + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/UI/FontMenuController.cs b/Source/ConformalDecals/UI/FontMenuController.cs index 5b28139..750eb82 100644 --- a/Source/ConformalDecals/UI/FontMenuController.cs +++ b/Source/ConformalDecals/UI/FontMenuController.cs @@ -3,27 +3,30 @@ using System.Collections.Generic; using ConformalDecals.Text; using UniLinq; using UnityEngine; +using UnityEngine.Events; using UnityEngine.UI; namespace ConformalDecals.UI { public class FontMenuController : MonoBehaviour { + [Serializable] + public class FontUpdateEvent : UnityEvent { } + + [SerializeField] private FontUpdateEvent _onFontChanged = new FontUpdateEvent(); + [SerializeField] private GameObject _menuItem; [SerializeField] private GameObject _menuList; - public DecalFont currentFont; - - public delegate void FontUpdateReceiver(DecalFont font); + private DecalFont _currentFont; - public FontUpdateReceiver fontUpdateCallback; - - public static FontMenuController Create(IEnumerable fonts, DecalFont currentFont, FontUpdateReceiver fontUpdateCallback) { + public static FontMenuController Create(IEnumerable fonts, DecalFont currentFont, UnityAction fontUpdateCallback) { var menu = Instantiate(UILoader.FontMenuPrefab, MainCanvasUtil.MainCanvas.transform, true); menu.AddComponent(); MenuNavigation.SpawnMenuNavigation(menu, Navigation.Mode.Automatic, true); var controller = menu.GetComponent(); - controller.fontUpdateCallback = fontUpdateCallback; - controller.currentFont = currentFont; + controller._currentFont = currentFont; + controller._onFontChanged.AddListener(fontUpdateCallback); + controller.Populate(fonts); return controller; } @@ -33,8 +36,8 @@ namespace ConformalDecals.UI { } public void OnFontSelected(DecalFont font) { - currentFont = font ?? throw new ArgumentNullException(nameof(font)); - fontUpdateCallback(currentFont); + _currentFont = font ?? throw new ArgumentNullException(nameof(font)); + _onFontChanged.Invoke(_currentFont); } public void Populate(IEnumerable fonts) { @@ -52,7 +55,7 @@ namespace ConformalDecals.UI { fontItem.Font = font; fontItem.fontSelectionCallback = OnFontSelected; - if (font == currentFont) active = fontItem.toggle; + if (font == _currentFont) active = fontItem.toggle; } if (active != null) active.isOn = true; diff --git a/Source/ConformalDecals/UI/TextEntryController.cs b/Source/ConformalDecals/UI/TextEntryController.cs index fbc5436..a4b883d 100644 --- a/Source/ConformalDecals/UI/TextEntryController.cs +++ b/Source/ConformalDecals/UI/TextEntryController.cs @@ -1,15 +1,18 @@ +using System; using ConformalDecals.Text; using TMPro; using UnityEngine; +using UnityEngine.Events; using UnityEngine.UI; namespace ConformalDecals.UI { public class TextEntryController : MonoBehaviour { - public delegate void TextUpdateReceiver(DecalText text); + [Serializable] + public class TextUpdateEvent : UnityEvent { } - public TextUpdateReceiver textUpdateCallback; + [SerializeField] private TextUpdateEvent _onTextUpdate = new TextUpdateEvent(); - public DecalText decalText; + private DecalText _decalText; private FontMenuController _fontMenu; [SerializeField] private Selectable _textBox; @@ -22,28 +25,28 @@ namespace ConformalDecals.UI { [SerializeField] private Toggle _smallCapsButton; [SerializeField] private Toggle _verticalButton; - public static TextEntryController Create(DecalText text, TextUpdateReceiver textUpdateCallback) { + public static TextEntryController Create(DecalText text, UnityAction textUpdateCallback) { var window = Instantiate(UILoader.TextEntryPrefab, MainCanvasUtil.MainCanvas.transform, true); window.AddComponent(); MenuNavigation.SpawnMenuNavigation(window, Navigation.Mode.Automatic, true); var controller = window.GetComponent(); - controller.decalText = text; - controller.textUpdateCallback = textUpdateCallback; + controller._decalText = text; + controller._onTextUpdate.AddListener(textUpdateCallback); text.font.SetupSample(controller._fontButton.GetComponentInChildren()); return controller; } private void Start() { - ((TMP_InputField) _textBox).text = decalText.text; + ((TMP_InputField) _textBox).text = _decalText.text; - _outlineWidthSlider.value = decalText.outlineWidth; - _boldButton.isOn = (decalText.style & FontStyles.Bold) != 0; - _italicButton.isOn = (decalText.style & FontStyles.Italic) != 0; - _underlineButton.isOn = (decalText.style & FontStyles.Underline) != 0; - _smallCapsButton.isOn = (decalText.style & FontStyles.SmallCaps) != 0; - _verticalButton.isOn = decalText.vertical; + _outlineWidthSlider.value = _decalText.outlineWidth; + _boldButton.isOn = (_decalText.style & FontStyles.Bold) != 0; + _italicButton.isOn = (_decalText.style & FontStyles.Italic) != 0; + _underlineButton.isOn = (_decalText.style & FontStyles.Underline) != 0; + _smallCapsButton.isOn = (_decalText.style & FontStyles.SmallCaps) != 0; + _verticalButton.isOn = _decalText.vertical; } @@ -53,26 +56,26 @@ namespace ConformalDecals.UI { } public void OnAnyUpdate() { - textUpdateCallback(decalText); + _onTextUpdate.Invoke(_decalText); } public void OnTextUpdate(string newText) { - this.decalText.text = newText; + this._decalText.text = newText; OnAnyUpdate(); } public void OnFontMenu() { - if (_fontMenu == null) _fontMenu = FontMenuController.Create(DecalConfig.Fonts, decalText.font, OnFontUpdate); + if (_fontMenu == null) _fontMenu = FontMenuController.Create(DecalConfig.Fonts, _decalText.font, OnFontUpdate); } public void OnFontUpdate(DecalFont font) { - decalText.font = font; + _decalText.font = font; font.SetupSample(_fontButton.GetComponentInChildren()); var textBox = ((TMP_InputField) _textBox); - textBox.textComponent.fontStyle = decalText.style | decalText.font.fontStyle; - textBox.fontAsset = decalText.font.fontAsset; + textBox.textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; + textBox.fontAsset = _decalText.font.fontAsset; OnAnyUpdate(); } @@ -80,64 +83,64 @@ namespace ConformalDecals.UI { public void OnColorMenu() { } public void OnColorUpdate(Color color) { - decalText.color = color; + _decalText.color = color; OnAnyUpdate(); } public void OnOutlineColorMenu() { } public void OnOutlineColorUpdate(Color color) { - decalText.outlineColor = color; + _decalText.outlineColor = color; OnAnyUpdate(); } public void OnOutlineUpdate(float value) { - decalText.outlineWidth = value; + _decalText.outlineWidth = value; OnAnyUpdate(); } public void OnBoldUpdate(bool state) { - if (state) decalText.style |= FontStyles.Bold; - else decalText.style &= ~FontStyles.Bold; + if (state) _decalText.style |= FontStyles.Bold; + else _decalText.style &= ~FontStyles.Bold; - ((TMP_InputField) _textBox).textComponent.fontStyle = decalText.style | decalText.font.fontStyle; + ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; OnAnyUpdate(); } public void OnItalicUpdate(bool state) { - if (state) decalText.style |= FontStyles.Italic; - else decalText.style &= ~FontStyles.Italic; + if (state) _decalText.style |= FontStyles.Italic; + else _decalText.style &= ~FontStyles.Italic; - ((TMP_InputField) _textBox).textComponent.fontStyle = decalText.style | decalText.font.fontStyle; + ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; OnAnyUpdate(); } public void OnUnderlineUpdate(bool state) { - if (state) decalText.style |= FontStyles.Underline; - else decalText.style &= ~FontStyles.Underline; + if (state) _decalText.style |= FontStyles.Underline; + else _decalText.style &= ~FontStyles.Underline; - ((TMP_InputField) _textBox).textComponent.fontStyle = decalText.style | decalText.font.fontStyle; + ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; OnAnyUpdate(); } public void OnSmallCapsUpdate(bool state) { - if (state) decalText.style |= FontStyles.SmallCaps; - else decalText.style &= ~FontStyles.SmallCaps; + if (state) _decalText.style |= FontStyles.SmallCaps; + else _decalText.style &= ~FontStyles.SmallCaps; - ((TMP_InputField) _textBox).textComponent.fontStyle = decalText.style | decalText.font.fontStyle; + ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; OnAnyUpdate(); } public void OnVerticalUpdate(bool state) { - decalText.vertical = state; + _decalText.vertical = state; OnAnyUpdate(); } } diff --git a/Source/ConformalDecals/UI/UILoader.cs b/Source/ConformalDecals/UI/UILoader.cs index 420cbe7..b2a3519 100644 --- a/Source/ConformalDecals/UI/UILoader.cs +++ b/Source/ConformalDecals/UI/UILoader.cs @@ -10,18 +10,22 @@ namespace ConformalDecals.UI { private static GameObject _textEntryPrefab; private static GameObject _fontMenuPrefab; + private static GameObject _colorPickerPrefab; public static GameObject FontMenuPrefab => _fontMenuPrefab; public static GameObject TextEntryPrefab => _textEntryPrefab; + public static GameObject ColorPickerPrefab => _colorPickerPrefab; private void Awake() { var prefabs = AssetBundle.LoadFromFile(Path + "ui.conformaldecals"); _textEntryPrefab = prefabs.LoadAsset("TextEntryPanel") as GameObject; _fontMenuPrefab = prefabs.LoadAsset("FontMenuPanel") as GameObject; + _colorPickerPrefab = prefabs.LoadAsset("ColorPickerPanel") as GameObject; ProcessWindow(_textEntryPrefab); ProcessWindow(_fontMenuPrefab); + ProcessWindow(_colorPickerPrefab); } private static void ProcessWindow(GameObject window) { diff --git a/Source/ConformalDecals/Util/ColorHSL.cs b/Source/ConformalDecals/Util/ColorHSL.cs index 06436f7..5ed7cc3 100644 --- a/Source/ConformalDecals/Util/ColorHSL.cs +++ b/Source/ConformalDecals/Util/ColorHSL.cs @@ -17,7 +17,7 @@ namespace ConformalDecals.Util { } public override string ToString() { - return $"HSLA({(object) this.h:F3}, {(object) this.s:F3}, {(object) this.l:F3}, {(object) this.a:F3})"; + return $"HSLA({this.h:F3}, {this.s:F3}, {this.l:F3}, {this.a:F3})"; } public string ToString(string format) { @@ -49,6 +49,41 @@ namespace ConformalDecals.Util { return ((Vector4) this).GetHashCode(); } + public float this[int index] { + get { + switch (index) { + case 0: + return this.h; + case 1: + return this.s; + case 2: + return this.l; + case 3: + return this.a; + default: + throw new IndexOutOfRangeException("Invalid Vector3 index!"); + } + } + set { + switch (index) { + case 0: + this.h = value; + break; + case 1: + this.s = value; + break; + case 2: + this.l = value; + break; + case 3: + this.a = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Vector3 index!"); + } + } + } + public static bool operator ==(ColorHSL lhs, ColorHSL rhs) { return lhs.Equals(rhs); } diff --git a/Source/ConformalDecals/Util/ParseUtil.cs b/Source/ConformalDecals/Util/ParseUtil.cs index fcb1ecd..1e02b06 100644 --- a/Source/ConformalDecals/Util/ParseUtil.cs +++ b/Source/ConformalDecals/Util/ParseUtil.cs @@ -139,44 +139,53 @@ namespace ConformalDecals.Util { throw new FormatException($"Improperly formatted {typeof(T)} value for {valueName} : '{valueString}"); } + public static bool TryParseHexColor(string valueString, out Color32 value) { + value = new Color32(0, 0, 0, byte.MaxValue); + + if (!int.TryParse(valueString, System.Globalization.NumberStyles.HexNumber, null, out var hexColor)) return false; + + switch (valueString.Length) { + case 8: // RRGGBBAA + value.a = (byte) (hexColor & 0xFF); + hexColor >>= 8; + goto case 6; + + case 6: // RRGGBB + value.b = (byte) (hexColor & 0xFF); + hexColor >>= 8; + value.g = (byte) (hexColor & 0xFF); + hexColor >>= 8; + value.r = (byte) (hexColor & 0xFF); + return true; + + case 4: // RGBA + value.a = (byte) ((hexColor & 0xF) << 4); + hexColor >>= 4; + goto case 3; + + case 3: // RGB + value.b = (byte) (hexColor & 0xF << 4); + hexColor >>= 4; + value.g = (byte) (hexColor & 0xF << 4); + hexColor >>= 4; + value.r = (byte) (hexColor & 0xF << 4); + return true; + + default: + return false; + } + } + public static bool TryParseColor32(string valueString, out Color32 value) { value = new Color32(0, 0, 0, byte.MaxValue); - // HTML-style hex color + // hex color if (valueString[0] == '#') { var hexColorString = valueString.Substring(1); - if (!int.TryParse(hexColorString, System.Globalization.NumberStyles.HexNumber, null, out var hexColor)) return false; - - switch (hexColorString.Length) { - case 8: // RRGGBBAA - value.a = (byte) (hexColor & 0xFF); - hexColor >>= 8; - goto case 6; - - case 6: // RRGGBB - value.b = (byte) (hexColor & 0xFF); - hexColor >>= 8; - value.g = (byte) (hexColor & 0xFF); - hexColor >>= 8; - value.r = (byte) (hexColor & 0xFF); - return true; - - case 4: // RGBA - value.a = (byte) ((hexColor & 0xF) << 4); - hexColor >>= 4; - goto case 3; - - case 3: // RGB - value.b = (byte) (hexColor & 0xF << 4); - hexColor >>= 4; - value.g = (byte) (hexColor & 0xF << 4); - hexColor >>= 4; - value.r = (byte) (hexColor & 0xF << 4); - return true; - - default: - return false; + if (TryParseHexColor(hexColorString, out var hexColor)) { + value = hexColor; + return true; } }