Skip to content

1.0.0

Compare
Choose a tag to compare
@ianmackenzie ianmackenzie released this 01 Jul 03:42
· 15 commits to elm-0.18 since this release

elm-geometry 1.0 is out! This is effectively version 3.0 of opensolid/geometry, with quite a few changes since opensolid/geometry 2.1.0:

  • Renamed all modules to remove OpenSolid. prefix
  • Removed JSON encoding/decoding modules and split Interval and Scalar modules out into their own packages
  • Added new ParameterValue and SweptAngle modules
  • Refactored curve evaluation functions such as pointOn
  • Improved Polygon2d functionality significantly
  • Streamlined constructors for many geometric types
  • Updated spline constructors
  • Added several new useful functions
  • Renamed several functions
  • Removed a few deprecated functions

In general, to update code written for opensolid/geometry to use elm-geometry, first remove the OpenSolid. prefix from all imports. Then, follow the compiler error messages and look within these release notes to see how individual function calls should be updated (for example, search for Vector3d.with to see that it has been replaced by Vector3d.withLength). That said, these release notes are not exhaustive, so if you run into anything not covered here, come ask in the #geometry channel on the Elm Slack!

Renamed modules

Modules no longer include an OpenSolid. prefix, so imports are now more succinct:

-- opensolid/geometry
import OpenSolid.Point3d as Point3d exposing (Point3d)

-- elm-geometry
import Point3d exposing (Point3d)

The module names in elm-geometry have been chosen to try to avoid conflicts with other published Elm packages, but if you do find a conflict, please file an issue.

Removed modules

opensolid/geometry included built-in support for JSON encoding and decoding, but after some discussion on Discourse and Slack it was decided to remove this functionality from elm-geometry. It may reappear later, but likely as a separate package.

A couple of modules have been moved out into their own packages:

New modules

Curve.ParameterValue

Evaluating points on curves now requires you to pass a ParameterValue instead of a plain Float. A ParameterValue is effectively a Float that is guaranteed to be between 0 and 1, which adds some extra type safety to curve evaluation. The ParameterValue module includes functions for constructing ParameterValues from plain Floats, and several convenient functions for constructing ranges of evenly-spaced parameter values.

Arc.SweptAngle

The Arc.SweptAngle module replaces the duplicate SweptAngle types and values that existed the Arc2d and EllipticalArc2d modules:

-- opensolid/geometry
Arc2d.smallPositive : Arc2d.SweptAngle
EllipticalArc2d.smallPositive : EllipticalArc2d.SweptAngle

-- elm-geometry
SweptAngle.smallPositive : SweptAngle.SweptAngle

Refactored curve evaluation

A very common and important operation in elm-geometry is evaluating positions and tangent directions at various points along a curve (such as an arc or a cubic Bezier spline). A few related changes have been made in elm-geometry to make this more type-safe and explicit. First of all, as mentioned above, curve evaluation functions now generally take ParameterValue arguments instead of plain Float ones:

-- opensolid/geometry
CubicSpline3d.pointOn : CubicSpline3d -> Float -> Point3d

-- elm-geometry
CubicSpline3d.pointOn : CubicSpline3d -> ParameterValue -> Point3d

You can construct a ParameterValue using ParameterValue.clamped, but for the common case of evaluating many points at once there are also functions for directly generating a list of parameter values and then evaluating curves at those values. For example, to get 11 points on a cubic spline (including the start and end points), you could use

pointsOnSpline =
    cubicSpline |> CubicSpline3d.pointsAt (ParameterValue.steps 10)

Note that if you take 1 step along a curve you get 2 points (start and end), if you take 2 steps along a curve you get 3 points (start, middle and end), if you take 10 steps along a curve you get 11 points, etc.

Curves now also support evaluating tangent directions and 'samples' (point/tangent direction pairs), but only if the curve is nondegenerate. If a curve is actually just a single point (e.g. a spline where all control points are equal), then the curve is said to be degenerate and the tangent direction is undefined. All curve types (Arc3d, EllipticalArc2d, CubicSpline3d etc.) now have functions like

CubicSpline3d.nondegenerate : CubicSpline3d -> Result Point3d CubicSpline3d.Nondegenerate

to attempt to convert a curve to its guaranteed-nondegenerate form. If the curve is in fact degenerate (consists of a single point) then you will get an Err with that point instead. Once you have a Nondegenerate value, you can then use it to evaluate tangent directions and samples:

CubicSpline3d.tangentDirection : CubicSpline3d.Nondegenerate -> ParameterValue -> Direction3d
CubicSpline3d.sample : CubicSpline3d.Nondegenerate -> ParameterValue -> ( Point3d, Direction3d )

For example, if you wanted to animate along a cubic spline, you might call CubicSpline3d.nondegenerate first to try to get a nondegenerate curve. If that succeeds, then use the resulting Nondegenerate value to evaluate points and tangent directions along the curve. If it fails, then you should apply special-case logic - perhaps drop that curve entirely from your animation path, or display your animated object in a fixed position (using the point returned in the Err case) with some default orientation.

Polygon2d improvements

The Polygon2d module has been improved significantly in this release. First of all, polygons can now have holes, which necessitated several changes:

  • Polygon2d.fromVertices has been renamed to Polygon2d.singleLoop to emphasize that it constructs a polygon without holes
  • Polygon2d.with has been added to construct polygons with holes
  • Polygon2d.outerLoop and Polygon2d.innerLoops accessors have been added
  • Polygon2d.clockwiseArea, Polygon2d.counterclockwiseArea and Polygon2d.mapVertices have been removed since they could not easily be made to work with the new polygon representation

Polygon2d.convexHull has been added to compute the convex hull of a set of points (thanks @gampleman!):

Convex hull of a set of points

Finally, polygons can now be triangulated, so you can define a polygon just by specifying its outline (and holes, if it has any)

Polygon with hole

then use Polygon2d.triangulate to turn that polygon into a list of triangles:

Polygon triangulation

This is primarily useful for WebGL rendering but has many other applications.

Streamlined constructors

There was a significant push in this release to streamline geometry construction functions. Many functions that previously took a single record argument have been reworked to take multiple arguments instead, with the following goals in mind:

  • Reduce verbosity
  • Support partial application/work well with |>, map, map2 etc.
  • Try not to sacrifice too much clarity/explicitness

For example,

-- opensolid/geometry
Vector2d.with { length = 3, direction = Direction2d.x }

can now be written as

-- elm-geometry
Vector2d.withLength 3 Direction2d.x

Similarly,

-- opensolid/geometry
Axis2d.with { originPoint = point, direction = direction }

can now be written as

-- elm-geometry
Axis2d.through point direction
-- OR
Axis2d.withDirection direction point

Having both versions allow you to do different things with partial application:

-- A list of axes in different directions all passing through the same origin point
List.map (Axis2d.through point) directions

-- A list of parallel axes (all having the same direction) through different points
List.map (Axis2d.withDirection direction) points

Many other constructors have been similarly updated:

opensolid/geometry elm-geometry
Arc2d.fromEndpoints Arc2d.withRadius: Float -> SweptAngle -> Point2d -> Point2d -> Maybe Arc2d
Arc2d.with Arc2d.sweptAround: Point2d -> Float -> Point2d -> Arc2d
Arc3d.around Arc3d.sweptAround: Axis3d -> Float -> Point3d -> Arc3d
Axis3d.with Axis3d.through: Point3d -> Direction3d -> Axis3d
Axis3d.withDirection: Direction3d -> Point3d -> Axis3d
Circle2d.with Circle2d.withRadius: Float -> Point2d -> Circle2d
Circle3d.with Circle3d.withRadius: Float -> Direction3d -> Point3d -> Circle3d
Direction3d.with Direction3d.fromAzimuthAndElevation: ( Float, Float ) -> Direction3d
Frame2d.with Frame2d.withXDirection: Direction2d -> Point2d -> Frame2d
Frame2d.withYDirection: Direction2d -> Point2d -> Frame2d
Frame3d.with Frame3d.withXDirection: Direction3d -> Point3d -> Frame3d
Frame3d.withYDirection: Direction3d -> Point3d -> Frame3d
Frame3d.withZDirection: Direction3d -> Point3d -> Frame3d
Plane3d.with Plane3d.through: Point3d -> Direction3d -> Plane3d
Plane3d.withNormalDirection: Direction3d -> Point3d -> Plane3d
SketchPlane3d.with SketchPlane3d.withNormalDirection: Direction3d -> Point3d -> SketchPlane3d
Sphere3d.with Sphere3d.withRadius: Float -> Point3d -> Sphere3d
Vector3d.with Vector3d.withLength : Float -> Direction3d -> Vector3d

Updated spline constructors

The quadratic and cubic spline constructors previously known as fromControlPoints have been updated to avoid the use of large tuples, and generally be consistent with constructors in other modules:

-- opensolid/geometry
QuadraticSpline2d.fromControlPoints ( p1, p2, p3 )
CubicSpline3d.fromControlPoints ( p1, p2, p3, p4 )

-- elm-geometry
QuadraticSpline2d.with
    { startPoint = p1
    , controlPoint = p2
    , endPoint = p3
    }
    
CubicSpline3d.with
    { startPoint = p1
    , startControlPoint = p2
    , endControlPoint = p3
    , endPoint = p4
    }

Similarly, CubicSpline#d.hermite has been replaced by fromEndpoints:

-- opensolid/geometry
CubicSpline3d.hermite ( p1, v1 ) ( p2, v2 )

-- elm-geometry
CubicSpline3d.fromEndpoints
    { startPoint = p1
    , startDerivative = v1
    , endPoint = p2
    , endDerivative = v2
    }

New functions

As a small but very useful change, all modules with translateBy functions now also have translateIn functions; for example,

triangle |> Triangle3d.translateIn Direction3d.y 5

means "translate triangle in the Y direction by 5 units".

Arc2d.from has been added as a new and convenient way to construct arcs from a start point, end point and swept angle:

cornerArc =
    Arc2d.from startPoint endPoint (degrees 90)

A new version of Arc2d.with has also been added:

semicircle =
    Arc2d.with
        { centerPoint = Point2d.origin
        , radius = 3
        , startAngle = degrees 0
        , sweptAngle = degrees 180
        }

Circle2d.sweptAround has been added to construct a circle by sweeping one point around a center point:

circle =
    Circle2d.sweptAround centerPoint pointOnCircle

Bounding boxes can now be constructed for splines:

CubicSpline2d.boundingBox : CubicSpline2d -> BoundingBox2d
CubicSpline3d.boundingBox : CubicSpline3d -> BoundingBox3d
QuadraticSpline2d.boundingBox : QuadraticSpline2d -> BoundingBox2d
QuadraticSpline3d.boundingBox : QuadraticSpline3d -> BoundingBox3d

It's now possible to compute the second derivative for cubic splines as well as quadratic ones:

CubicSpline2d.secondDerivative : CubicSpline2d -> ParameterValue -> Vector2d
CubicSpline3d.secondDerivative : CubicSpline3d -> ParameterValue -> Vector3d

If you have an arc length parameterized curve, you can now recover the original curve from it:

CubicSpline2d.fromArcLengthParameterized : CubicSpline2d.ArcLengthParameterized -> CubicSpline2d
CubicSpline3d.fromArcLengthParameterized : CubicSpline3d.ArcLengthParameterized -> CubicSpline3d
EllipticalArc2d.fromArcLengthParameterized : EllipticalArc2d.ArcLengthParameterized -> EllipticalArc2d
QuadraticSpline2d.fromArcLengthParameterized : QuadraticSpline2d.ArcLengthParameterized -> QuadraticSpline2d
QuadraticSpline3d.fromArcLengthParameterized : QuadraticSpline3d.ArcLengthParameterized -> QuadraticSpline3d

Frame2d.atCoordinates has been added as a convenient shorthand for Frame2d.atPoint+Point2d.fromCoordinates, and similar for Frame3d:

Frame2d.atCoordinates : ( Float, Float ) -> Frame2d
Frame3d.atCoordinates : ( Float, Float, Float ) -> Frame3d

Point2d values can now be constructed directly from their polar coordinates with respect to a particular Frame2d:

Point2d.fromPolarCoordinatesIn : Frame2d -> ( Float, Float ) -> Point2d

Finally, like Direction2d, Vector2d values can now also be conveniently (and efficiently) rotated 90 degrees clockwise or counterclockwise:

Vector2d.rotateClockwise : Vector2d -> Vector2d
Vector3d.rotateCounterclockwise : Vector2d -> Vector2d

Renamed functions

Several functions have been renamed to try to be more descriptive, read better in pipelines, reduce circular dependencies between modules, or be more consistent with how other functions are named:

opensolid/geometry elm-geometry
Axis2d.flip Axis2d.reverse
Axis3d.flip Axis3d.reverse
BoundingBox2d.hullOf BoundingBox2d.aggregate
BoundingBox3d.hullOf BoundingBox3d.aggregate
BoundingBox2d.with BoundingBox2d.fromExtrema
BoundingBox3d.with BoundingBox3d.fromExtrema
Circle3d.around Circle3d.sweptAround
Direction2d.angle Direction2d.toAngle
Direction2d.flip Direction2d.reverse
Direction3d.flip Direction3d.reverse
Frame2d.flipX Frame2d.reverseX
Frame2d.flipY Frame2d.reverseY
Frame3d.flipX Frame3d.reverseX
Frame3d.flipY Frame3d.reverseY
Frame3d.flipZ Frame3d.reverseZ
LineSegment2d.normalDirection LineSegment2d.perpendicularDirection
LineSegment3d.normalDirection LineSegment3d.perpendicularDirection
Plane3d.flip Plane3d.reverseNormal
Point2d.hull BoundingBox2d.from
Point3d.hull BoundingBox3d.from
Point2d.hullOf BoundingBox2d.containingPoints
Point3d.hullOf BoundingBox3d.containingPoints
Point2d.in_ Point2d.fromCoordinatesIn
Point3d.in_ Point2d.fromCoordinatesIn
SketchPlane3d.flipX SketchPlane3d.reverseX
SketchPlane3d.flipY SketchPlane3d.reverseY
SketchPlane3d.plane SketchPlane3d.toPlane
Vector2d.flip Vector2d.reverse
Vector3d.flip Vector3d.reverse

Removed functions

A few functions have been removed in this release, but each one was simply a deprecated alias for a better-named replacement:

Removed Replacement
BoundingBox2d.overlaps BoundingBox2d.intersects
BoundingBox3d.overlaps BoundingBox3d.intersects
Point2d.distanceAlong Point2d.signedDistanceAlong
Point3d.distanceAlong Point3d.signedDistanceAlong