add two data converters

this PR adds the UI to create converters in the editor, and includes the first two:
- a rounder that accepts a property for rounding
- a toString converter that can convert for now numbers and enum. It could be extended to colors and booleans in the future.

https://github.com/user-attachments/assets/2946d8ea-0fdf-4039-83d4-79b5fdfc6169

https://github.com/user-attachments/assets/ec60b523-ecb2-4453-9280-c99822706b2e

Diffs=
2c0927fc5 add two data converters (#7742)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index 16f26cb..c15f5b7 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-8c7ac03294339ae2ad856b541c730dc16cec018d
+2c0927fc5e9a5c420305a81446ac41eb5b896b2c
diff --git a/dev/defs/data_bind/converters/data_converter_rounder.json b/dev/defs/data_bind/converters/data_converter_rounder.json
new file mode 100644
index 0000000..30dfbfd
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_rounder.json
@@ -0,0 +1,20 @@
+{
+  "name": "DataConverterRounder",
+  "key": {
+    "int": 489,
+    "string": "dataconverterrounder"
+  },
+  "extends": "data_bind/converters/data_converter.json",
+  "properties": {
+    "decimals": {
+      "type": "uint",
+      "typeRuntime": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 669,
+        "string": "decimals"
+      },
+      "description": "Number of decimals to round to"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_to_string.json b/dev/defs/data_bind/converters/data_converter_to_string.json
new file mode 100644
index 0000000..5584ce4
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_to_string.json
@@ -0,0 +1,8 @@
+{
+  "name": "DataConverterToString",
+  "key": {
+    "int": 490,
+    "string": "dataconvertertostring"
+  },
+  "extends": "data_bind/converters/data_converter.json"
+}
\ No newline at end of file
diff --git a/dev/defs/node.json b/dev/defs/node.json
index 90ff182..36a1ea3 100644
--- a/dev/defs/node.json
+++ b/dev/defs/node.json
@@ -21,7 +21,8 @@
             "string": "xArtboard"
           }
         ]
-      }
+      },
+      "bindable": true
     },
     "y": {
       "type": "double",
@@ -38,7 +39,8 @@
             "string": "yArtboard"
           }
         ]
-      }
+      },
+      "bindable": true
     },
     "styleValue": {
       "type": "uint",
diff --git a/include/rive/data_bind/converters/data_converter_rounder.hpp b/include/rive/data_bind/converters/data_converter_rounder.hpp
new file mode 100644
index 0000000..063d38c
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_rounder.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_CONVERTER_ROUND_HPP_
+#define _RIVE_DATA_CONVERTER_ROUND_HPP_
+#include "rive/generated/data_bind/converters/data_converter_rounder_base.hpp"
+#include "rive/data_bind/data_values/data_value.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterRounder : public DataConverterRounderBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataType outputType() override { return DataType::number; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_to_string.hpp b/include/rive/data_bind/converters/data_converter_to_string.hpp
new file mode 100644
index 0000000..0757e60
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_to_string.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_DATA_CONVERTER_TO_STRING_HPP_
+#define _RIVE_DATA_CONVERTER_TO_STRING_HPP_
+#include "rive/generated/data_bind/converters/data_converter_to_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterToString : public DataConverterToStringBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataType outputType() override { return DataType::string; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index 3d23e26..d38b451 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -119,6 +119,8 @@
 #include "rive/data_bind/bindable_property_number.hpp"
 #include "rive/data_bind/bindable_property_string.hpp"
 #include "rive/data_bind/converters/data_converter.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
 #include "rive/data_bind/data_bind.hpp"
 #include "rive/data_bind/data_bind_context.hpp"
 #include "rive/draw_rules.hpp"
@@ -451,6 +453,10 @@
                 return new BindablePropertyBoolean();
             case DataBindBase::typeKey:
                 return new DataBind();
+            case DataConverterRounderBase::typeKey:
+                return new DataConverterRounder();
+            case DataConverterToStringBase::typeKey:
+                return new DataConverterToString();
             case DataBindContextBase::typeKey:
                 return new DataBindContext();
             case BindablePropertyStringBase::typeKey:
@@ -1010,6 +1016,9 @@
             case DataBindBase::converterIdPropertyKey:
                 object->as<DataBindBase>()->converterId(value);
                 break;
+            case DataConverterRounderBase::decimalsPropertyKey:
+                object->as<DataConverterRounderBase>()->decimals(value);
+                break;
             case BindablePropertyEnumBase::propertyValuePropertyKey:
                 object->as<BindablePropertyEnumBase>()->propertyValue(value);
                 break;
@@ -2034,6 +2043,8 @@
                 return object->as<DataBindBase>()->flags();
             case DataBindBase::converterIdPropertyKey:
                 return object->as<DataBindBase>()->converterId();
+            case DataConverterRounderBase::decimalsPropertyKey:
+                return object->as<DataConverterRounderBase>()->decimals();
             case BindablePropertyEnumBase::propertyValuePropertyKey:
                 return object->as<BindablePropertyEnumBase>()->propertyValue();
             case NestedArtboardLeafBase::fitPropertyKey:
@@ -2663,6 +2674,7 @@
             case DataBindBase::propertyKeyPropertyKey:
             case DataBindBase::flagsPropertyKey:
             case DataBindBase::converterIdPropertyKey:
+            case DataConverterRounderBase::decimalsPropertyKey:
             case BindablePropertyEnumBase::propertyValuePropertyKey:
             case NestedArtboardLeafBase::fitPropertyKey:
             case WeightBase::valuesPropertyKey:
@@ -3239,6 +3251,8 @@
                 return object->is<DataBindBase>();
             case DataBindBase::converterIdPropertyKey:
                 return object->is<DataBindBase>();
+            case DataConverterRounderBase::decimalsPropertyKey:
+                return object->is<DataConverterRounderBase>();
             case BindablePropertyEnumBase::propertyValuePropertyKey:
                 return object->is<BindablePropertyEnumBase>();
             case NestedArtboardLeafBase::fitPropertyKey:
diff --git a/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp b/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp
new file mode 100644
index 0000000..b1098fb
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_DATA_CONVERTER_ROUNDER_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_ROUNDER_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterRounderBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 489;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterRounderBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t decimalsPropertyKey = 669;
+
+private:
+    uint32_t m_Decimals = 0;
+
+public:
+    inline uint32_t decimals() const { return m_Decimals; }
+    void decimals(uint32_t value)
+    {
+        if (m_Decimals == value)
+        {
+            return;
+        }
+        m_Decimals = value;
+        decimalsChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataConverterRounderBase& object)
+    {
+        m_Decimals = object.m_Decimals;
+        DataConverter::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case decimalsPropertyKey:
+                m_Decimals = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return DataConverter::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void decimalsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp b/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp
new file mode 100644
index 0000000..c919cba
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_DATA_CONVERTER_TO_STRING_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_TO_STRING_BASE_HPP_
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterToStringBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 490;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterToStringBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_rounder.cpp b/src/data_bind/converters/data_converter_rounder.cpp
new file mode 100644
index 0000000..86422ff
--- /dev/null
+++ b/src/data_bind/converters/data_converter_rounder.cpp
@@ -0,0 +1,19 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+
+using namespace rive;
+
+DataValue* DataConverterRounder::convert(DataValue* input)
+{
+    auto output = new DataValueNumber();
+    if (input->is<DataValueNumber>())
+    {
+        float value = input->as<DataValueNumber>()->value();
+        auto numberOfPlaces = decimals();
+        // TODO: @hernan review this way of rounding
+        float rounder = pow(10.0f, (float)numberOfPlaces);
+        output->value(std::round(value * rounder) / rounder);
+    }
+    return output;
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_to_string.cpp b/src/data_bind/converters/data_converter_to_string.cpp
new file mode 100644
index 0000000..6c62f38
--- /dev/null
+++ b/src/data_bind/converters/data_converter_to_string.cpp
@@ -0,0 +1,36 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+#include "rive/data_bind/data_values/data_value_enum.hpp"
+#include "rive/data_bind/data_values/data_value_string.hpp"
+
+using namespace rive;
+
+DataValue* DataConverterToString::convert(DataValue* input)
+{
+    auto output = new DataValueString();
+    if (input->is<DataValueNumber>())
+    {
+        float value = input->as<DataValueNumber>()->value();
+        std::string str = std::to_string(value);
+        if (str.find('.') != std::string::npos)
+        {
+            // Remove trailing zeroes
+            str = str.substr(0, str.find_last_not_of('0') + 1);
+            // If the decimal point is now the last character, remove that as well
+            if (str.find('.') == str.size() - 1)
+            {
+                str = str.substr(0, str.size() - 1);
+            }
+        }
+        output->value(str);
+    }
+    else if (input->is<DataValueEnum>())
+    {
+        auto dataEnum = input->as<DataValueEnum>()->dataEnum();
+        auto index = input->as<DataValueEnum>()->value();
+        auto enumValue = dataEnum->value(index);
+        output->value(enumValue);
+    }
+    return output;
+}
\ No newline at end of file
diff --git a/src/generated/data_bind/converters/data_converter_rounder_base.cpp b/src/generated/data_bind/converters/data_converter_rounder_base.cpp
new file mode 100644
index 0000000..e6a03a3
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_rounder_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_rounder_base.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+
+using namespace rive;
+
+Core* DataConverterRounderBase::clone() const
+{
+    auto cloned = new DataConverterRounder();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_to_string_base.cpp b/src/generated/data_bind/converters/data_converter_to_string_base.cpp
new file mode 100644
index 0000000..c600a8d
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_to_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_to_string_base.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
+
+using namespace rive;
+
+Core* DataConverterToStringBase::clone() const
+{
+    auto cloned = new DataConverterToString();
+    cloned->copy(*this);
+    return cloned;
+}