Elm-geometry 3: Now with units and coordinate systems

elm-geometry 3 is out! This release is the result of many months of work (and tons of useful feedback and contributions from the community) updating elm-geometry to keep track of both units and coordinate systems at compile time. This builds on elm-units to keep track of (for example) whether a given Point2d is in on-screen units (pixels) or real-world units (meters), or even some other kind of custom unit like tiles in a tile-based 2D game. Keeping track of coordinate systems is similar but unique to elm-geometry; this lets you keep track of things like whether a given point is defined in (for example) ‘local’ or ‘global’ coordinates, and is useful especially in more complex applications. Functions are also provided to convert between different units, and between different coordinate systems, in a rigorous and type-safe way.

These changes will break any code that uses elm-geometry 1.x, and may require a fair bit of work to update. For example, where before you might have written

import Point2d

point =
    Point2d.fromCoordinates ( 200, 300 )

you might now instead write

import Point2d
import Pixels exposing (pixels)

point =
    Point2d.xy (pixels 200) (pixels 300)

or perhaps just

import Point2d

point =
    Point2d.pixels 200 300

In addition, most functions that returned a Float now generally return a Quantity Float units, which means you will need to either convert those values to a Float using functions like Length.inMeters, or work directly with the values using functions from the Quantity module. For example, where before you might have written

if Point2d.distanceFrom p1 p2 > 10 then ...

you might now write something like

if Point2d.distanceFrom p1 p2 |> Quantity.greaterThan (Length.meters 10) then ...

As a result, while I certainly recommend using version 3 for all new projects, it may not be worth the effort to update existing apps - elm-geometry 1.x still works fine and has no known bugs, and I’m happy to support it for the foreseeable future. That said, if you have a published package that currently uses elm-geometry 1.x (or the temporary, never-officially-published 2.x) then it would be great if you could update it to use 3.x. I’m hoping that elm-geometry 3.x can be an “LTS” release that can reliably be used as a base for other packages, so it would be great to get all published packages using it to avoid dependency conflicts/ecosystem fragmentation. I’m happy to help with this process by answering questions or submitting PRs; please reach out to me (@ianmackenzie) on Slack!

I’m currently working on some high-level documentation for elm-geometry (based on the excellent elm-pages) that will complement the API reference documentation and discuss units, coordinate systems and other topics like 2D and 3D transformations in more detail; in the meantime, check out the README for a brief discussion of units and coordinate systems, and the release notes to get a sense of what’s changed since 1.x (you’ll have to look at the release notes for both 2.0.0 and 3.0.0 to get the full picture).

Happy to answer any questions either here or in the #geometry channel on the Elm Slack!

25 Likes

This is great, and it’s obvious that this is an absolute labour of love. I was especially delighted to play around with the Voronoi tesselation.

This made me think however that there might be space for an svg-simple or geometry-simple package building on top of this and elm-geometry-svg. The way I imagine it:

  • as a quick-start, give a simple, visible canvas backdrop (I tried to do something here)
  • let you import only one or two modules, so that you have lines, polygons, splines, etc all in one place, along with their drawing functions
  • it wouldn’t directly expose the units
  • It would let you draw lines, polygons, etc just based on lists of (x, y) coordinates, wrapping Point2d without directly expsing it.

The idea is basically to make it really fast and easy to go from nothing to drawing stuff and manipulating shapes. That way it would be a progressive gateway drug to this more full-fledged package.

What do you think, would this be fun to use, or is the above all covered by the raw svg package?

Hey @2mol, thanks! I’m actually working on a high level 2D drawing package almost like what you describe, although it does expose the units/Point2d type etc. Take a look at the examples to get a sense of what using the package would look like, although keep in mind that things may change before release.

I should also mention that evancz/elm-playground seems like a pretty good match for what you described, although that’s entirely independent of elm-geometry.

Oh yeah, thanks for reminding me of elm-playground! I’ll look at it in some more detail, but at first glance I think my ideal library would indeed be based on yours. Especially for transformations, Bézier curves, and leveraging some of your more advanced functionality like Voronoi.

I managed to create a working puzzle by the way: https://ellie-app.com/7qc2yL5hPpca1 :tada:

The examples you linked are also useful to learn how to use your library in a more elegant way, thanks!

Has adding units had much of an impact on performance? Not that I am up against any perf limits with SVG that I am using - just curious.

@2mol that puzzle looks awesome, great work!

@rupert I confess I haven’t done detailed performance comparison yet. I’m confident that it should be possible to get equivalent performance even with the addition of units, since in an --optimize build the Quantity Float units values will just be Float values at runtime (a compiler optimization introduced in Elm 0.19). But to get that maximum performance, it’s necessary to manually unwrap and re-wrap Quantity values and do all computations on the raw Float values instead of using the corresponding type-safe Quantity functions; for example in

sum =
    a |> Quantity.plus b

the use of the Quantity.plus function will likely introduce some function call overhead compared to

sum =
    let
        (Quantity aValue) =
            a

        (Quantity bValue) =
            b
    in
    Quantity (aValue + bValue)

In most low-level performance-critical code in elm-geometry code I’ve already done the above kind of transformation, but I haven’t done it everywhere.

On a related note, though, there’s one big performance improvement in elm-geometry 3 I’m excited about: effectively zero-overhead interop with code that uses plain records like { x : Float, y : Float } to represent points or vectors. Even though the public elm-geometry API uses units everywhere, the internal representation of a Point2d is actually just a { x : Float, y : Float }. This means that functions like Point2d.fromPixels or Point3d.toMeters are basically no-ops - they don’t incur any actual conversion or memory allocation overhead (at least in an --optimize build), they just ‘cast’ the given value to a different type. This should make it very efficient to have code that uses a mix of elm-geometry and plain record types (as long as you’re working in ‘base’ units like meters or pixels, but you should be doing that anyways =)).

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.