-
Notifications
You must be signed in to change notification settings - Fork 61
Refactored AffineTransform towards System.Numerics.Matrix3x2 #189
Conversation
…merics.Matrix3x2, System.Windows.Media.Matrix and so on. Added Translation, Rotation and Scaling properties to AffineTransform. Added UnitTests for the related changes.
@vpenades I like your suggestion about changing the method names to be "ConcatenateScale", etc... Would you open to making those changes as well as part of your PR? Regarding why ScalingCanvas needs to track the scale, it's because in Android and Skia for some drawing operations if you scale and the draw some of the vector shapes, you end up with pixelated results. By tracking the scale, we can simulate what CoreGraphics does and do the scaling ourself so that we get better visual results. Great idea on switching to using a Span. This library pre-dates the existence of those classes. I'd welcome that change. |
Sure, I'll do the changes
Ok, I understand, I hope the "AverageScale" property works for these case scenarios.
Also, I've seen the method GetMatrix(float[] ) which is also misleading... does it mean getting the data into the AffineTransform? I would change it to CopyTo(span ...); |
@vpenades CopyTo(span) is a much better name. It's intent is to get the values from the Matrix/Transform copied into an existing array of floats. |
Sadly, adding System.Memory to allow supporting Span and ReadonlySpan was unsuccessful, because apparently Tizen40 is failing to compile. Maybe it's related to my dev environment. So I'll skip this one until someone adds System.Memory nuget package |
…hods Remove reduntant SetMatrix methods
I've done with renaming the methods agreed. let me know if you see something else that can be improved. |
Thanks. I'll look at the PR tonight. Really appreciate all of your contributions!!! |
Would you consider making AffineTransform inmutable? people are used to deal with matrices "by value" and not "by ref". By being a class you have some advantages, but class+mutable is quite dangerous to say the least. Consider this example: var t = AffineTransform.GetScaleInstance(5,5);
var state1 = new CanvasState();
state1.Transform = t;
t.RotateInDegrees(90);
var state2 = new CanvasState();
state2.Transform = t; In this case, one can believe that state1 has a Scale(5,5) and state2 has a Scale(5,5)xRotate(90) , but in fact, since the reference used in state1.Transform is dragged on, the following RotateInDegrees operation is still affecting state1.Transform. To avoid this kind of scenarios, the best approach is to make AffineTransform inmutable, so every operation will produce a new copy. |
I think this PR doesn't go far enough as I can't see the point of this class as .NET already has Matrix3x2, and soon some generic versions as well, which have been through a full api review, see dotnet/runtime#24168. If this project adopted Matrix3x2 it would benefit from the work already done on SIMD h/w acceleration and make code sharing across .NET libraries easier. Introducing another overlapping class just confuses developers. As this project is still experimental, now is the time to make such changes to put it in a stronger position for the long-term. I say this as I want Microsoft.Maui.Graphics to thrive! |
I completely agree with you, but @jonlipsky has the last word on this. So if he agrees on moving to Matrix3x2, I'll be happy to do the changes and add appropiate extensions to preserve the key funcionality of AffineTransform into Matrix3x2. If not, I'll try to make AffineTransform as friendly as possible to those used to Matrix3x2. My preferences would be (in order from most desired to least desired)
|
@vpenades @sparkie108 I have been thinking along the same lines. The AffineTransform class served it's purpose 11 years ago when System.Numerics didn't exist; however, now that we have a cross platform library that serves that need, I think we can drop it. My main concern would be keeping the helper methods available in AffineTransform so that users don't have to reimplement those themselves. @vpenades I like your proposed solution of moving to Matrix3x2 with extension methods to augment it. Thoughts? |
The thing is... the issue is not with AffineTransform itself, but with the rest of the APIs , that is the fact that the rest of the APIs enforce you to use AffineTransform. My idea is to replace AffineTransform in the APIs, so whoever wants to use Matrix3x2 it can do so. But I also want to keep AffineTransform and much of its functionality around, so it can be used as a Matrix3x2 factory of sorts. Than, and some additional extensions (if needed) will do the trick. Also, I would like to add Transform(matrix3x2) methods to PointF ... it needs to be done this way because matrix3x2 is closed. So if you all agree, I'll be working on it |
@vpenades I agree with your approach, which is what I was trying to express initially; however, perhaps I wasn't clear enough. I look forward to reviewing your changes. This should be a significant improvement! |
Okey, I've did the replacement. Now, all the APIs that were using AffineTransform now use Matrix3x2 Now AffineTransform could be completely removed, or repurposed in this way: public readonly struct AffineTransform
{
private readonly Matrix3x2 _Matrix;
// move here some of the new extension methods & keep those AffineTransform methods that can be useful.
} My intention would not be writing a full fledged replacement of Matrix3x2, just the stuff that's missing from matrix3x4, stuff like:
About the Concatenate* methods: I would either remove them, or reverse the multiplication order to match the one of Matrix3x2. In any case, AffineTransform is isolated from the rest of the code now, so it's easy to refactor it at will, |
I vote for extension methods on Matrix3x2 and remove AffineTransform entirely. If you want to calculate the "average" scale, then the maths is here: The correct extension for Matrix3x2 would be: public static float AverageScale(this Matrix3x2 matrix) => MathF.Sqrt(matrix.M11 * matrix.M11 + matrix.M21 * matrix.M21); |
I'm hesitant about using extensions all the way because two reasons:
So my gut tells me that the safe approach is to move the extension methods to AffineTransform as static methods (but not extensions), so
I've ran some unit tests, and I don't think The idea of average scale is that if you have a scale of 1 over the x axis and a scale of 2 over the y axis, the "average" scale would be 1.5f ... and that should also stand even after rotating the matrix. you could also define it in terms of how much the area of an object will grow or shrink based on the transform, irrespective of any rotation applied, and also taking into account non orthogonal scaling. In other words, an "average scale of a matrix" must meet this test: var scale1 = Matrix3x2.CreateScale( 2, 3 ).GetAverageScale();
var scale2 = (Matrix3x2.CreateScale( 2, 3 ) * Matrix3x2.CreateRotation(1) ).GetAverageScale();
Assert.Equal( scale1, scale2 ); But I think a better way would be to calculate the area growth based on the scale, and from there the length. And as it happens, the determinant of a 2D Matrix is also the (signed) area of a matrix. In other words: the absolute so this: public static float GetAverageScale(this in Matrix3x2 matrix)
{
var s = matrix.GetScale();
return (Math.Abs(s.X) + Math.Abs(s.Y)) / 2;
} would become this: public static float GetLengthScale(this in Matrix3x2 matrix)
{
var area = Math.Abs(matrix.GetDeterminant());
return (float)Math.Sqrt(area);
} This solution works for all kinds of matrices, even skewed ones. |
I've also noticed this on public class CanvasState
{
public float Scale { get; set; } = 1;
public Matrix3x2 Transform { get; set; }
} to: public class CanvasState
{
private float transformScale = 1;
private Matrix3x2 transform = Matrix3x2.Identity;
public float TransformScale => this.scale
public Matrix3x2 Transform
{
get => this.transform;
set
{
this.transform = value;
this.scale = value.GetLengthScale();
}
}
} This ensures that transform and scale are always linked. Also, if CanvasState is disposable, it should implement proper dispose pattern. |
Agreed sqrt of determinant is a more general solution. Apologies I missed the Matrix3x2Extensions class (there's a lot of files changed!). I think that's all that is required and AffineTransform can be dropped, it's just another way of doing the same thing. |
…t need to do it anymore.
I almost agree that AffineTransform can be completely removed, With the introduction of Matrix3x2, it's no longer being used anywere. Also, with the lastest commit, the only important methods (the scaling, construction and deconstruction) can be moved into CanvasState directly, so we could also avoid the risk of future method collision, and we could have the transform operations where it matters; it's important to keep the API surface to a minimum |
@jonlipsky @sparkie108 Trying to move this forward: The only thing's left is to decide what to do with AffineTransform, now that it's no longer being used by the rest of the APIs. Choices are:
My feeling is that Matrix3x2 is a very complete API and there's plenty of libraries that add addional stuff to it; I don't think MAUI is the right library for adding stuff to the System.Numerics namespace. (*) The idea of defining AffineTransform as S.R.T would make it look like this: struct AffineTransform
{
public Vector2 Scale;
public float Rotation;
public Vector2 Translation;
} But again, I feel like I'm extending System.Numerics in the wrong place. |
@vpenades I would vote for removing it completely and we'll see if we get requests to add it back. I agree with you that in many ways we're extending System.Numerics in the wrong place. For my own personal application, I do need some of those methods; however, I'll just add them in as extension methods within my own codebase that extend Matrix3x2. |
@jonlipsky Okey, I've removed AffineTransform, I've kept the extensions but I've made the extensions internal to the library. I've had to add a static I think this PR is ready to review, as it's completed its main purpose to replace AffineTransform with Matrix3x2. There's some design issues I've noticed, that I would leave for another PR: it's the relationship between ICanvas, CanvasState, and the Native transform methods: Right now, modifying the native transforms doesn't reflect on the CanvasState base class, which can be an issue; I think it's possible to reverse, and letting CanvasState base class control the transform of the derived classes via the |
I think this has ended up with a very good solution, @vpenades thanks for the great work! |
Considerations:
ICanvas.ConcatenateTransform(AffineTransform transform);
withICanvas.ConcatenateTransform(in Matrix3x2 transform);
As usual, feel free to add/remove, comment anything you need.... also I humbly request to throughfully test all that code, since it's quite critical stuff.... 2D graphics with odd scalings and rotations are quite tricky to handle.