Improve NodeNamer readability. (#211)

* Improve NodeNamer readability.

This is a follow-up from a previous PR where there were some questions about the NodeNamer, so I've refactored it to take advantage of C# switch expressions and added better comments.
Also move LottieGen to .NET Core 3.1 (was 2.2). This means we can use newer features, and we get an actual exe rather than having to use the "dotnet" bootstrapper.

* Fix typo found in CR.

* CR feedback.

* Fix issue found in testing.

* Another issue found in testing.

* Fix comment.
This commit is contained in:
Simeon 2020-01-10 14:01:09 -08:00 коммит произвёл GitHub
Родитель ce1e7f194c
Коммит 9f14a5e9c3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 177 добавлений и 188 удалений

Просмотреть файл

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>LottieGen</ToolCommandName>

Просмотреть файл

@ -2,7 +2,7 @@
LottieGen is a tool for generating C#, C++, and other outputs from Lottie / Bodymovin JSON files. LottieGen is built as a [.NET Core global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools), which means it will run anywhere .NET Core is available, including Linux and Mac.
LottieGen requires [.NET Core 2.2 SDK](https://dotnet.microsoft.com/download/dotnet-core/2.2) or later.
LottieGen requires [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1) or later.
## Installing
*The following commands are examples only; adjust the paths and versions as necessary.*
@ -13,19 +13,19 @@ The latest release version can be [installed from NuGet](https://www.nuget.org/p
A specific version can be installed from NuGet:
dotnet tool install -g LottieGen --version 6.0.0
dotnet tool install -g LottieGen --version 6.1.0
CI builds can be [installed from MyGet](https://dotnet.myget.org/feed/uwpcommunitytoolkit/package/nuget/LottieGen):
dotnet tool install -g LottieGen --add-source https://dotnet.myget.org/F/uwpcommunitytoolkit/api/v3/index.json --version 6.1.0-build.3
dotnet tool install -g LottieGen --add-source https://dotnet.myget.org/F/uwpcommunitytoolkit/api/v3/index.json --version 6.1.0-build.19
Local builds can be installed from your bin\nupkg directory:
dotnet tool install -g LottieGen --add-source f:\GitHub\Lottie-Windows\bin\nupkg --version 6.1.0-build.11.g31523b44e4
dotnet tool install -g LottieGen --add-source f:\GitHub\Lottie-Windows\bin\nupkg --version 6.1.0-build.18.g31523b44e4
Local builds can be run directly:
dotnet f:\GitHub\Lottie-Windows\LottieGen\bin\AnyCpu\Debug\netcoreapp2.2\lottiegen.dll
f:\GitHub\Lottie-Windows\LottieGen\bin\AnyCpu\Debug\netcoreapp3.1\lottiegen.exe
## Updating
dotnet tool update -g LottieGen

Просмотреть файл

@ -91,23 +91,155 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.CodeGen
}
}
static string AppendDescription(string baseName, string description)
=> baseName + (string.IsNullOrWhiteSpace(description) ? string.Empty : $"_{description}");
// Returns the value from the given keyframe, or null.
static T? ValueFromKeyFrame<T>(KeyFrameAnimation<T>.KeyFrame kf)
where T : struct
// Returns a description of the given CompositionObject, suitable for use as an identifier.
static string DescribeCompositionObject(TNode node, CompositionObject obj)
{
return kf is KeyFrameAnimation<T>.ValueKeyFrame valueKf ? (T?)valueKf.Value : null;
var result = obj.Type switch
{
// For some animations, we can include a description of the start and end values
// to make the names more descriptive.
CompositionObjectType.ColorKeyFrameAnimation
=> AppendDescription("ColorAnimation", DescribeAnimationRange((ColorKeyFrameAnimation)obj)),
CompositionObjectType.ScalarKeyFrameAnimation
=> AppendDescription($"{TryGetAnimatedPropertyName(node)}ScalarAnimation", DescribeAnimationRange((ScalarKeyFrameAnimation)obj)),
// Do not include descriptions of the animation range for vectors - the names
// end up being very long, complicated, and confusing to the reader.
CompositionObjectType.Vector2KeyFrameAnimation => $"{TryGetAnimatedPropertyName(node)}Vector2Animation",
CompositionObjectType.Vector3KeyFrameAnimation => $"{TryGetAnimatedPropertyName(node)}Vector3Animation",
CompositionObjectType.Vector4KeyFrameAnimation => $"{TryGetAnimatedPropertyName(node)}Vector4Animation",
// Geometries include their size as part of the description.
CompositionObjectType.CompositionRectangleGeometry
=> AppendDescription("Rectangle", Vector2AsId(((CompositionRectangleGeometry)obj).Size)),
CompositionObjectType.CompositionRoundedRectangleGeometry
=> AppendDescription("RoundedRectangle", Vector2AsId(((CompositionRoundedRectangleGeometry)obj).Size)),
CompositionObjectType.CompositionEllipseGeometry
=> AppendDescription("Ellipse", Vector2AsId(((CompositionEllipseGeometry)obj).Radius)),
CompositionObjectType.ExpressionAnimation => DescribeExpressionAnimation((ExpressionAnimation)obj),
CompositionObjectType.CompositionColorBrush => DescribeCompositionColorBrush((CompositionColorBrush)obj),
CompositionObjectType.CompositionColorGradientStop => DescribeCompositionColorGradientStop((CompositionColorGradientStop)obj),
CompositionObjectType.StepEasingFunction => DescribeStepEasingFunction((StepEasingFunction)obj),
// All other cases, just ToString() the type name.
_ => obj.Type.ToString(),
};
// Remove the "Composition" prefix so the name is easier to read.
// The prefix is redundant as far as the reader is concerned because most of the
// objects have it and it doesn't indicate anything useful to the reader.
return StripPrefix(result, "Composition");
}
static (T? First, T? Last) FirstAndLastValuesFromKeyFrame<T>(KeyFrameAnimation<T> animation)
where T : struct
static string DescribeCompositionColorBrush(CompositionColorBrush obj)
{
// If there's only one keyframe, return it as the last value and leave the first value null.
var first = animation.KeyFrameCount > 1 ? ValueFromKeyFrame(animation.KeyFrames.First()) : null;
var last = ValueFromKeyFrame(animation.KeyFrames.Last());
return (first, last);
// Color brushes that are not animated get names describing their color.
// Optimization ensures there will only be one brush for any one non-animated color.
if (obj.Animators.Count > 0)
{
// Brush is animated. Give it a name based on the colors in the animation.
var colorAnimation = obj.Animators.Where(a => a.AnimatedProperty == "Color").First().Animation;
if (colorAnimation is ColorKeyFrameAnimation colorKeyFrameAnimation)
{
return AppendDescription("AnimatedColorBrush", DescribeAnimationRange(colorKeyFrameAnimation));
}
else
{
// The color is bound to a property set.
var objectName = ((IDescribable)obj).Name;
return string.IsNullOrWhiteSpace(objectName)
? "BoundColorBrush"
: $"{objectName}ColorBrush";
}
}
else
{
// Brush is not animated. Give it a name based on the color.
return AppendDescription("ColorBrush", obj.Color?.Name);
}
}
static string DescribeCompositionColorGradientStop(CompositionColorGradientStop obj)
{
if (obj.Animators.Count > 0)
{
// Gradient stop is animated. Give it a name based on the colors in the animation.
var colorAnimation = obj.Animators.Where(a => a.AnimatedProperty == "Color").First().Animation;
if (colorAnimation is ColorKeyFrameAnimation colorKeyFrameAnimation)
{
return AppendDescription("AnimatedGradientStop", DescribeAnimationRange(colorKeyFrameAnimation));
}
else
{
// The color is bound to an expression.
return "BoundColorStop";
}
}
else
{
// Gradient stop is not animated. Give it a name based on the color.
return AppendDescription("GradientStop", obj.Color.Name);
}
}
static string DescribeExpressionAnimation(ExpressionAnimation obj)
{
var expression = obj.Expression;
var expressionType = expression.InferredType;
return expressionType.IsValid && !expressionType.IsGeneric
? $"{expressionType.Constraints}ExpressionAnimation"
: "ExpressionAnimation";
}
static string DescribeStepEasingFunction(StepEasingFunction obj)
{
// Recognize 2 common patterns: HoldThenStep and StepThenHold
if (obj.StepCount == 1)
{
if (obj.IsFinalStepSingleFrame && !obj.IsInitialStepSingleFrame)
{
return "HoldThenStepEasingFunction";
}
else if (obj.IsInitialStepSingleFrame && !obj.IsFinalStepSingleFrame)
{
return "StepThenHoldEasingFunction";
}
}
// Didn't recognize the pattern.
return "EasingFunction";
}
static string DescribeLoadedImageSurface(TNode node, LoadedImageSurface obj)
{
string result = null;
switch (obj.Type)
{
case LoadedImageSurface.LoadedImageSurfaceType.FromStream:
result = "ImageFromStream";
break;
case LoadedImageSurface.LoadedImageSurfaceType.FromUri:
var loadedImageSurfaceFromUri = (LoadedImageSurfaceFromUri)obj;
// Get the image file name only.
var imageFileName = loadedImageSurfaceFromUri.Uri.Segments.Last();
var imageFileNameWithoutExtension = imageFileName.Substring(0, imageFileName.LastIndexOf('.'));
// Replace any disallowed character with underscores.
var cleanedImageName = new string((from ch in imageFileNameWithoutExtension
select char.IsLetterOrDigit(ch) ? ch : '_').ToArray());
// Remove any duplicated underscores.
cleanedImageName = cleanedImageName.Replace("__", "_");
result = AppendDescription("Image", cleanedImageName);
break;
default:
throw new InvalidOperationException();
}
return result;
}
// Returns a string for use in an identifier that describes a ColorKeyFrameAnimation, or null
@ -131,6 +263,20 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.CodeGen
: null;
}
// Returns the value from the given keyframe, or null.
static T? ValueFromKeyFrame<T>(KeyFrameAnimation<T>.KeyFrame kf)
where T : struct
=> kf is KeyFrameAnimation<T>.ValueKeyFrame valueKf ? (T?)valueKf.Value : null;
static (T? First, T? Last) FirstAndLastValuesFromKeyFrame<T>(KeyFrameAnimation<T> animation)
where T : struct
{
// If there's only one keyframe, return it as the last value and leave the first value null.
var first = animation.KeyFrameCount > 1 ? ValueFromKeyFrame(animation.KeyFrames.First()) : null;
var last = ValueFromKeyFrame(animation.KeyFrames.Last());
return (first, last);
}
static string TryGetAnimatedPropertyName(TNode node)
{
// Find the property name that references this animation.
@ -144,181 +290,24 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.UIData.CodeGen
return animators.Length == 1 ? SanitizePropertyName(animators[0]) : null;
}
static string SanitizePropertyName(string propertyName) =>
propertyName?.Replace(".", string.Empty);
static string AppendDescription(string baseName, string description)
=> baseName + (string.IsNullOrWhiteSpace(description) ? string.Empty : $"_{description}");
static string DescribeCompositionObject(TNode node, CompositionObject obj)
{
string result = null;
switch (obj.Type)
{
case CompositionObjectType.ColorKeyFrameAnimation:
result = AppendDescription("ColorAnimation", DescribeAnimationRange((ColorKeyFrameAnimation)obj));
break;
case CompositionObjectType.ScalarKeyFrameAnimation:
result = AppendDescription($"{TryGetAnimatedPropertyName(node)}ScalarAnimation", DescribeAnimationRange((ScalarKeyFrameAnimation)obj));
break;
case CompositionObjectType.Vector2KeyFrameAnimation:
result = $"{TryGetAnimatedPropertyName(node)}Vector2Animation";
break;
case CompositionObjectType.Vector3KeyFrameAnimation:
result = $"{TryGetAnimatedPropertyName(node)}Vector3Animation";
break;
case CompositionObjectType.Vector4KeyFrameAnimation:
result = $"{TryGetAnimatedPropertyName(node)}Vector4Animation";
break;
case CompositionObjectType.CompositionColorBrush:
// Color brushes that are not animated get names describing their color.
// Optimization ensures there will only be one brush for any one non-animated color.
var brush = (CompositionColorBrush)obj;
if (brush.Animators.Count > 0)
{
// Brush is animated. Give it a name based on the colors in the animation.
var colorAnimation = brush.Animators.Where(a => a.AnimatedProperty == "Color").First().Animation;
if (colorAnimation is ColorKeyFrameAnimation colorKeyFrameAnimation)
{
result = AppendDescription("AnimatedColorBrush", DescribeAnimationRange(colorKeyFrameAnimation));
}
else
{
// The color is bound to a property set.
var objectName = ((IDescribable)brush).Name;
static string SanitizePropertyName(string propertyName)
=> propertyName?.Replace(".", string.Empty);
result = string.IsNullOrWhiteSpace(objectName)
? "BoundColorBrush"
: $"{objectName}ColorBrush";
}
}
else
{
// Brush is not animated. Give it a name based on the color.
result = AppendDescription("ColorBrush", brush.Color?.Name);
}
break;
case CompositionObjectType.CompositionColorGradientStop:
var stop = (CompositionColorGradientStop)obj;
if (stop.Animators.Count > 0)
{
// Brush is animated. Give it a name based on the colors in the animation.
var colorAnimation = stop.Animators.Where(a => a.AnimatedProperty == "Color").First().Animation;
if (colorAnimation is ColorKeyFrameAnimation colorKeyFrameAnimation)
{
result = AppendDescription("AnimatedGradientStop", DescribeAnimationRange(colorKeyFrameAnimation));
}
else
{
// The color is bound to an expression.
result = "BoundColorStop";
}
}
else
{
// Brush is not animated. Give it a name based on the color.
result = AppendDescription("GradientStop", stop.Color.Name);
}
break;
case CompositionObjectType.CompositionRectangleGeometry:
var rectangle = (CompositionRectangleGeometry)obj;
result = AppendDescription("Rectangle", Vector2AsId(rectangle.Size));
break;
case CompositionObjectType.CompositionRoundedRectangleGeometry:
var roundedRectangle = (CompositionRoundedRectangleGeometry)obj;
result = AppendDescription("RoundedRectangle", Vector2AsId(roundedRectangle.Size));
break;
case CompositionObjectType.CompositionEllipseGeometry:
var ellipse = (CompositionEllipseGeometry)obj;
result = AppendDescription("Ellipse", Vector2AsId(ellipse.Radius));
break;
case CompositionObjectType.ExpressionAnimation:
var expressionAnimation = (ExpressionAnimation)obj;
var expression = expressionAnimation.Expression;
var expressionType = expression.InferredType;
if (expressionType.IsValid && !expressionType.IsGeneric)
{
result = $"{expressionType.Constraints.ToString()}ExpressionAnimation";
}
else
{
result = "ExpressionAnimation";
}
break;
case CompositionObjectType.StepEasingFunction:
// Recognize 2 common patterns: HoldThenStep and StepThenHold
var stepEasingFunction = (StepEasingFunction)obj;
if (stepEasingFunction.StepCount == 1 && stepEasingFunction.IsFinalStepSingleFrame && !stepEasingFunction.IsInitialStepSingleFrame)
{
result = "HoldThenStepEasingFunction";
}
else if (stepEasingFunction.StepCount == 1 && stepEasingFunction.IsInitialStepSingleFrame && !stepEasingFunction.IsFinalStepSingleFrame)
{
result = "StepThenHoldEasingFunction";
}
else
{
// Didn't recognize the pattern.
goto default;
}
break;
default:
result = obj.Type.ToString();
break;
}
// Remove the "Composition" prefix so the name is easier to read.
const string compositionPrefix = "Composition";
if (result.StartsWith(compositionPrefix))
{
result = result.Substring(compositionPrefix.Length);
}
return result;
}
static string DescribeLoadedImageSurface(TNode node, LoadedImageSurface obj)
{
string result = null;
var loadedImageSurface = (LoadedImageSurface)node.Object;
switch (loadedImageSurface.Type)
{
case LoadedImageSurface.LoadedImageSurfaceType.FromStream:
result = "ImageFromStream";
break;
case LoadedImageSurface.LoadedImageSurfaceType.FromUri:
var loadedImageSurfaceFromUri = (LoadedImageSurfaceFromUri)loadedImageSurface;
// Get the image file name only.
var imageFileName = loadedImageSurfaceFromUri.Uri.Segments.Last();
var imageFileNameWithoutExtension = imageFileName.Substring(0, imageFileName.LastIndexOf('.'));
// Replace any disallowed character with underscores.
var cleanedImageName = new string((from ch in imageFileNameWithoutExtension
select char.IsLetterOrDigit(ch) ? ch : '_').ToArray());
// Remove any duplicated underscores.
cleanedImageName = cleanedImageName.Replace("__", "_");
result = AppendDescription("Image", cleanedImageName);
break;
default:
throw new InvalidOperationException();
}
return result;
}
// Removes the given prefix from a name.
static string StripPrefix(string name, string prefix)
=> name.StartsWith(prefix)
? name.Substring(prefix.Length)
: name;
// A float for use in an id.
static string FloatAsId(float value) => value.ToString("0.###", CultureInfo.InvariantCulture).Replace('.', 'p').Replace('-', 'm');
static string FloatAsId(float value)
=> value.ToString("0.###", CultureInfo.InvariantCulture).Replace('.', 'p').Replace('-', 'm');
// A Vector2 for use in an id.
static string Vector2AsId(Vector2 size)
{
return size.X == size.Y
? FloatAsId(size.X)
: $"{FloatAsId(size.X)}x{FloatAsId(size.Y)}";
}
=> size.X == size.Y ? FloatAsId(size.X) : $"{FloatAsId(size.X)}x{FloatAsId(size.Y)}";
}
}