Xamarin-macios: CGAffineTransform.Scale does not work like Swift's .scaledBy(x:y:)

Created on 25 Aug 2018  Â·  8Comments  Â·  Source: xamarin/xamarin-macios

Steps to Reproduce

(Playground and Workbook attached)

  1. In Xcode, create a new Playground
  2. import CoreGraphics
  3. Execute:
let xform = CGAffineTransform(translationX: 1, y: 2).scaledBy(x: 3, y: 4)

debugPrint(xform)
  1. The result is __C.CGAffineTransform(a: 3.0, b: 0.0, c: 0.0, d: 4.0, tx: 1.0, ty: 2.0)

  2. Open Xamarin Workbooks

  3. Import CoreGraphics
  4. Execute what seems to be the equivalent code:
var xlatAndScale = CGAffineTransform.MakeTranslation(1, 2);
xlatAndScale.Scale(3, 4);
xlatAndScale

Expected Behavior

"Like Swift" behavior would produce xx:3.0 yx:0.0 xy:0.0 yy:4.0 x0:1.0 y0:2.0

Actual Behavior

"xx:3.0 yx:0.0 xy:0.0 yy:4.0 x0:3.0 y0:8.0"

It's clear what we're doing, but if this is what we want to do, I'd suggest another method that is `MakeTranslatedAndScaled(CGPoint translation, CGFloat scaleX, CGFloat scaleY) that just sets the values directly.

An added complexity is that the Swift code :

let xformR = CGAffineTransform(translationX: 1, y: 2).scaledBy(x: 3, y: 4).rotated(by: 3.1417 / 4)

produces C.CGAffineTransform(a: 2.121263413764814, b: 2.828503029102253, c: -2.12137727182669, d: 2.828351218353085, tx: 1.0, ty: 2.0)

Workaround

Set the scale first and then set the translation:

var scaleAndXlat = CGAffineTransform.MakeScale(3, 4);
scaleAndXlat.Translate(1, 2);
scaleAndXlat

Results in: "xx:3.0 yx:0.0 xy:0.0 yy:4.0 x0:1.0 y0:2.0"

Environment

=== Visual Studio Community 2017 for Mac (Preview) ===

Version 7.6 Preview (7.6 build 1773)
Installation UUID: 6b94f136-026d-4a5a-bf6d-af2c0d8dc019
Runtime:
    Mono 5.12.0.273 (2018-02/f59eac4c0f1) (64-bit)
    GTK+ 2.24.23 (Raleigh theme)
    Xamarin.Mac 4.4.1.178 (master / eeaeb7e6)

    Package version: 512000273

=== NuGet ===

Version: 4.3.1.4445

=== .NET Core ===

Runtime: /usr/local/share/dotnet/dotnet
Runtime Versions:
    2.1.1
    2.0.5
    2.0.3
    2.0.0
SDK: /usr/local/share/dotnet/sdk/2.1.301/Sdks
SDK Versions:
    2.1.301
    2.1.4
    2.0.3
    2.0.0
MSBuild SDKs: /Library/Frameworks/Mono.framework/Versions/5.12.0/lib/mono/msbuild/15.0/bin/Sdks

=== Xamarin.Profiler ===

Version: 1.6.3
Location: /Applications/Xamarin Profiler.app/Contents/MacOS/Xamarin Profiler

=== Xamarin.Android ===

Version: 8.4.0.1 (Visual Studio Community)
Android SDK: /Users/larryobrien/Library/Android/sdk
    Supported Android versions:
        4.1 (API level 16)
        5.0 (API level 21)
        6.0 (API level 23)
        7.0 (API level 24)
        7.1 (API level 25)
        8.0 (API level 26)
        8.1 (API level 27)

SDK Tools Version: 26.1.1
SDK Platform Tools Version: 25.0.3
SDK Build Tools Version: 27.0.3

Java SDK: /usr
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)

Android Designer EPL code available here:
https://github.com/xamarin/AndroidDesigner.EPL

=== Xamarin Inspector ===

Version: 1.4.0
Hash: b3f92f9
Branch: master
Build date: Fri, 19 Jan 2018 22:00:34 GMT
Client compatibility: 1

=== Apple Developer Tools ===

Xcode 10.0 (14313.2)
Build 10L213o

=== Xamarin.Mac ===

Version: 4.99.3.344 (Visual Studio Community)
Hash: 0b726519
Branch: 
Build date: 2018-08-09 10:37:58-0400

=== Xamarin.iOS ===

Version: 11.99.3.224 (Visual Studio Community)
Hash: 0b726519
Branch: HEAD
Build date: 2018-08-09 10:37:57-0400

=== Build Information ===

Release ID: 706001773
Git revision: e5958ff9015312c81fc3b1b6b6413585575ca869
Build date: 2018-06-19 12:19:27+00
Build branch: release-7.6
Xamarin extensions: 6910f36b43e7d559ff40ae268e73ae26c30a2a6b

=== Operating System ===

Mac OS X 10.13.6
Darwin 17.7.0 Darwin Kernel Version 17.7.0
    Thu Jun 21 22:53:14 PDT 2018
    root:xnu-4570.71.2~1/RELEASE_X86_64 x86_64

=== Enabled user installed extensions ===

AddinMaker 1.4.2
Larry's F# Templates 2.0.8
MicroFramework 1.0.3
Internet of Things (IoT) development (Preview) 7.5.3


Example Project (If Possible)

AffineTransforms.zip

enhancement iOS macOS

Most helpful comment

My suggestion is to add a new function called ScaleBy that mimics Swift's behavior.

public void ScaleBy (nfloat sx, nfloat sy)
{
    this = Multiply (MakeScale (sx, sy), this);
}

And additional document it:

  • CGAffineTransform.Scale: "This function does t' = t * [ sx 0 0 sy 0 0 ], which is not equivalent to Swift's scaleBy:. Use CGAffineTransform.ScaleBy in that case"
  • CGAffineTransform.ScaleBy: "This function does t' = [ sx 0 0 sy 0 0 ] * t, which is equivalent to Swift's scaleBy:.

All 8 comments

I could confirm the different results given by the C# and Swift test cases. Investigating this a bit further.

Ah ok our scale is incorrect (not doing the same as the native version at least).

The native scaleBy code has a comment that indicates what it's doing: t' = [ sx 0 0 sy 0 0 ] * t

We do t' = t * [ sx 0 0 sy 0 0 ]

See CGAffineTransform.Scale where t is a (here) and [ sx 0 0 sy 0 0 ] or MakeScale is b (passed as argument here).

Matrices are non-commutative so our code isn't equivalent to the native operation.

We need:

C# public void Scale (nfloat sx, nfloat sy) { this = Multiply (MakeScale (sx, sy), this); }

My suggestion is to add a new function called ScaleBy that mimics Swift's behavior.

public void ScaleBy (nfloat sx, nfloat sy)
{
    this = Multiply (MakeScale (sx, sy), this);
}

And additional document it:

  • CGAffineTransform.Scale: "This function does t' = t * [ sx 0 0 sy 0 0 ], which is not equivalent to Swift's scaleBy:. Use CGAffineTransform.ScaleBy in that case"
  • CGAffineTransform.ScaleBy: "This function does t' = [ sx 0 0 sy 0 0 ] * t, which is equivalent to Swift's scaleBy:.

I like that idea. Otherwise Sebastien suggested we'd copy System.Drawing. So if we look at https://docs.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.matrixorder?view=netframework-4.7.2.

We'd do something like:

```C#
public enum MultiplyOrder {
Prepend = 0,
Append = 1,
}

public void Scale (nfloat sx, nfloat sy) {
Scale (sx, sy, MultiplyOrder.Append);
}

public void Scale (nfloat sx, nfloat sy, MultiplyOrder order)
{
switch (order) {
case MultiplyOrder.Append: // The new operation is applied after the old operation.
this = Multiply (this, MakeScale (sx, sy)); // t' = t * [ sx 0 0 sy 0 0 ]
break;
case MultiplyOrder.Prepend: // The new operation is applied before the old operation.
default:
this = Multiply (MakeScale (sx, sy), this); // t' = [ sx 0 0 sy 0 0 ] * t – Swift equivalent
break;
}
}
```

I like the System.Drawing approach.

@VincentDondain yes, that's how we should expose both versions. However better use MatrixOrder (like .NET) since we're sharing the same API _pattern_

We could also add [Advice] on the existing API that mention the default order.

Using an enum (MatrixOrder) looks good to me too, although in the implementation I'd throw ArgumentOutOfRangeException in the default case (instead of treating it as .Prepend).

Was this page helpful?
0 / 5 - 0 ratings