소스 검색

Merge branch 'master' into fixes/18.08

Krzysztof Krysiński 3 주 전
부모
커밋
ce3d207d0d

+ 23 - 0
.github/workflows/localization-key-reference.yml

@@ -0,0 +1,23 @@
+name: Localization Reference Key Check
+
+on:
+  push:
+    branches: [ "master" ]
+  pull_request:
+    branches: [ "master" ]
+
+jobs:
+  check-key-references:
+    name: "Check Localization Keys are referenced"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.13'
+          cache: 'pip'
+          cache-dependency-path: .github/workflows/localization/check-key-references.pip.txt
+      - name: Install dependencies
+        run: pip install -r .github/workflows/localization/check-key-references.pip.txt
+      - name: Check Localization Key References
+        run: python .github/workflows/localization/check-key-references.py

+ 1 - 0
.github/workflows/localization/check-key-references.pip.txt

@@ -0,0 +1 @@
+pyahocorasick

+ 70 - 0
.github/workflows/localization/check-key-references.py

@@ -0,0 +1,70 @@
+import json
+import os
+import logging
+from collections import OrderedDict
+import time
+import ahocorasick
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger()
+
+# PATHS
+REFERENCE_LANGUAGE = "src/PixiEditor/Data/Localization/Languages/en.json"
+SEARCH_DIRECTORIES = ["src/PixiEditor/Views", "src/PixiEditor/ViewModels", "src/PixiEditor", "src/"]
+IGNORE_DIRECTORIES = ["src/PixiEditor/Data/Localization"]
+
+def load_json(file_path):
+    """Load language JSON"""
+    try:
+        with open(file_path, "r", encoding="utf-8-sig") as f:
+            return json.load(f, object_pairs_hook=OrderedDict)
+    except FileNotFoundError:
+        print(f"::error::File not found: {file_path}")
+        return OrderedDict()
+    except json.JSONDecodeError as e:
+        print(f"::error::Failed to parse JSON in {file_path}: {e}")
+        return OrderedDict()
+
+def build_automaton(keys: list[str]) -> ahocorasick.Automaton:
+    A = ahocorasick.Automaton()
+    for i, k in enumerate(keys):
+        A.add_word(k, (i, k))
+    A.make_automaton()
+    return A
+
+def find_missing_keys(keys):
+    automaton = build_automaton(keys)
+    present = set()
+
+    ignore_prefixes = tuple(os.path.abspath(p) for p in IGNORE_DIRECTORIES)
+    for base_dir in SEARCH_DIRECTORIES:
+        for root, dirs, files in os.walk(base_dir, topdown=True):
+            dirs[:] = [d for d in dirs if not os.path.abspath(os.path.join(root, d)).startswith(ignore_prefixes)]
+            for file in files:
+                with open(os.path.join(root, file), "r", encoding="utf‑8", errors="ignore") as f:
+                    for _, (_, k) in automaton.iter(f.read()):
+                        present.add(k)
+                        if len(present) == len(keys):
+                            return []
+    return sorted(set(keys) - present)
+
+def main():
+    keys = load_json(REFERENCE_LANGUAGE)
+
+    print("Searching trough keys...")
+    start = time.time()
+    missing_keys = find_missing_keys(keys)
+    end = time.time()
+    print(f"Done, searching took {end - start}s")
+
+    if len(missing_keys) > 0:
+        print("Unreferenced keys have been found")
+        for key in missing_keys:
+            print(f"::error file={REFERENCE_LANGUAGE},title=Unreferenced key::No reference to '{key}' found")
+        return 1
+    else:
+        print("All keys have been referenced")
+        return 0
+    
+if __name__ == "__main__":
+    exit(main())

+ 163 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -18,6 +18,11 @@ public class NoiseNode : RenderNode
     private NoiseType previousNoiseType = Nodes.NoiseType.FractalPerlin;
     private int previousOctaves = -1;
     private VecD previousOffset = new VecD(0d, 0d);
+    private VoronoiFeature previousVoronoiFeature = Nodes.VoronoiFeature.F1;
+    private double previousRandomness = double.NaN;
+    private double previousAngleOffset = double.NaN;
+    
+    private Shader voronoiShader;
 
     private Paint paint = new();
 
@@ -25,7 +30,7 @@ public class NoiseNode : RenderNode
         ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
 
     public InputProperty<NoiseType> NoiseType { get; }
-
+    
     public InputProperty<VecD> Offset { get; }
     
     public InputProperty<double> Scale { get; }
@@ -33,6 +38,12 @@ public class NoiseNode : RenderNode
     public InputProperty<int> Octaves { get; }
 
     public InputProperty<double> Seed { get; }
+    
+    public InputProperty<VoronoiFeature> VoronoiFeature { get; }
+    
+    public InputProperty<double> Randomness { get; }
+    
+    public InputProperty<double> AngleOffset { get; }
 
     public NoiseNode()
     {
@@ -45,6 +56,13 @@ public class NoiseNode : RenderNode
             .WithRules(validator => validator.Min(1));
 
         Seed = CreateInput(nameof(Seed), "SEED", 0d);
+        
+        VoronoiFeature = CreateInput(nameof(VoronoiFeature), "VORONOI_FEATURE", Nodes.VoronoiFeature.F1);
+        
+        Randomness = CreateInput(nameof(Randomness), "RANDOMNESS", 1d)
+            .WithRules(v => v.Min(0d).Max(1d));
+        
+        AngleOffset = CreateInput(nameof(AngleOffset), "ANGLE_OFFSET", 0d);
     }
 
     protected override void OnPaint(RenderContext context, DrawingSurface target)
@@ -54,6 +72,9 @@ public class NoiseNode : RenderNode
             || previousOctaves != Octaves.Value
             || previousNoiseType != NoiseType.Value
             || previousOffset != Offset.Value
+            || previousVoronoiFeature != VoronoiFeature.Value
+            || Math.Abs(previousRandomness - Randomness.Value) > 0.000001
+            || Math.Abs(previousAngleOffset - AngleOffset.Value) > 0.000001
             || double.IsNaN(previousScale))
         {
             if (Scale.Value < 0.000001)
@@ -67,6 +88,10 @@ public class NoiseNode : RenderNode
                 return;
             }
 
+            if (paint.Shader != voronoiShader)
+            {
+                paint?.Shader?.Dispose();
+            }
             paint.Shader = shader;
 
             // Define a grayscale color filter to apply to the image
@@ -76,6 +101,9 @@ public class NoiseNode : RenderNode
             previousSeed = Seed.Value;
             previousOctaves = Octaves.Value;
             previousNoiseType = NoiseType.Value;
+            previousVoronoiFeature = VoronoiFeature.Value;
+            previousRandomness = Randomness.Value;
+            previousAngleOffset = AngleOffset.Value;
         }
 
         RenderNoise(target);
@@ -101,7 +129,11 @@ public class NoiseNode : RenderNode
         {
             return false;
         }
-        
+
+        if (paint.Shader != voronoiShader)
+        {
+            paint?.Shader?.Dispose();
+        }
         paint.Shader = shader;
         paint.ColorFilter = grayscaleFilter;
         
@@ -122,17 +154,145 @@ public class NoiseNode : RenderNode
                 (float)(1d / Scale.Value),
                 (float)(1d / Scale.Value),
                 octaves, (float)Seed.Value),
+            Nodes.NoiseType.Voronoi => GetVoronoiShader((float)(1d / (Scale.Value)), octaves, (float)Seed.Value, (int)VoronoiFeature.Value, (float)Randomness.Value, (float)AngleOffset.Value),
             _ => null
         };
 
         return shader;
     }
 
+    private Shader GetVoronoiShader(float frequency, int octaves, float seed, int feature, float randomness, float angleOffset)
+    {
+        string voronoiShaderCode = """
+                                   uniform float iSeed;
+                                   uniform float iFrequency;
+                                   uniform int iOctaves;
+                                   uniform float iRandomness;
+                                   uniform int iFeature;
+                                   uniform float iAngleOffset;
+                                   
+                                   const int MAX_OCTAVES = 8;
+                                   const float LARGE_NUMBER = 1e9;
+                                   const float FEATURE_SEED_SCALE = 10.0;
+                                   const float PI = 3.14159265;
+                                   
+                                   float hashPoint(float2 p, float seed) {
+                                       p = fract(p * float2(0.3183099, 0.3678794) + seed);
+                                       p += dot(p, p.yx + 19.19);
+                                       return fract(p.x * p.y);
+                                   }
+                                   
+                                   float2 getFeaturePoint(float2 cell, float seed, float randomness, float angleOffset) {
+                                       float2 randomCellOffset = float2(
+                                           hashPoint(cell, seed),
+                                           hashPoint(cell, seed + 17.0)
+                                       );
+                                       
+                                       float2 featurePoint = mix(float2(0.5, 0.5), randomCellOffset, randomness);
+                                       
+                                       float angle = hashPoint(cell, seed + 53.0) * PI * 2.0;
+                                       angle += angleOffset;
+                                       
+                                       float2 dir = float2(cos(angle), sin(angle));
+                                       float offsetAmount = 0.15;
+                                       featurePoint += dir * offsetAmount * randomness;
+                                       
+                                       featurePoint = clamp(featurePoint, 0.0, 1.0);
+                                       
+                                       return featurePoint;
+                                   }
+                                   
+                                   float2 getVoronoiDistances(float2 pos, float seed) {
+                                       float2 cell = floor(pos);
+                                       float minDist = LARGE_NUMBER;
+                                       float secondMinDist = LARGE_NUMBER;
+                                   
+                                       for (int y = -1; y <= 1; y++) {
+                                           for (int x = -1; x <= 1; x++) {
+                                               float2 neighborCell = cell + float2(float(x), float(y));
+                                               float2 featurePoint = getFeaturePoint(neighborCell, seed, iRandomness, iAngleOffset);
+                                               float2 delta = pos - (neighborCell + featurePoint);
+                                               float dist = length(delta);
+                                               
+                                               if (dist < minDist) {
+                                                   secondMinDist = minDist;
+                                                   minDist = dist;
+                                               } else if (dist < secondMinDist) {
+                                                   secondMinDist = dist;
+                                               }
+                                           }
+                                       }
+                                       return float2(minDist, secondMinDist);
+                                   }
+                                   
+                                   half4 main(float2 uv) {
+                                       float noiseSum = 0.0;
+                                       float amplitude = 1.0;
+                                       float amplitudeSum = 0.0;
+                                   
+                                       for (int octave = 0; octave < MAX_OCTAVES; octave++) {
+                                           if (octave >= iOctaves) break;
+                                   
+                                           float freq = iFrequency * exp2(float(octave));
+                                           float2 samplePos = uv * freq;
+                                           
+                                           float dist = 0.0;
+                                           float2 distances = getVoronoiDistances(samplePos, iSeed + float(octave) * FEATURE_SEED_SCALE);
+                                           float f1 = distances.x;
+                                           float f2 = distances.y;
+                                           
+                                           if (iFeature == 0) {
+                                               dist = f1;
+                                           }
+                                           else if (iFeature == 1) {
+                                               dist = f2;
+                                           }
+                                           else if (iFeature == 2) {
+                                               dist = f2 - f1;
+                                           }
+                                   
+                                           noiseSum += dist * amplitude;
+                                           amplitudeSum += amplitude;
+                                           amplitude *= 0.5;
+                                       }
+                                   
+                                       return half4(noiseSum / amplitudeSum);
+                                   }
+                                   """;
+        
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iSeed", new Uniform("iSeed", seed));
+        uniforms.Add("iFrequency", new Uniform("iFrequency", frequency));
+        uniforms.Add("iOctaves", new Uniform("iOctaves", octaves));
+        uniforms.Add("iRandomness", new Uniform("iRandomness", randomness));
+        uniforms.Add("iFeature", new Uniform("iFeature", feature));
+        uniforms.Add("iAngleOffset", new Uniform("iAngleOffset", angleOffset));
+
+        if (voronoiShader == null)
+        {
+            voronoiShader = Shader.Create(voronoiShaderCode, uniforms, out _);
+        }
+        else
+        {
+            voronoiShader = voronoiShader.WithUpdatedUniforms(uniforms);
+        }
+        
+        return voronoiShader;
+    }
+
     public override Node CreateCopy() => new NoiseNode();
 }
 
 public enum NoiseType
 {
     TurbulencePerlin,
-    FractalPerlin
+    FractalPerlin,
+    Voronoi
+}
+
+public enum VoronoiFeature
+{
+    F1 = 0, // Distance to the closest feature point
+    F2 = 1, // Distance to the second-closest feature point
+    F2MinusF1 = 2
 }

+ 1 - 0
src/PixiEditor.IdentityProvider.PixiAuth/PixiAuthIdentityProvider.cs

@@ -129,6 +129,7 @@ public class PixiAuthIdentityProvider : IIdentityProvider
             Error(e.Message, e.TimeLeft);
             LoginTimeout?.Invoke(e.TimeLeft);
         }
+        // Can produce SESSION_NOT_FOUND or USER_NOT_FOUND as a message, this comment ensure it's catched by the localization key checker
         catch (PixiAuthException authException)
         {
             Error(authException.Message);

+ 8 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -946,6 +946,13 @@
   "RAW_COLOR_SAMPLE_MODE": "Raw",
   "FRACTAL_PERLIN_NOISE_TYPE": "Perlin",
   "TURBULENCE_PERLIN_NOISE_TYPE": "Turbulence",
+  "VORONOI_NOISE_TYPE": "Voronoi",
+  "VORONOI_FEATURE": "Feature",
+  "F1_VORONOI_FEATURE": "F1",
+  "F2_VORONOI_FEATURE": "F2",
+  "F2_MINUS_F1_VORONOI_FEATURE": "F2-F1",
+  "RANDOMNESS": "Randomness",
+  "ANGLE_OFFSET": "Angle Offset",
   "INHERIT_COLOR_SPACE_TYPE": "Inherit",
   "SRGB_COLOR_SPACE_TYPE": "sRGB",
   "LINEAR_SRGB_COLOR_SPACE_TYPE": "Linear sRGB",
@@ -1085,6 +1092,7 @@
   "REMAP_NODE": "Remap",
   "TEXT_TOOL_ACTION_DISPLAY": "Click on the canvas to add a new text (drag while clicking to set the size). Click on existing text to edit it.",
   "PASTE_CELS": "Paste cels",
+  "PASTE_CELS_DESCRIPTIVE": "Paste cels from clipboard into the current frame",
   "SCALE_X": "Scale X",
   "SCALE_Y": "Scale Y",
   "TRANSLATE_X": "Translate X",

+ 27 - 17
src/PixiEditor/Helpers/Converters/EnumToLocalizedStringConverter.cs

@@ -1,4 +1,6 @@
-using System.Globalization;
+using System.Diagnostics;
+using System.Globalization;
+using System.Reflection;
 using PixiEditor.Extensions.Helpers;
 using PixiEditor.UI.Common.Localization;
 
@@ -6,30 +8,38 @@ namespace PixiEditor.Helpers.Converters;
 
 internal class EnumToLocalizedStringConverter : SingleInstanceConverter<EnumToLocalizedStringConverter>
 {
+    private Dictionary<object, string> enumTranslations = new(
+        typeof(EnumToLocalizedStringConverter).Assembly
+            .GetCustomAttributes()
+            .OfType<ILocalizeEnumInfo>()
+            .Select(x => new KeyValuePair<object, string>(x.GetEnumValue(), x.LocalizationKey)));
+    
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
-        if (value is Enum enumValue)
+        if (value is not Enum enumValue)
         {
-            if (EnumHelpers.HasDescription(enumValue))
-            {
-                return EnumHelpers.GetDescription(enumValue);
-            }
+            return value;
+        }
+
+        if (enumTranslations.TryGetValue(enumValue, out var assemblyDefinedKey))
+        {
+            return assemblyDefinedKey;
+        }
 
-            return ToLocalizedStringFormat(enumValue);
+        if (EnumHelpers.HasDescription(enumValue))
+        {
+            return EnumHelpers.GetDescription(enumValue);
         }
 
-        return value;
+        ThrowUntranslatedEnumValue(enumValue);
+        return enumValue;
     }
 
-    private string ToLocalizedStringFormat(Enum enumValue)
+    [Conditional("DEBUG")]
+    private static void ThrowUntranslatedEnumValue(object value)
     {
-        // VALUE_ENUMTYPE
-        // for example BlendMode.Normal becomes NORMAL_BLEND_MODE
-
-        string enumType = enumValue.GetType().Name;
-
-        string value = enumValue.ToString();
-
-        return $"{value.ToSnakeCase()}_{enumType.ToSnakeCase()}".ToUpper();
+        throw new ArgumentException(
+            $"Enum value '{value.GetType()}.{value}' has no value defined. Either add a Description attribute to the enum values or a LocalizeEnum attribute in EnumTranslations.cs for third party enums",
+            nameof(value));
     }
 }

+ 0 - 19
src/PixiEditor/Helpers/EnumDescriptionConverter.cs

@@ -1,19 +0,0 @@
-using System.Globalization;
-using PixiEditor.Extensions.Helpers;
-using PixiEditor.Helpers.Converters;
-using PixiEditor.UI.Common.Localization;
-
-namespace PixiEditor.Helpers;
-
-internal class EnumDescriptionConverter : SingleInstanceConverter<EnumDescriptionConverter>
-{
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        if (value is Enum enumValue)
-        {
-            return EnumHelpers.GetDescription(enumValue);
-        }
-
-        return value;
-    }
-}

+ 18 - 0
src/PixiEditor/Helpers/LocalizeEnumAttribute.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Helpers;
+
+public interface ILocalizeEnumInfo
+{
+    public object GetEnumValue();
+    
+    public string LocalizationKey { get; }
+}
+
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+public class LocalizeEnumAttribute<T>(T value, string key) : Attribute, ILocalizeEnumInfo where T : Enum
+{
+    public T Value { get; } = value;
+
+    object ILocalizeEnumInfo.GetEnumValue() => Value;
+    
+    public string LocalizationKey { get; } = key;
+}

+ 100 - 0
src/PixiEditor/Models/EnumTranslations.cs

@@ -0,0 +1,100 @@
+using Drawie.Backend.Core.Shaders.Generation;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using PixiEditor.AnimationRenderer.Core;
+using PixiEditor.ChangeableDocument.Changeables.Graph.ColorSpaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.Helpers;
+
+[assembly: LocalizeEnum<StrokeCap>(StrokeCap.Butt, "BUTT_STROKE_CAP")]
+[assembly: LocalizeEnum<StrokeCap>(StrokeCap.Round, "ROUND_STROKE_CAP")]
+[assembly: LocalizeEnum<StrokeCap>(StrokeCap.Square, "SQUARE_STROKE_CAP")]
+
+[assembly: LocalizeEnum<StrokeJoin>(StrokeJoin.Bevel, "BEVEL_STROKE_JOIN")]
+[assembly: LocalizeEnum<StrokeJoin>(StrokeJoin.Round, "ROUND_STROKE_JOIN")]
+[assembly: LocalizeEnum<StrokeJoin>(StrokeJoin.Miter, "MITER_STROKE_JOIN")]
+
+[assembly: LocalizeEnum<GrayscaleNode.GrayscaleMode>(GrayscaleNode.GrayscaleMode.Weighted, "WEIGHTED_GRAYSCALE_MODE")]
+[assembly: LocalizeEnum<GrayscaleNode.GrayscaleMode>(GrayscaleNode.GrayscaleMode.Average, "AVERAGE_GRAYSCALE_MODE")]
+[assembly: LocalizeEnum<GrayscaleNode.GrayscaleMode>(GrayscaleNode.GrayscaleMode.Custom, "CUSTOM_GRAYSCALE_MODE")]
+
+[assembly: LocalizeEnum<ColorSampleMode>(ColorSampleMode.ColorManaged, "COLOR_MANAGED_COLOR_SAMPLE_MODE")]
+[assembly: LocalizeEnum<ColorSampleMode>(ColorSampleMode.Raw, "RAW_COLOR_SAMPLE_MODE")]
+
+[assembly: LocalizeEnum<TileMode>(TileMode.Clamp, "CLAMP_TILE_MODE")]
+[assembly: LocalizeEnum<TileMode>(TileMode.Decal, "DECAL_TILE_MODE")]
+[assembly: LocalizeEnum<TileMode>(TileMode.Mirror, "MIRROR_TILE_MODE")]
+[assembly: LocalizeEnum<TileMode>(TileMode.Repeat, "REPEAT_TILE_MODE")]
+
+[assembly: LocalizeEnum<CombineSeparateColorMode>(CombineSeparateColorMode.RGB, "R_G_B_COMBINE_SEPARATE_COLOR_MODE")] 
+[assembly: LocalizeEnum<CombineSeparateColorMode>(CombineSeparateColorMode.HSV, "H_S_V_COMBINE_SEPARATE_COLOR_MODE")]
+[assembly: LocalizeEnum<CombineSeparateColorMode>(CombineSeparateColorMode.HSL, "H_S_L_COMBINE_SEPARATE_COLOR_MODE")]
+
+[assembly: LocalizeEnum<VectorPathOp>(VectorPathOp.Difference, "DIFFERENCE_VECTOR_PATH_OP")]
+[assembly: LocalizeEnum<VectorPathOp>(VectorPathOp.Intersect, "INTERSECT_VECTOR_PATH_OP")]
+[assembly: LocalizeEnum<VectorPathOp>(VectorPathOp.Union, "UNION_VECTOR_PATH_OP")]
+[assembly: LocalizeEnum<VectorPathOp>(VectorPathOp.Xor, "XOR_VECTOR_PATH_OP")]
+[assembly: LocalizeEnum<VectorPathOp>(VectorPathOp.ReverseDifference, "REVERSE_DIFFERENCE_VECTOR_PATH_OP")]
+
+[assembly: LocalizeEnum<QualityPreset>(QualityPreset.VeryLow, "VERY_LOW_QUALITY_PRESET")]
+[assembly: LocalizeEnum<QualityPreset>(QualityPreset.Low, "LOW_QUALITY_PRESET")]
+[assembly: LocalizeEnum<QualityPreset>(QualityPreset.Medium, "MEDIUM_QUALITY_PRESET")]
+[assembly: LocalizeEnum<QualityPreset>(QualityPreset.High, "HIGH_QUALITY_PRESET")]
+[assembly: LocalizeEnum<QualityPreset>(QualityPreset.VeryHigh, "VERY_HIGH_QUALITY_PRESET")]
+
+[assembly: LocalizeEnum<ColorSpaceType>(ColorSpaceType.Inherit, "INHERIT_COLOR_SPACE_TYPE")]
+[assembly: LocalizeEnum<ColorSpaceType>(ColorSpaceType.Srgb, "SRGB_COLOR_SPACE_TYPE")]
+[assembly: LocalizeEnum<ColorSpaceType>(ColorSpaceType.LinearSrgb, "LINEAR_SRGB_COLOR_SPACE_TYPE")]
+
+[assembly: LocalizeEnum<EasingType>(EasingType.Linear, "LINEAR_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InSine, "IN_SINE_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutSine, "OUT_SINE_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutSine, "IN_OUT_SINE_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InQuad, "IN_QUAD_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutQuad, "OUT_QUAD_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutQuad, "IN_OUT_QUAD_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InCubic, "IN_CUBIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutCubic, "OUT_CUBIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutCubic, "IN_OUT_CUBIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InQuart, "IN_QUART_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutQuart, "OUT_QUART_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutQuart, "IN_OUT_QUART_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InQuint, "IN_QUINT_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutQuint, "OUT_QUINT_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutQuint, "IN_OUT_QUINT_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InExpo, "IN_EXPO_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutExpo, "OUT_EXPO_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutExpo, "IN_OUT_EXPO_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InCirc, "IN_CIRC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutCirc, "OUT_CIRC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutCirc, "IN_OUT_CIRC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InBack, "IN_BACK_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutBack, "OUT_BACK_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutBack, "IN_OUT_BACK_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InElastic, "IN_ELASTIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutElastic, "OUT_ELASTIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutElastic, "IN_OUT_ELASTIC_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InBounce, "IN_BOUNCE_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.OutBounce, "OUT_BOUNCE_EASING_TYPE")]
+[assembly: LocalizeEnum<EasingType>(EasingType.InOutBounce, "IN_OUT_BOUNCE_EASING_TYPE")]
+
+[assembly: LocalizeEnum<RotationType>(RotationType.Degrees, "DEGREES_ROTATION_TYPE")]
+[assembly: LocalizeEnum<RotationType>(RotationType.Radians, "RADIANS_ROTATION_TYPE")]
+
+[assembly: LocalizeEnum<NoiseType>(NoiseType.TurbulencePerlin, "TURBULENCE_PERLIN_NOISE_TYPE")]
+[assembly: LocalizeEnum<NoiseType>(NoiseType.FractalPerlin, "FRACTAL_PERLIN_NOISE_TYPE")]
+[assembly: LocalizeEnum<NoiseType>(NoiseType.Voronoi, "VORONOI_NOISE_TYPE")]
+
+[assembly: LocalizeEnum<OutlineType>(OutlineType.Simple, "SIMPLE_OUTLINE_TYPE")]
+[assembly: LocalizeEnum<OutlineType>(OutlineType.Gaussian, "GAUSSIAN_OUTLINE_TYPE")]
+[assembly: LocalizeEnum<OutlineType>(OutlineType.PixelPerfect, "PIXEL_PERFECT_OUTLINE_TYPE")]
+
+[assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F1, "F1_VORONOI_FEATURE")]
+[assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2, "F2_VORONOI_FEATURE")]
+[assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2MinusF1, "F2_MINUS_F1_VORONOI_FEATURE")]

+ 5 - 1
src/PixiEditor/Models/Handlers/Toolbars/PaintBrushShape.cs

@@ -1,7 +1,11 @@
-namespace PixiEditor.Models.Handlers.Toolbars;
+using System.ComponentModel;
+
+namespace PixiEditor.Models.Handlers.Toolbars;
 
 public enum PaintBrushShape
 {
+    [Description("PAINT_BRUSH_SHAPE_CIRCLE")]
     Circle,
+    [Description("PAINT_BRUSH_SHAPE_SQUARE")]
     Square,
 }

+ 5 - 1
src/PixiEditor/Models/Tools/BrightnessMode.cs

@@ -1,7 +1,11 @@
-namespace PixiEditor.Models.Tools;
+using System.ComponentModel;
+
+namespace PixiEditor.Models.Tools;
 
 public enum BrightnessMode
 {
+    [Description("BRIGHTNESS_MODE_DEFAULT")]
     Default,
+    [Description("BRIGHTNESS_MODE_REPEAT")]
     Repeat
 }

+ 29 - 1
src/PixiEditor/ViewModels/Document/Nodes/NoiseNodeViewModel.cs

@@ -1,8 +1,36 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 [NodeViewModel("NOISE_NODE", "IMAGE", PixiPerfectIcons.Noise)]
-internal class NoiseNodeViewModel : NodeViewModel<NoiseNode>;
+internal class NoiseNodeViewModel : NodeViewModel<NoiseNode>
+{
+    private GenericEnumPropertyViewModel Type { get; set; }
+    private GenericEnumPropertyViewModel VoronoiFeature { get; set; }
+    private NodePropertyViewModel Randomness { get; set; }
+    private NodePropertyViewModel AngleOffset { get; set; }
+
+    public override void OnInitialized()
+    {
+        Type = FindInputProperty("NoiseType") as GenericEnumPropertyViewModel;
+        VoronoiFeature = FindInputProperty("VoronoiFeature") as GenericEnumPropertyViewModel;
+        Randomness = FindInputProperty("Randomness");
+        AngleOffset = FindInputProperty("AngleOffset");
+
+        Type.ValueChanged += (_, _) => UpdateInputVisibility();
+        UpdateInputVisibility();
+    }
+
+    private void UpdateInputVisibility()
+    {
+        if (Type.Value is not NoiseType type)
+            return;
+        
+        Randomness.IsVisible = type == NoiseType.Voronoi;
+        VoronoiFeature.IsVisible = type == NoiseType.Voronoi;
+        AngleOffset.IsVisible = type == NoiseType.Voronoi;
+    }
+}

+ 8 - 1
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShape.cs

@@ -1,9 +1,16 @@
-namespace PixiEditor.Views.Overlays.BrushShapeOverlay;
+using System.ComponentModel;
+
+namespace PixiEditor.Views.Overlays.BrushShapeOverlay;
 internal enum BrushShape
 {
+    [Description("BRUSH_SHAPE_HIDDEN")]
     Hidden,
+    [Description("BRUSH_SHAPE_PIXEL")]
     Pixel,
+    [Description("BRUSH_SHAPE_SQUARE")]
     Square,
+    [Description("BRUSH_SHAPE_CIRCLE_PIXELATED")]
     CirclePixelated,
+    [Description("BRUSH_SHAPE_CIRCLE_SMOOTH")]
     CircleSmooth
 }

+ 3 - 4
src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml

@@ -4,9 +4,8 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:settings="clr-namespace:PixiEditor.ViewModels.Tools.ToolSettings.Settings"
              xmlns:enums="clr-namespace:PixiEditor.ChangeableDocument.Enums;assembly=PixiEditor.ChangeableDocument"
-             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
-             xmlns:helpers="clr-namespace:PixiEditor.Helpers"
              xmlns:localization="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.Views.Tools.ToolSettings.Settings.EnumSettingView">
     <Design.DataContext>
@@ -20,12 +19,12 @@
         <ComboBox.ItemContainerTheme>
             <ControlTheme TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
                 <Setter Property="Tag" Value="{Binding .}"/>
-                <Setter Property="(localization:Translator.Key)" Value="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
+                <Setter Property="(localization:Translator.Key)" Value="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}"/>
             </ControlTheme>
         </ComboBox.ItemContainerTheme>
        <ComboBox.ItemTemplate>
            <DataTemplate>
-               <TextBlock localization:Translator.Key="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
+               <TextBlock localization:Translator.Key="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}"/>
            </DataTemplate>
        </ComboBox.ItemTemplate>
     </ComboBox>