elm-geometry icon indicating copy to clipboard operation
elm-geometry copied to clipboard

Add dedicated transformation types/modules?

Open ianmackenzie opened this issue 5 years ago • 8 comments

For efficient composite transformations:

module Transformation3d

translateBy : Vector3d -> Transformation3d

rotateAround : Axis3d -> Float -> Transformation3d

mirrorAcross : Plane3d -> Transformation3d

sequence : List Transformation3d -> Transformation3d

identity : Transformation3d

preservesHandedness : Transformation3d -> Bool

then have Point3d.transformBy : Transformation3d -> Point3d -> Point3d etc.

I don't love having two ways to do the same thing (if you want to rotate a bunch of points, do you use Point3d.rotateAround or Point3d.transformBy with Transformation3d.rotateAround?) but this may be the cleanest solution that allows for efficient, non-error-prone transformations.

Should scaling be supported? Then you couldn't transform frames, directions etc...unless Transformation3d had a type variable that indicated whether it was Rigid or NonRigid.

Could get fancier and allow conversions between units/coordinate systems...perhaps Transformation3d would have type variables rigidity, inputCoordinates and outputCoordinates and include functions relativeTo, placeIn and convertUnits or similar? (This is assuming #66 happens.) This would, however, mean that you could only use sequence for a sequence of transformations that did not change units or coordinates - those would need a slightly different approach.

ianmackenzie avatar Sep 27 '18 14:09 ianmackenzie

Could get fancy and have phantom type parameters to tag transformations as non-scaling or non-shearing, to support transforming directions or circles etc. Perhaps:

module Transformation2d

identity : Transformation units coordinates a

translateBy :
    Vector2d units coordinates
    -> Transformation2d units coordinates a

rotateAround :
    Point2d units coordinates
    -> Angle
    -> Transformation2d units coordinates a

scaleAbout : 
    Point2d units coordinates
    -> Float 
    -> Transformation2d units coordinates { a | scaling : Allowed }

scaleAlong : 
    Axis2d units coordinates 
    -> Float 
    -> Transformation2d units coordinates { a | scaling : Allowed, shear : Allowed }

sequence : 
    List (Transformation2d units coordinates a) 
    -> Transformation2d units coordinates a
module Triangle2d

apply : 
    Transformation2d units coordinates { scaling : Allowed, shear : Allowed } 
    -> Triangle2d units coordinates 
    -> Triangle2d units coordinates
module Circle2d

apply : 
    Transformation2d units coordinates { scaling : Allowed } 
    -> Circle2d units coordinates 
    -> Circle2d units coordinates
module Frame2d

apply : 
    Transformation2d units coordinates {} 
    -> Frame2d units coordinates 
    -> Frame2d units coordinates

ianmackenzie avatar Oct 08 '18 15:10 ianmackenzie

Perhaps have the function called apply instead of transformBy? So you could write code like

let
    transformation =
        Rotation.around axis angle
in
List.map (Point3d.apply transformation) points

Less likely to be interpreted as the 'default' function to use for transformations...and a bit shorter!

ianmackenzie avatar Dec 12 '18 02:12 ianmackenzie

First of all, great package!

I have started experimenting with Elm by writing an astrodynamics package. In this field you often want to apply the same transformation to the position and velocity vectors. Thus, being able to reuse a transformation would make the code simpler and improve efficiency at the same time, e.g. https://github.com/helgee/elm-astrodynamics/blob/master/src/Astrodynamics.elm#L216

So, this is my 👍

helgee avatar Feb 01 '19 07:02 helgee

Thanks @helgee! I think there are lots of useful applications of a generic Transformation3d type, but as long as you're not applying a scale factor then you should be able to achieve much the same effect using a Frame3d. For example, I think you could write your code as something like

cartesian : KeplerianElements -> Float -> ( Vector3d, Vector3d )
cartesian elements mu =
    let
        semiLatus =
            if almostEqual elements.eccentricity 0 then
                elements.semiMajorAxis

            else
                elements.semiMajorAxis * (1 - elements.eccentricity ^ 2)

        ( rPerifocal, vPerifocal ) =
            perifocal semiLatus elements.eccentricity elements.trueAnomaly mu

        rotatedFrame =
            Frame3d.atOrigin
                |> Frame3d.rotateAround Axis3d.z elements.argumentOfPericenter
                |> Frame3d.rotateAround Axis3d.x elements.inclination
                |> Frame3d.rotateAround Axis3d.z elements.ascendingNode
    in
    ( rPerifocal |> Vector3d.placeIn rotatedFrame
    , vPerifocal |> Vector3d.placeIn rotatedFrame
    )

where you construct a rotated frame and then "place" the vectors "in" that frame. This is equivalent to treating rPerifocal and vPerifocal as vectors in local coordinates within rotatedFrame, and getting the same vectors expressed in global coordinates. (Happy to go into more details if that doesn't make sense...)

I can imagine placeIn and relativeTo being generally useful in an astrodynamics package to convert between different (Cartesian) coordinate systems, although with one gotcha - transformations between coordinate systems don't consider velocity or angular velocity, so positions/displacements should always convert correctly but relative velocities would be incorrect when working with moving frames. (The next version of elm-geometry, which will track units and coordinate systems at compile time, should make this a compile-time error, but for now you'll just have to be a bit careful.)

Really excited to see you working on an astrodynamics package for Elm - looking forward to seeing what you come up with!

ianmackenzie avatar Feb 01 '19 22:02 ianmackenzie

Just wondering if a more transformation oriented API would make sense? Since the reason for using these is to compose transformations (otherwise you might as well use the functions in the specific modules).

So more like:

module Transformation2d

identity : Transformation units coordinates

translateBy :
    Vector2d units coordinates
    -> Transformation2d units coordinates
    -> Transformation2d units coordinates

rotate:
    Angle
    -> Transformation2d units coordinates
    -> Transformation2d units coordinates

scale : 
    Float 
   -> Transformation2d units coordinates
    -> Transformation2d units coordinates

scaleXY : 
    Float
    -> Float 
    -> Transformation2d units coordinates
    -> Transformation2d units coordinates

-- any maybe low level

mult : ((Float, Float, Float), (Float, Float, Float)) -> Transformation2d units coordinates -> Transformation2d units coordinates

gampleman avatar Feb 20 '19 13:02 gampleman

That could make sense - that would get around the issue of arguments to sequence needing to have all the same units/coordinates types, so it would be easier to support transformations that included conversions between units or coordinate systems. (Not 100% sure if that's a good thing though...how much complex logic should really be packed into a single Transformation2d? Translation, rotation, scaling, shear, unit conversion, coordinate system conversion?)

I actually can see use cases for Transformation2d even if it's only a single transformation, since you can apply a Transformation2d to values of different types. Unlike (for example) Point2d.translateBy vector, passing Transformation2d.translateBy vector as a function argument means that function can apply the transformation to multiple different types internally.

I also kind of like the simplicity and orthogonality of the current API: translateBy, rotateAround etc. are just individual transformations, and sequence is for combining transformations. With the proposed API, the main transformation functions kind of combine the operations of "defining a transformation" and "combining that transformation with others".

ianmackenzie avatar Feb 20 '19 14:02 ianmackenzie

Perhaps, but they compose quite nicely.

myTransformation : Transformation2d units coordinates -> Transformation2d units coordinates
myTransformation = 
      Transformation2d.scale 2
          >> Transformation2d.translateBy (Vector2d.fromComponents 200 -400)
          >> Transformation2d.rotate (Angle.degrees 40)
          >> Transformation2d.translateBy (Vector2d.fromComponents 100 200)

vs

myTransformation : Transformation2d units coordinates -> Transformation2d units coordinates
myTransformation initial = 
    Transformation2d.sequence
        [ initial
        , Transformation2d.scale 2
        , Transformation2d.translateBy (Vector2d.fromComponents 200 -400)
        , Transformation2d.rotate (Angle.degrees 40)
        , Transformation2d.translateBy (Vector2d.fromComponents 100 200)
        ]

Anyway, I think both would be pretty nice, so no strong argument here.

gampleman avatar Feb 20 '19 14:02 gampleman

True - that is cool! Although I would probably write your second example as

myTransformation : Transformation2d units coordinates
myTransformation = 
    Transformation2d.sequence
        [ Transformation2d.scale 2
        , Transformation2d.translateBy (Vector2d.fromComponents 200 -400)
        , Transformation2d.rotate (Angle.degrees 40)
        , Transformation2d.translateBy (Vector2d.fromComponents 100 200)
        ]

which you could then compose by including it in other sequences:

finalTransformation : Transformation2d units coordinates
finalTransformation =
    Transformation2d.sequence
        [ Transformation2d.mirrorAcross Axis2d.x
        , myTransformation
        , Transformation2d.mirrorAcross Axis2d.y
        ]

ianmackenzie avatar Feb 20 '19 14:02 ianmackenzie