elm-geometry
elm-geometry copied to clipboard
Add dedicated transformation types/modules?
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.
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
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!
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 👍
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!
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
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".
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.
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
]