Giter VIP home page Giter VIP logo

elm-geometry's Introduction

Hey! πŸ‘‹

I work as a software developer at Generative, and I maintain several open-source Elm packages including:

I'm interested in:

  • 3D geometry
  • Computer-aided design (CAD)
  • Computer-aided manufacturing (CAM)
  • Using those to help address the climate crisis!

You can generally find me (@ianmackenzie) on the Elm Slack; I tend to hang out mostly in the #geometry and #webgl channels.

elm-geometry's People

Contributors

danfishgold avatar daphee avatar davcamer avatar folkertdev avatar g-belmonte avatar gampleman avatar ianmackenzie avatar matyjas avatar mpizenberg avatar mrl1605 avatar odf avatar sebastiankg avatar suzzzwood avatar w0rm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elm-geometry's Issues

Add heterogeneous intersection functions

  • Axis2d.intersectionWithBoundingBox : BoundingBox2d -> Axis2d -> Maybe LineSegment2d
  • Axis2d.intersectionWithTriangle : Triangle2d -> Axis2d -> Maybe LineSegment2d
  • Axis3d.intersectionWithBoundingBox : BoundingBox3d -> Axis3d -> Maybe LineSegment3d
  • Axis3d.intersectionWithPlane : Plane3d -> Axis3d -> Maybe Point3d
  • Axis3d.intersectionWithTriangle : Triangle3d -> Axis3d -> Maybe Point3d
  • Axis3d.intersectionWithSphere : Sphere3d -> Axis3d -> Maybe LineSegment3d
  • LineSegment2d.intersectionWithAxis : Axis2d -> LineSegment2d -> Maybe Point2d
  • LineSegment2d.intersectionWithBoundingBox : BoundingBox2d -> LineSegment2d -> Maybe LineSegment2d
  • LineSegment2d.intersectionPointsWithBoundingBox : BoundingBox2d -> LineSegment2d -> List Point2d
  • LineSegment2d.intersectionWithTriangle : Triangle2d -> LineSegment2d -> Maybe LineSegment2d
  • LineSegment2d.intersectionPointsWithTriangle : Triangle2d -> LineSegment2d -> List Point2d
  • LineSegment2d.intersectionWithCircle : Circle2d -> LineSegment2d -> Maybe LineSegment2d
  • LineSegment2d.intersectionPointsWithCircle : Circle2d -> LineSegment2d -> List Point2d
  • LineSegment3d.intersectionWithBoundingBox : BoundingBox3d -> LineSegment3d -> Maybe LineSegment3d
  • LineSegment3d.intersectionPointsWithBoundingBox : BoundingBox3d -> LineSegment3d -> List Point3d
  • LineSegment3d.intersectionWithPlane : Plane3d -> LineSegment3d -> Maybe Point3d
  • LineSegment3d.intersectionWithTriangle : Triangle3d -> LineSegment3d -> Maybe Point3d
  • Triangle3d.intersectionWithPlane : Plane3d -> Triangle3d -> Maybe LineSegment3d

Add homogeneous intersection functions

  • Axis2d.intersectionPoint : Axis2d -> Axis2d -> Maybe Point2d
  • Plane3d.intersectionAxis : Plane3d -> Plane3d -> Maybe Axis3d
  • LineSegment2d.intersectionPoint : LineSegment2d -> LineSegment2d -> Maybe Point2d
  • Triangle3d.intersectionLineSegment : Triangle3d -> Triangle3d -> Maybe LineSegment3d

Question: Frame2d -> Scale

Hello,
First thanks for your libraries, they are neat.

I have a question Frame2d (and Frame3d) reprΓ©sent a coordinate system on wich you can project shape.
They can be translated and rotated, is it sane to think that they also could be scaled ?
For example to represent a scene in wich some element attached to a specific frame description could be automaticaly scaled down/rotated/translated ?

Add 'fromFields' constructors for all types

These would simply construct a value from separate arguments:

Point2d.fromFields : Float -> Float -> Point2d
Frame3d.fromFields : Point3d -> Direction3d -> Direction3d -> Direction3d -> Frame3d
BoundingBox2d.fromFields : Float -> Float -> Float -> Float -> BoundingBox2d

These would be relatively low-level (BoundingBox2d.fromFields is pretty error-prone!) but would be useful for passing to mapN functions commonly encountered when constructing JSON decoders or fuzzers. Taking some existing code as examples,

axis2d : Decoder Axis2d
axis2d =
    Decode.map2
        (\originPoint direction ->
            Axis2d { originPoint = originPoint, direction = direction }
        )
        (Decode.field "originPoint" point2d)
        (Decode.field "direction" direction2d)

would become

axis2d : Decoder Axis2d
axis2d =
    Decode.map2 Axis2d.fromFields
        (Decode.field "originPoint" point2d)
        (Decode.field "direction" direction2d)

and

axis2d : Fuzzer Axis2d
axis2d =
    let
        axis originPoint direction =
            Axis2d { originPoint = originPoint, direction = direction }
    in
    Fuzz.map2 axis point2d direction2d

would become

axis2d : Fuzzer Axis2d
axis2d =
    Fuzz.map2 Axis2d.fromFields point2d direction2d

Add interval-valued distance functions to geometry types

  • LineSegment2d.signedDistanceAlong : Axis2d -> LineSegment2d -> Interval
  • LineSegment2d.signedDistanceFrom : Axis2d -> LineSegment2d -> Interval
  • LineSegment3d.signedDistanceAlong : Axis3d -> LineSegment3d -> Interval
  • LineSegment3d.signedDistanceFrom : Plane3d -> LineSegment3d -> Interval
  • Triangle2d.signedDistanceAlong : Axis2d -> Triangle2d -> Interval
  • Triangle2d.signedDistanceFrom : Axis2d -> Triangle2d -> Interval
  • Triangle3d.signedDistanceAlong : Axis3d -> Triangle3d -> Interval
  • Triangle3d.signedDistanceFrom : Plane3d -> Triangle3d -> Interval
  • Triangle2d.distanceFrom : Triangle2d -> Triangle2d -> Interval
  • Triangle2d.distanceFromPoint : Point2d -> Triangle2d -> Interval
  • Triangle3d.distanceFrom : Triangle3d -> Triangle3d -> Interval
  • Triangle3d.distanceFromPoint : Point3d -> Triangle3d -> Interval
  • BoundingBox2d.signedDistanceAlong : Axis2d -> BoundingBox2d -> Interval
  • BoundingBox2d.signedDistanceFrom : Axis2d -> BoundingBox2d -> Interval
  • BoundingBox3d.signedDistanceAlong : Axis3d -> BoundingBox3d -> Interval
  • BoundingBox3d.signedDistanceFrom : Plane3d -> BoundingBox3d -> Interval
  • BoundingBox2d.distanceFrom : BoundingBox2d -> BoundingBox2d -> Interval
  • BoundingBox2d.distanceFromPoint : Point2d -> BoundingBox2d -> Interval
  • BoundingBox3d.distanceFrom : BoundingBox3d -> BoundingBox3d -> Interval
  • BoundingBox3d.distanceFromPoint : Point3d -> BoundingBox3d -> Interval
  • BoundingBox3d.distanceFromAxis : Axis3d -> BoundingBox3d -> Interval
  • Rectangle2d.signedDistanceAlong : Axis2d -> BoundingBox2d -> Interval
  • Rectangle2d.signedDistanceFrom : Axis2d -> BoundingBox2d -> Interval
  • Rectangle3d.signedDistanceAlong : Axis3d -> BoundingBox3d -> Interval
  • Rectangle3d.signedDistanceFrom : Plane3d -> BoundingBox3d -> Interval

Add rectangle type(s)?

It would be useful to have Rectangle2d and perhaps Cuboid3d types in OpenSolid. But what should the representation be? I think it's a given that these objects should be able to be rotated, so they can't be represented simply by min/max X/Y etc. or by two diagonal vertices. Some possible representations:

type Rectangle2d
    = Rectangle2d
        { centerFrame : Frame2d
        , dimensions : ( Float, Float )
        }

type Cuboid3d
    = Cuboid3d
        { centerFrame : Frame3d
        , dimensions : ( Float, Float, Float )
        }

Rename 'BoundingBox#d.overlaps' -> 'intersects'

overlaps is too easily interpreted as implying real, finite overlap instead of just touching along an edge or at a vertex. Renaming to intersects should alleviate this and emphasize the relationship with intersection.

Convert text-based examples to visual ones where that makes sense

I think many elm-geometry functions could be more easily explained with an image or two instead of looking at a bunch of numerical text output. For example, the current documentation example for Point3d.rotateAround is:

axis =
    Axis3d.x

angle =
    degrees 45

point =
    Point3d.fromCoordinates ( 3, 1, 0 )

Point3d.rotateAround axis angle point
--> Point3d.fromCoordinates ( 3, 0.7071, 0.7071 )

Ideally, I think the code example would simply be something like

p2 =
    Point3d.rotateAround axis (degrees 45) p1

and then there would be an SVG image showing the axis, the two points (labelled as p1 and p2), and maybe something like a dotted arc between the two points showing the rotation path.

For more complex examples, the image might actually be an iframe with an interactive example (written in Elm, of course!) that lets you drag input points, tweak numerical parameters etc. and see the output change.

This will require a significant amount of design and implementation work to come up with a nice consistent visual style for the examples and a convenient framework for generating the images.

Polygon2d.triangulate can fail if vertices are repeated

Specifically, if a loop is given where the first point is equal to the last point, the duplicate will not be removed. This is because Monotone.removeDuplicates only removes successive duplicates, and doesn't check to see if the first point is equal to the last. Thanks @w0rm for the report!

Add 'translateIn' to all applicable types

Can have pretty much the exact same implementation for all types, for example

module OpenSolid.Triangle3d

{-| Translate a triangle in a given direction by a given distance.

    Triangle3d.translateIn Direction3d.z 5 exampleTriangle
    --> Triangle3d.fromVertices
    -->     ( Point3d.fromCoordinates ( 1, 0, 5 )
    -->     , Point3d.fromCoordinates ( 2, 0, 5 )
    -->     , Point3d.fromCoordinates ( 2, 1, 8 )
    -->     )

-}
translateIn : Direction3d -> Float -> Triangle3d -> Triangle3d
translateIn direction distance =
    translateBy
        (Vector3d.with
            { length = distance
            , direction = direction
            }
        )

Rename BoundingBox#d.centroid -> centerPoint

This would be consistent with Circle2d, Ellipse2d and Rectangle2d - Triangle2d uses centroid but only because the 'center point' of a triangle is not well defined (is it the centroid? center of the incircle? center of the circumcircle?) so centroid is more explicit. For a BoundingBox#d the center point is pretty clear and unambiguous, so it would be good to be consistent with Rectangle2d. This would of course be a breaking change (#56), but as an initial step centerPoint could be added and centroid could be deprecated.

Rename distanceAlong -> signedDistanceAlong

Point#d.distanceAlong is a bit inconsistent in that all other distance functions either return a nonnegative value or explicitly indicate that they return a signed value (signedDistanceFrom).

Add wiki pages

  • 2D/3D transformations
  • Transformations, matrices and frames
  • Related projects/ecosystem
  • Arc length parameterization
  • Curve evaluation

Potential breaking changes for elm-geometry 1.0

This is a meta-issue for potential breaking changes to make for an elm-geometry 1.0 version. Feel free to post suggestions in the comments and I'll edit this issue text.

Module naming

I used prefixed module names like OpenSolid.Point2d to avoid potential naming conflicts with modules from other packages, but based on elm/compiler#1625 it seems that this might be considered "silly module renaming". Should the prefixes be removed? I think they should remain in a few cases like OpenSolid.Geometry.Decode, but otherwise module names could be switched to just plain Point2d, Triangle2d etc. What about the Scalar and Interval modules? Those are fairly generic names, so might benefit from staying as OpenSolid.Scalar and OpenSolid.Interval...but then that feels a bit inconsistent/arbitrary. Rename to Scalar1d and Interval1d and just lay claim to all module names ending in 1d, 2d or 3d?

Switching to non-prefixed names would be more consistent with other Elm packages - elm-community/elm-test uses top-level Expect, Fuzz and Test modules, mdgriffith/style-elements uses top-level Style and Element modules, terezka/elm-plot uses a top-level Plot module, etc.

Additionally, if the non-prefixed names don't work out and run into conflicts with other packages, then these conflicts will provide additional data points for elm/compiler#1625.

Package split

This is tracked in a separate issue, but splitting this package into two smaller packages would certainly be a major version change (although it shouldn't actually require any code changes, just modifications to dependencies in elm-package.json).

Moves/renames

  • It would probably make sense to have SweptAngle as its own module now that Arc2d and EllipticalArc2d both use the same concept (currently just two independent but identical Arc2d.SweptAngle and EllipticalArc2d.SweptAngle types). This would likely mean adding an Internal.SweptAngle type and just exposing SweptAngle.smallPositive etc.; the Arc2d and EllipticalArc2d modules could then pattern-match on the Internal.SweptAngle constructors. (Directly referring to stuff like SweptAngle.SmallPositive feels a bit weird, and I can't think of any cases where it would be useful for end-user code to pattern-match on SweptAngle values.) On the other hand, if the OpenSolid prefix is removed, then it might not make sense to have a top-level SweptAngle module (unlikely to conflict with modules from other packages, but a little less obvious what package it comes from than something like Point3d).
  • I find I often want to write Direction2d.toAngle instead of Direction2d.angle. angle is consistent with other functions like components and coordinates in that it treats the returned angle as a property of the direction ("the angle of the direction") instead of an alternate representation ("the direction converted to an angle"); I wouldn't want to have Point2d.toTuple or Point2d.toCoordinates, for example, but somehow Direction2d.toAngle feels more natural than Direction2d.angle. Similarly should SketchPlane3d.plane be SketchPlane3d.toPlane? Does it make more sense to talk about "the plane of a sketch plane" or "converting a sketch plane to a plane"?
  • It may be more clear to rename some or all of the with functions to have more descriptive names. In some cases like Axis#d.fromOriginAndDirection this might also imply switching from a single record argument to multiple 'plain' arguments. Possible renames:
    • Frame2d.with to Frame2d.fromOriginAndXDirection (and add Frame2d.fromOriginAndYDirection)
    • Frame3d.with to Frame3d.fromOriginAndZDirection (and add Frame3d.fromOriginAndXDirection, Frame3d.fromOriginAndYDirection)
    • Vector#d.with to Vector#d.fromLengthAndDirection
    • BoundingBox#d.with to BoundingBox#d.fromExtrema
    • Interval.with to Interval.fromExtrema
    • Axis#d.with to Axis#d.fromOriginAndDirection
    • Plane3d.with to Plane3d.fromPointAndNormal
    • Arc2d.with to Arc2d.fromCenterAndStart
    • Circle#d.with to Circle#d.fromCenterAndRadius (although Circle3d also requires an axialDirection parameter...)
    • Sphere3d.with to Sphere3d.fromCenterAndRadius
    • Direction3d.with to Direction3d.fromSphericalAngles
    • Ellipse2d.with to Ellipse2d.fromCenter, or perhaps change to Ellipse2d.centeredOn : Frame2d -> { xRadius : Float, yRadius : Float } -> Ellipse2d
    • EllipticalArc2d.with to EllipticalArc2d.fromCenter, or perhaps change to EllipticalArc2d.centeredOn : Frame2d -> { xRadius : Float, yRadius : Float, startAngle : Float, endAngle : Float } -> EllipticalArc2d
    • SketchPlane3d.with to SketchPlane3d.fromPointAndNormal
    • Rectangle2d.with to Rectangle2d.axisAligned

Switch from Maybe to Result

Currently there are several constructor/factory functions such as Arc2d.fromEndpoints that return Maybe values if construction fails (invalid arguments given, no solution found, etc.). Ideally many of these should be switched to return Result values instead with a custom error type that indicates the failure reason; for example, @folkertdev's proposal for EllipticalArc2d.fromEndpoints:

type ArcError
    = IdenticalStartAndEndpoint -- omit this segment
    | RadiusIsZero -- (i.e. endpoint == center) draw straight line segment 
    | NoSolution -- maybe point to docs on how svg deals with this

One possible modification: change the NoSolution case to actually include the 'best approximation' elliptical arc with X and Y radius scaled to make arc construction possible. This way attempting to construct an ellipse with invalid/impossible radii is an Err, but the user can explicitly choose to fall back to the scaled ellipse if they want (which is what browser SVG engines do, since that's what's mandated by the SVG spec).

Any plans for affine transformation matrixes?

This library looks really great - awesome features and superb documentation.

There's one thing I can't seem to find though - matrix transformations (for affine transformations). I like how rotateBy, translateBy, etc is really intuitive and doesn't require a deep understanding of linear algebra, but I'm porting some geographic JS code that just has a hardcoded 3x3 transformation matrix. I have no idea what it is doing (translating, scaling or rotating) as my linear algebra is extremely rusty. What I'd like is a function like http://package.elm-lang.org/packages/elm-community/linear-algebra/1.2.1/Math-Matrix4#transform but for 2D as well as 3D. From memory stacking linear tranformations and then applying the resulting matrix to a point is also potentially faster than applying transforms to points one at a time, but I could be wrong about that. Perhaps matrix transforms could be added as an advanced feature?

Problems importing `OpenSolid.Rectangle2d`

Hey, since 2.1.0 I have a weird problem importing OpenSolid.Rectangle2d. It can be reproduced by visiting https://ellie-app.com/qnhLYgjy8a1/1 and editing the line

import OpenSolid.Arc2d

so it reads

import OpenSolid.Rectangle2d

and trying to compile. Ellie wouldn’t let me save or share the project with the latter in it πŸ€·β€β™‚οΈ

This is also happening locally in my project. I didn’t notice any other weird un-importable modules like this πŸ™‚

Add point-in-polygon test

Probably best approach is to use winding number: http://geomalgorithms.com/a03-_inclusion.html

However, this is still linear time in the number of polygon edges. Is there a more efficient approach for large, complex polygons? One idea would be to construct a triangulation of the polygon, store the triangles in an R-tree and then use that to determine which (if any) of the triangles a given point is contained in. However, for (say) the triangulation of a polygon approximating a circle (with many thin triangles crossing the entire polygon) there may still be O(n) triangles whose bounding box overlaps with any given point.

Add Interval sin/cos

This is the last remaining piece of work from #1. I have an old implementation in Scala starting here that may be useful.

module Interval exposing (...sin, cos, ...)

sin : Interval -> Interval
cos : Interval -> Interval

Convert documentation examples to tests

I started converting a bunch of the examples shown in the documentation to tests; for example see commits 8025fe7 and be3dbb1. It would be great if someone could help converting over more of them! As nice as it would be, I don't think it's feasible to use something like stoeffel/elm-verify-examples since most tests in this package require using custom comparison functions that include a small numerical tolerance for roundoff.

The general procedure would be to find examples in a particular module, convert them to tests in the corresponding tests file, then run the tests locally using elm-test to verify that they pass.

Potential breaking changes for 2.0

This is a meta-issue for potential breaking changes to make for elm-geometry 2.0 version. Feel free to post suggestions in the comments and I'll edit this issue text.

Package split

Splitting out points, vectors, directions, axes, planes, frames and bounding boxes into their own elm-geometry-primitives package could make that package more attractive for use in other Elm projects (if nothing else, it would require less frequent version changes). Splitting this package into two smaller packages would certainly be a major version change (although it shouldn't actually require any code changes, just modifications to dependencies in elm-package.json).

Switch from Maybe to Result

Currently there are several constructor/factory functions such as Arc2d.withRadius that return Maybe values if construction fails (invalid arguments given, no solution found, etc.). Ideally many of these should be switched to return Result values instead with a custom error type that indicates the failure reason; for example, @folkertdev's proposal for EllipticalArc2d.fromEndpoints:

type ArcError
    = IdenticalStartAndEndpoint -- omit this segment
    | RadiusIsZero -- (i.e. endpoint == center) draw straight line segment 
    | NoSolution -- maybe point to docs on how svg deals with this

One possible modification: change the NoSolution case to actually include the 'best approximation' elliptical arc with X and Y radius scaled to make arc construction possible. This way attempting to construct an ellipse with invalid/impossible radii is an Err, but the user can explicitly choose to fall back to the scaled ellipse if they want (which is what browser SVG engines do, since that's what's mandated by the SVG spec).

Simplified bounding box overlap/separation functions

Tracked in #54.

Renames

  • #59: Rename BoundingBox#d.centroid to centerPoint
  • #65: Rename Frame2d.xy/Frame3d.xyz to Frame#d.atOrigin

Overhaul curve evaluation functions

Draft API:

module Geometry.Parameter

type Resolution
    = Resolution Float.Range.Resolution

type OutOfRange
    = OutOfRange

numSteps : Int -> Resolution
maxStepSize : Float -> Resolution
map : (Float -> a) -> Resolution -> List a
forEach : Resolution -> (Float -> a) -> List a
values : Resolution -> List Float
range : Resolution -> Float.Range
module Geometry.ArcLength

type Resolution
    = Resolution Float.Range.Resolution

type OutOfRange
    = OutOfRange

numSteps : Int -> Resolution
maxStepSize : Float -> Resolution
module Geometry.Sample

type Error 
    = OutOfRange
    | DegenerateInput
module CubicSpline2d

points : Parameter.Resolution -> CubicSpline2d -> List Point2d
samples : Parameter.Resolution -> CubicSpline2d -> List ( Point2d, Direction2d )
evenlySpacedPoints : Accuracy -> ArcLength.Resolution -> CubicSpline2d -> List Point2d
evenlySpacedSamples : Accuracy -> ArcLength.Resolution -> CubicSpline2d -> List ( Point2d, Direction2d )

pointOn : CubicSpline2d -> Float -> Result Parameter.OutOfRange Point2d
sample : CubicSpline2d -> Float -> Result Sample.Error ( Point2d, Direction2d )
pointAlong : ArcLengthParameterized -> Float -> Result ArcLength.OutOfRange Point2d
sampleAlong : ArcLengthParameterized -> Float -> Result Sample.Error ( Point2d, Direction2d )

arcLength : Accuracy -> CubicSpline2d -> Float
arcLengthParameterized : Accuracy -> CubicSpline2d -> ArcLengthParameterized

-- Derivative functions: only for concrete curve types, not Curve2d/Curve3d?
derivative : Int -> CubicSpline2d -> Float -> Vector2d -- only internal?
firstDerivativeMagnitude : CubicSpline2d -> Float -> Float
maxSecondDerivativeMagnitude : CubicSpline2d -> Float

Remove inline HTML from docs

Elm 0.19 no longer seems to support inline HTML in Markdown documentation. This means that several changes will be necessary:

  • Replace the <image> tags at the top of each module with plain Markdown image references (will require resizing the source images, since manually specifying a desired size will no longer be an option)
  • Replace the interactive elliptical arc example with one or more static images
  • Replace all occurrences of <code>...&nbsp;...</code> with something else (use Markdown inline code and accept that it might get broken over multiple lines, or switch to code blocks)
  • Review documentation for any other occurrences

Customize how polygon interiors are triangulated

Allow customization of how interiors of polygons are triangulated, for example ensuring that no triangle is larger than a certain value. This will be important for meshing curved surfaces, where the triangulation depends on surface curvature as well as the shape of the edges.

It would be great if the polygon triangulation algorithm could be extended by simply adding a bunch of standalone extra triangulation points - this would be a new vertex type in the monotone triangulation algorithm in addition to the existing start, end, split, merge, left and right. Ideally the algorithm would simply ignore these vertices if they were outside the polygon (perhaps possible by just seeing where the vertex is in relation to the current edges contacting the sweep line!).

Voronoi Diagrams/Delaunay Triangulations

This proposes adding support for computing Voronoi Diagrams or Delaunay Triangulations or both.

Voronoi diagrams have many usecases, mine relates to uses in data visualization - as I would like to expose an interface aimed at those usecases in elm-visualization.

An important usecase there is to be able to quickly compute the closest point to another point (optionally limited by a radius), for example to support mouse interactions with a data set with many points. However it can be used also to help compute labels for a scatterplot.

These usecases impact a design of an API:

  1. The resulting Voronoi should be able to serve as an index giving a fast way to find the nearest site to a point.
  2. The api should be fast. AFAIK the runtime complexity should be O(nlogn).
  3. It would help if the API allows incremental construction or even modification rather than rebuilding the Voronoi from scratch every time the data changes.

From my brief research on this topic, I found this algorithm that should satisfy 2 and 3 above.

This algorithm needs to compute the convex hull, which seems like that would be a useful addition in its own right.


Some other things that might be worth considering:

  1. Supporting different distance metrics. An obvious use case seems geographical voronoi and weighted euclidian - i.e. for taking an importance metric into consideration (I believe this is useful for Voronoi treemaps).
  2. Supporting 3 or more dimensions. I personally have no usecase for this (apart from the weighted usecase which can be expressed as a 3 dimensional Voronoi projected onto a plane).

Resources:

Add 'section' functions to curve types

Possible signatures:

CubicSpline2d.section : 
    Float
    -> Float
    -> CubicSpline2d units coordinates
    -> CubicSpline2d units coordinates
-- or CubicSpline2d.sectionFrom

-- Clamp to valid range
CubicSpline2d.sectionAlong :
    ArcLengthParameterized units coordinates
    -> Quantity Float units
    -> Quantity Float units
    -> CubicSpline2d units coordinates

Extend Polygon2d to support holes

Keep existing API the same but add something like

Polygon2d.with : { boundary : List Point2d, holes : List (List Point2d) } -> Polygon2d

This will require changing the internal implementation to store the boundary and holes separately.

Add Tolerance module

Initial design:

type Tolerance
    = MaxError Float
    | MaxAngle Float
    | MinNumSegments Int
    | AllOf (List Tolerance)

Tolerance.maxError : Float -> Tolerance

Tolerance.maxAngle : Float -> Tolerance

Tolerance.minNumSegments : Int -> Tolerance

Tolerance.allOf : List Tolerance -> Tolerance

-- determine a required number of segments given a
-- max second derivative magnitude and a tolerance
Tolerance.numSegments : Float -> Tolerance -> Int

Should be able to be used by both curves and surfaces.

LineSegment2d.intersectionPoint is occasionally non-symmetric

Example failure: https://travis-ci.org/opensolid/geometry/builds/299799590

↓ LineSegment2d

βœ— Intersection should be symmetric

Given (LineSegment2d (Point2d (-10,0.000001),Point2d (10,0.000001)),LineSegment2d (Point2d (-10,0.0000510582621136058),Point2d (10,0)))

    Just (Point2d (9.608290623846546,0.0000010000000000000004))

    β•·

    β”‚ Expect.equal

    β•΅

    Just (Point2d (9.608290623846546,0.000001))

Similarly https://travis-ci.org/opensolid/geometry/builds/321986057.

Tiny difference but a difference nonetheless!

Port web content from opensolid.github.io

All images and interactive examples from opensolid.github.io should be moved to ianmackenzie.github.io/elm-geometry. As suggested in opensolid/opensolid.github.io#1, this move should probably also involve figuring out a structure that allows for a simple home page that links to the various examples, future guide documents etc. and generally makes the various contents more discoverable.

Simplify overlap/separation checking functions?

Switch from

overlappingBy : Order -> Float -> BoundingBox2d -> BoundingBox2d -> Bool
separatedBy : Order -> Float -> BoundingBox2d -> BoundingBox2d -> Bool

to

overlappingByAtLeast : Float -> BoundingBox2d -> BoundingBox2d -> Bool
separatedByAtLeast : Float -> BoundingBox2d -> BoundingBox2d -> Bool

where overlappingByAtLeast 0 includes touching and separatedByAtLeast 0 excludes touching.

Several advantages:

  • Simpler (can cut down on the currently-massive docs!)
  • More efficient (don't have to switch on Order value in the implementation, helper functions like squaredSeparationAmount can be specialized for what is currently just the GT case)
  • Arguably clearer
  • Avoids weird EQ case
  • Nicely symmetric with intersects: overlappingByAtLeast 0 means intersecting, separatedByAtLeast 0 means not intersecting

This wouldn't allow checking for strict intersection, though (overlap of any amount strictly greater than zero). Perhaps overlappingByMoreThan and separatedByLessThan?

Add Scalar module

Have a couple convenient functions for dealing with scalar vaules:

  • equalWithin : Float -> Float -> Float -> Bool
  • hull : Float -> Float -> Interval (see #1)
  • hullOf : List Float -> Interval

Add Interval type and module

Will be used for return value of functions like LineSegment3d.distanceAlong.

Type should be type Interval = Interval { minValue : Float, maxValue : Float }.

Functions should include:

  • singleton : Float -> Interval
  • minValue : Interval -> Float
  • maxValue : Interval -> Float
  • width : Interval -> Float
  • midpoint : Interval -> Float
  • contains : Float -> Interval -> Bool
  • intersects : Interval -> Interval -> Bool
  • isContainedIn : Interval -> Interval -> Bool
  • hull : Interval -> Interval -> Interval
  • hullOf : List Interval -> Maybe Interval
  • intersection : Interval -> Interval -> Maybe Interval
  • sin : Interval -> Interval, cos : Interval -> Interval (useful for implementing Arc2d.boundingBox)

Add Sphere3d type and module

type Sphere3d = Sphere3d { centerPoint : Point3d, radius : Float }

This would be very similar to the Circle2d type and module, so that could be used as an example/template. A couple of interesting Sphere3d-specific additions:

projectOnto : Plane3d -> Sphere3d -> Circle3d
projectInto : SketchPlane3d -> Sphere3d -> Circle2d

Potential breaking changes for 2.0

This is a meta-issue for potential breaking changes to make for a 2.0 version. Feel free to post suggestions in the comments and I'll edit this issue text.

Module naming

I used prefixed module names like OpenSolid.Point2d to avoid potential naming conflicts with modules from other packages, but based on elm/compiler#1625 it seems that this might be considered "silly module renaming". Should the prefixes be removed? I think they should remain in a few cases like OpenSolid.Geometry.Types and OpenSolid.Geometry.Decode, but otherwise module names could be switched to just plain Point2d, Triangle2d etc.

This would be more consistent with other Elm packages - elm-community/elm-test uses top-level Expect, Fuzz and Test modules, mdgriffith/style-elements uses top-level Style and Element modules, terezka/elm-plot uses a top-level Plot module, etc.

Additionally, if the non-prefixed names don't work out and run into conflicts with other packages, then these conflicts will provide additional data points for elm/compiler#1625.

Renames

I used things like radialDistanceFrom to disambiguate from plain distanceFrom (which takes another point as argument) while keeping the nice 'read as a phrase' quality. However, I've come to think these names are a little too 'cute'/obscure and it would probably be better to use more 'boring' names, even though Point3d.distanceFromAxis axis doesn't read quite as nicely as Point3d.radialDistanceFrom axis.

  • Point3d.radialDistanceFrom -> Point3d.distanceFromAxis
  • Point3d.squaredRadialDistanceFrom -> Point3d.squaredDistanceFromAxis
  • Point3d.projectRadiallyOnto -> Point3d.projectOntoAxis

Removals

  • Direction#d.scaleBy: use Vector#d.in_ instead (sounds a bit weird to 'scale a direction')
  • Perhaps move primitive types (points, vectors, directions, axes, planes, frames, bounding boxes) to their own opensolid/primitives package? Could make them more appealing for people who don't want to depend on a large package or (shudder) 'framework'...
  • Remove Circle2d.area and Circle3d.area and add Disk2d and Disk3d types/modules with those functions? Then a 'circle' would be a curve (that has no area) and a 'disk' would be a closed circle (that has area). Circles and disks would probably both be considered to have 'circumference'. This clears up some ambiguity - for example right now should a WebGL Render3d.circle function render a curve or a surface?

Type changes

  • Add a normalDirection field to SketchPlane3d? This would allow sketch planes to be flipped, and would help in defining surfaces and bodies. It would also be a bit more consistent with Plane3d in that the normal direction would get mirrored whenever mirroring the plane. However, there could be some weird edge cases where semantics of left-handed sketch planes (where the normal direction is opposite to the cross product of the X and Y directions) are not clear...

Signature changes

  • [Point,Vector]#d.interpolate: instead of a synonym for interpolateFrom : a -> a -> Float -> a, switch to ( a, a ) -> Float -> a and add interpolateBy : Float -> ( a, a ) -> a (both designed for different applications of chaining).
  • Change Arc2d.fromEndpoints to take a record with startPoint, endPoint, radius, windingDirection and whichHalf (?) fields instead of separate arguments? More clear, more similar to Arc2d.with...

Opaque types?

Should some or all of the types in this package be switched to opaque with constructor functions?

Pros

  • Generally follows best practice of making types opaque
  • Allows internal representation to change in the future (possibly to some optimized native representation if necessary)
  • More consistent, especially for beginners: functions are lower case, types and modules are upper case
  • Reads better in pipelines: Math.Vector3.toTuple >> Point3d.fromComponents is pretty readable while Math.Vector3d.toTuple >> Point3d seems a bit awkward
  • Could hide the Types module and switch to more conventional import OpenSolid.Point2d as Point2d exposing (Point2d) etc.

Cons

  • More verbose (for example Point3d.fromCoordinates ( 1, 2, 3 ) instead of Point3d ( 1, 2, 3 ))
  • Not clear how to elegantly construct more complex objects (Axis3d.with { originPoint = ..., direction = ... }? Axis3d.axis3d { originPoint = ..., direction = ...}, expecting that people would expose the axis3d function and use it unqualified?)
  • Less obvious how things work internally - for example if you understand how Elm works you can deduce for yourself that Direction3d ( 2, 0, 0 ) would just construct an invalid direction (since there's no way to customize the behavior of a constructor), while you might guess that Direction3d.fromComponents ( 2, 0, 0 ) would perform normalization. It might be nice for constructors to perform normalization, but then they should really also return Maybe or Result values. I kind of like the fact that having direct Direction3d, Frame3d constructors etc. allows you to directly construct objects with no normalization etc. overhead, but makes it pretty clear that you are then responsible for correctness yourself.

Add Elliptical Arcs

My primary use case for elliptical arcs is working with svg paths.

A full description of how svg uses and defines elliptical arcs is part of the svg spec.

The spec describes two parameterizations:

Both store

  • xAxisOffset (written as phi), the rotation offset relative to the x-axis
  • the major and minor radii

endpoint

  • stores two points (the start and end point)
  • the arc flag (large or small arc)
  • the sweep flag (direction of the arc - clockwise or counter-clockwise)

center

  • stores one point (the center)
  • stores two angles - the starting angle and the delta-theta

In svg path syntax, ellipses are described in the endpoint parameterization, and the endpoints are also
used in correcting out of range radii. The rest of the math uses the center parameterization.


We discussed about constructors, here are some ideas in types.
type EllipticalArc2d = ... 

-- constructor ideas 
with
    :  { centerPoint : Point2d, startPoint : Point2d, sweptAngle : Float, xAxisRotate Float, radii : (Float, Float) }
    -> Maybe EllipticalArc2d

fromAngles 
    :  { centerPoint : Point2d, startAngle : Float, sweptAngle : Float, xAxisRotate : Float, radii : (Float, Float) }
    -> EllipticalArc2d

fromEndpoints
    :  { startPoint : Point2d, endPoint : Point2d
   , radii : (Float, Float), xAxisRotate : Float, sweptAngle : SweptAngle }
    -> Maybe EllipticalArc2d

-- for conversion to svg 
endpointParameterizaton : 
    EllipticalArc2d
     -> { startPoint : Point2d, endPoint : Point2d, radii : (Float, Float), xAxisRotate : Float, sweptAngle : SweptAngle }

Because the radius cannot be deduced, the with constructor needs to return a maybe in case
the radii and the xAxisRotate can't make an ellipse that goes through the point.

The rest of the api would mirror Arc2d: properties, evaluation, transforms, arc length parameterization.

Add self intersection for polygon and polyline

Computing self intersection for a polygon or a polyline is not obvious, even if an intersection function for line segments is provided. I recently had to do this so I wonder if that's something that would be interesting for OpenSolid. I have implemented a function with this interface:

selfIntersection : Polygon2d -> Maybe Point2d

It "stops" as soon as one intersection point is found. This was because I only wanted to know if the polygon was simple or not. I used (almost) the Shamos-Hoey algorithm which is a simplification of Bentley-Ottmann intersection algorithm stopping at first intersection.

Let me know if that's something that you'd like to add to OpenSolid.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.