Sunday 10 August 2014

Implicit Converters

I already spoke a bit about converting types.

Since in FlareTic all the "dataflow patch types" use some form of generic holder for pins, link is done simply as:

Check if input type is assignable from output type, if yes allow link.
Now this creates some issues as for example float is not assignable to bool.

So I don't want to create AsBool (Float) , AsFloat(Bool) and so on for all types, that makes a patch bloated for nothing, I need another solution.

Enters converters

Code Snippet
  1. public interface ITypeConverterFactory
  2. {
  3.     bool CanConvert(Type from, Type to);
  4.     ITypeConverter CreateConverter();
  5. }
  6.  
  7. public interface ITypeConverter
  8. {
  9.     object Convert(object from);
  10.     bool Warning { get; }
  11.     string WarnMessage { get; }
  12. }

The first interface allows to check if we can convert types, and when we connect creates the converter that does the job.

Now when we check for connection, we have a list of factories, so if we find a converter, we can link :)

For raw .NET primitive types, you have to build by yourself, so to speed this up, few generic builders:

Code Snippet
  1. public class FuncConverter<TFrom, TTo> : ITypeConverter
  2. {
  3.     private readonly Func<TFrom, TTo> convertFunc;
  4.     private readonly bool warning;
  5.     private readonly string message;
  6.  
  7.     public FuncConverter(Func<TFrom, TTo> func, bool warning, string message)
  8.     {
  9.         this.convertFunc = func;
  10.         this.warning = warning;
  11.         this.message = message;
  12.     }
  13.  
  14.     public object Convert(object from)
  15.     {
  16.         return this.convertFunc((TFrom)from);
  17.     }
  18.  
  19.     public bool Warning
  20.     {
  21.         get { return this.warning; }
  22.     }
  23.  
  24.     public string WarnMessage
  25.     {
  26.         get { return this.message; }
  27.     }
  28. }

And the factory :

Code Snippet
  1. public abstract class FuncConverterFactory<TFrom, TTo> : ITypeConverterFactory
  2. {
  3.     private readonly Func<TFrom, TTo> convertFunc;
  4.     private readonly bool warn;
  5.     private readonly string msg;
  6.  
  7.     public FuncConverterFactory(Func<TFrom, TTo> func, bool warning = false, string message = "")
  8.     {
  9.         this.convertFunc = func;
  10.         this.warn = warning;
  11.         this.msg = message;
  12.     }
  13.  
  14.     public bool CanConvert(Type from, Type to)
  15.     {
  16.         return from == typeof(TFrom) && to == typeof(TTo);
  17.     }
  18.  
  19.     public ITypeConverter CreateConverter()
  20.     {
  21.         return new FuncConverter<TFrom, TTo>(this.convertFunc, this.warn, this.msg);
  22.     }
  23. }


Next bit of boilerplate, could have done several ways, but you only do it once ;)

Code Snippet
  1. public class IntToFloatConverter : FuncConverterFactory<int, float>
  2. {
  3.     public IntToFloatConverter() : base(i => i) { }
  4. }
  5.  
  6. public class FloatToIntConverter : FuncConverterFactory<float, int>
  7. {
  8.     public FloatToIntConverter() : base(f => (int)f, true, "Conversion from float to int is explicit, some data can be lost" ) { }
  9. }

You can notice that explicit casting also induce a message, so we can warn user in some cases.

Now for all other types, we go reflect implicit/explicit table for other types:

Code Snippet
  1. public class ImplicitReflectionConverterFactory : ITypeConverterFactory
  2. {
  3.     public bool CanConvert(Type from, Type to)
  4.     {
  5.         var f = from.GetMethods().Where(
  6.             m => m.Name == "op_Implicit"
  7.             && m.ReturnType == to
  8.             && m.GetParameters().Count() == 1
  9.             && m.GetParameters()[0].ParameterType == from).FirstOrDefault() != null;
  10.  
  11.         if (f) { return true; }
  12.  
  13.         f = to.GetMethods().Where(
  14.             m => m.Name == "op_Implicit"
  15.             && m.ReturnType == to
  16.             && m.GetParameters().Count() == 1
  17.             && m.GetParameters()[0].ParameterType == from).FirstOrDefault() != null;
  18.  
  19.         return f;
  20.     }
  21.  
  22.     public ITypeConverter CreateConverter()
  23.     {
  24.         return new ImplicitReflectionConverter();
  25.     }
  26. }

Now let's go a bit forward, why not also being able to auto convert opencv image to dx11 texture, that's why you use a converter factory, since now converter will also hold a resource, so we need one instance per link.

Converter code is just boilerplate, create texture for the same format as opencv image, and copy content in gpu when possible.


Here we go, of course user interface must reflect that we are using a converter, so blue mean converter, yellow means converter with warning.

Now opencv image can be directly linked to any node that wants a texture, and you can notice sharpdx types can be auto translated (Color4 has a Vector3 explicit operator, so it's automatically scanned by the reflector).

I love clean patches ;)

Now one common issue, we might want some form of control over the conversion, for example, float to string, we'd like to specify a format. So let's push the concept forward and make our link configurable via inspector.

Easy then, let's add a small additional interface:

Code Snippet
  1. public interface IConfigurableTypeConverter : ITypeConverter
  2. {
  3.     IEnumerable<IParameter> Parameters { get; }
  4. }

And here we go:

Code Snippet
  1. public class StringFormatConverter : IConfigurableTypeConverter
  2. {
  3.     private StringParameter format;
  4.  
  5.     public StringFormatConverter()
  6.     {
  7.         this.format = new StringParameter(new StringParameterConverter());
  8.         this.format.Value = "f";
  9.         this.format.Name = "Format";
  10.     }
  11.  
  12.     public object Convert(object from)
  13.     {          
  14.        return String.Format("{0:" + format.Value + "}", from);
  15.     }
  16.  
  17.     public bool Warning
  18.     {
  19.         get { return false; }
  20.     }
  21.  
  22.     public string WarnMessage
  23.     {
  24.         get { return ""; }
  25.     }
  26.  
  27.     public IEnumerable<IParameter> Parameters
  28.     {
  29.         get
  30.         {
  31.             yield return this.format;
  32.         }
  33.     }
  34. }

Now when we select a link in the editor, if it implements this interface we can manage properties, as per the screenshot:



So far it's great, using the reflection from c# helped to speed up the process a lot.

On next stage, we have the obvious question/problem, what happens if we have multiple converters that allow similar types?

Here we have several choices:
  • Take the first one
  • Show a selector when we complete link
  • Have the selector in the inspector instead
I haven't made a choice, but take first one + selector is the most sensible option for now.






No comments:

Post a Comment