ianmackenzie/elm-geometry

2D/3D geometric data types and operations


Keywords
cad, coordinate-systems, curve, elm, geometry, point, transformations, vector
License
MPL-2.0
Install
elm-package install ianmackenzie/elm-geometry 1.0.0

Documentation

What is it?

OpenSolid consists of a set of Elm libraries for working with geometry. It is intended to provide a solid foundation for HTML-based applications in areas such as CAD (computer-aided design), CAM (computer-aided manufacturing), and 2D/3D visualization.

This library contains the OpenSolid core data types and operations - vectors, directions (type-safe unit vectors), points, axes, planes and frames (coordinate systems) in 2D and 3D:

Vector3d.zero == Vector3d ( 0, 0, 0 )
Direction2d.y == Direction2d ( 0, 1 )
Point2d.origin == Point2d ( 0, 0 )

Axis3d.z ==
    Axis3d
        { originPoint = Point3d.origin
        , direction = Direction3d.z
        }

Frame2d.xy ==
    Frame2d
        { originPoint = Point2d.origin
        , xDirection = Direction2d.x
        , yDirection = Direction2d.y
        }

Plane3d.yz ==
    Plane3d
        { originPoint = Point3d.origin
        , normalDirection = Direction3d.x
        }

A large range of geometric operations are supported:

-- Constructors

Direction2d.fromAngle (degrees 30) ==
    Direction2d ( 0.866, 0.5 )

Point3d.midpoint Point3d.origin (Point3d (1, 4, 5)) ==
    Vector2d ( 0.5, 2, 2.5 )

-- Arithmetic

Vector3d.plus (Vector3d ( 1, 2, 3 )) (Vector3d ( 4, 5, 6 )) ==
    Vector3d ( 5, 7, 9 )

Point3d.vectorFrom (Point3d ( 1, 1, 1 )) (Point3d ( 3, 5, 4 )) ==
    Vector3d ( 2, 4, 3 )

Point2d.distanceFrom Point2d.origin (Point2d ( 1, 1 )) == 1.4142

-- Transformations

Point3d.mirrorAcross Plane3d.xy (Point3d ( 1, 2, 3 )) ==
    Point3d ( 1, 2, -3 )

Vector2d.rotateBy (degrees 45) (Vector2d ( 1, 1 )) ==
    Vector2d ( 0, 1.4142 )

Point2d.rotateAround Point2d.origin (degrees 45) (Point2d ( 1, 0 )) ==
    Point2d ( 0.7071, 0.7071 )

Plane3d.translateBy (Vector3d ( 0, 0, 3 )) Plane3d.xy ==
    Plane3d.offsetBy 3 Plane3d.xy

Point3d.projectOnto Plane3d.xy (Point3d ( 2, 1, 3 )) ==
    Point3d ( 2, 1, 0 )

Vector3d.projectionIn Direction3d.z (Vector3d ( 3, 1, 4 )) ==
    Vector3d ( 0, 0, 4 )

JSON encoders and decoders for all types are also provided in the Encode and Decode modules.

How do I use it?

To install, run

elm package install opensolid/core

or add

"opensolid/core": "1.0.0 <= v < 2.0.0"

to your elm-package.json.

Most OpenSolid modules are designed to imported as qualified, for example

import OpenSolid.Core.Point3d as Point3d

The main exception is the Types module, which only contains type definitions and is intended to be imported without qualification:

import OpenSolid.Core.Types exposing (..)

For technical details, check out the documentation for each module, but if you're interested in some of the guiding principles and rationale behind OpenSolid, read on!

What makes it different?

OpenSolid is functionally similar to other vector/geometry libraries, but works differently in a few subtle but meaningful ways. In general, OpenSolid has a more geometric than mathematical focus. For example, distinct types are used for points, vectors and directions which many other libraries treat as a single generic vector type.

Transformations

Many of the functions in this library deal with different kinds of transformations - translations, rotations, scalings, mirrors, and projections. Unlike most other geometric libraries, however, OpenSolid does not use matrices to define transformations (in fact, matrices are not used anywhere). Instead of having functions to create transformation matrices which can then later be applied to values, transformations in OpenSolid are just functions that can be used directly:

rotatedPoint = Point2d.rotateAround Point2d.origin (degrees 45) originalPoint

This has many advantages. First, partial function application means that transformations can be directly used with higher-order functions like List.map:

pointsOnXAxis =
    [ Point2d ( 1, 0 )
    , Point2d ( 2, 0 )
    , Point2d ( 3, 0 )
    ]

rotateNinetyDegrees : Point2d -> Point2d
rotateNinetyDegrees =
    Point2d.rotateAround Point2d.origin (degrees 90)

pointsOnYAxis =
    List.map rotateNinetyDegrees pointsOnXAxis

pointsOnYAxis ==
    [ Point2d ( 0, 1 )
    , Point2d ( 0, 2 )
    , Point2d ( 0, 3 )
    ]

Second, transformations can be composed like any other functions to produce composite transformations (no more having to remember multiplication order of matrices!):

rotateThenScale : Point2d -> Point2d
rotateThenScale =
    Point2d.rotateAround Point2d.origin (degrees 90)
        >> Point2d.scaleAbout Point2d.origin 1.5

rotateThenScale (Point2d ( 1, 0 )) ==
    Point2d ( 0, 1.5 )

rotateThenScale (Point2d ( 0, 2 )) ==
    Point2d ( -3, 0 )

(Yes, in this particular case it doesn't actually matter whether you rotate first and then scale or the other way around, but you get the idea.)

Directions

OpenSolid uses the concept of a 'direction' where other libraries typically use vectors with unit length. Having separate types helps to keep track of whether a vector has already been normalized - no more having to guess whether a function that accepts a vector argument actually needs a unit vector, and if so whether you're expected to normalize the vector yourself or whether the function will do that internally.

You can normalize a vector to produce a direction with the Vector2d.direction and Vector3d.direction functions, but they actually return Maybe values since the zero vector has no direction:

Vector2d.direction (Vector2d ( 3, 0 )) ==
    Just (Direction2d ( 1, 0 ))

Vector2d.direction (Vector2d ( -2, 2 )) ==
    Just (Direction2d ( -0.7071, 0.7071 ))

Vector2d.direction (Vector2d ( 0, 0 )) ==
    Nothing

This takes advantage of Elm's type system to ensure that all code considers the degenerate zero-vector case. For example, given an eye point and a point to look at, the corresponding view direction could be determined with

Vector3d.direction (Point3d.vectorFrom eyePoint lookAtPoint)

This would return a Maybe Direction3d, with Nothing corresponding to the case where the eye point and point to look at are coincident (in which case the view direction is not well-defined and some special-case logic is needed).

Coordinates and components

Explicitly working with individual X/Y/Z point coordinates and vector components is easy to do in OpenSolid when necessary:

forwardSpeed =
    Vector3d.xComponent velocityVector

height =
    Point3d.zCoordinate position

In many cases, however, it is equally easy and often advantageous to consider points and vectors as abstract geometric quantities and treat their internal representations by coordinates/components as an implementation detail. For example, the above might be replaced by

forwardSpeed =
    Vector3d.componentIn forwardDirection velocityVector

height =
    Point3d.signedDistanceFrom groundPlane position

where (perhaps in a Constants.elm file or similar) you would define

forwardDirection =
    Direction3d.x

groundPlane =
    Plane3d.xy

This approach is less coupled to the particular coordinate system being used and adapts more easily to changes - perhaps the forward direction becomes a configuration setting, or the ground plane becomes dynamic and shifts or tilts over time.