├── .travis.yml ├── .gitattributes ├── tests ├── elm-verify-examples.json └── Tests │ ├── Literals.elm │ ├── Triangle2d.elm │ ├── Frame2d.elm │ ├── SketchPlane3d.elm │ ├── Frame3d.elm │ ├── LineSegment3d.elm │ ├── Vector3d.elm │ ├── Circle3d.elm │ ├── Circle2d.elm │ ├── EllipticalArc3d.elm │ ├── Arc3d.elm │ ├── Block3d.elm │ ├── Rectangle2d.elm │ ├── DelaunayTriangulation2d.elm │ ├── Cone3d.elm │ ├── Arc2d.elm │ ├── Cylinder3d.elm │ ├── RationalCubicSpline3d.elm │ ├── RationalCubicSpline2d.elm │ ├── RationalQuadraticSpline3d.elm │ ├── EllipticalArc2d.elm │ ├── RationalQuadraticSpline2d.elm │ ├── VoronoiDiagram2d.elm │ ├── Vector2d.elm │ ├── CubicSpline3d.elm │ └── Polyline2d.elm ├── elm-test-wrapper.sh ├── sandbox ├── src │ ├── EllipticalArc2dBoundingBox.elm │ ├── Arc2dBoundingBox.elm │ ├── ReleaseNotes │ │ ├── DefaultParameterization.elm │ │ ├── Common.elm │ │ └── ArcLengthParameterization.elm │ ├── VisualTest.elm │ ├── Ellipse2dDistanceAlongAxis.elm │ ├── EllipticalArc2dDistanceAlongAxis.elm │ ├── NurbsCircle.elm │ ├── DistanceAlongAxis2d.elm │ ├── RegionTriangulation.elm │ ├── ConvexHull.elm │ ├── BoundingBox2dTest.elm │ ├── BSplines.elm │ ├── PointInPolygon.elm │ └── DelaunayTriangulation.elm └── elm.json ├── doc ├── images │ ├── elm-package.json │ ├── FillsAndStrokes.elm │ ├── HermiteCubicSpline.elm │ └── EllipticalArc2d │ │ ├── With1.elm │ │ └── With2.elm └── interactive │ └── elm-package.json ├── AUTHORS ├── src ├── Unsafe │ ├── Direction2d.elm │ └── Direction3d.elm ├── Quantity │ └── Extra.elm ├── Curve.elm ├── SweptAngle.elm ├── Polygon2d │ └── EdgeSet.elm ├── Surface3d.elm └── QuadraticSpline1d.elm ├── benchmarks ├── elm.json ├── PointSet.elm ├── ArcLengthParameterization.elm ├── DelaunayTriangulation.elm ├── PolygonTriangulation.elm └── CoordinateAccess.elm ├── .gitignore ├── PULL_REQUEST_TEMPLATE.md ├── flake.lock ├── elm.json ├── flake.nix ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elm 2 | dist: focal 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | * text eol=lf 4 | -------------------------------------------------------------------------------- /tests/elm-verify-examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "../src", 3 | "tests": [ 4 | "Sphere3d" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /elm-test-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT_DIR="`dirname $0`" 3 | find $SCRIPT_DIR/tests/VerifyExamples -type f -exec sed -i "s/Expect.equal/Expect.equalWithinTolerance/" {} \; 4 | elm-test 5 | -------------------------------------------------------------------------------- /sandbox/src/EllipticalArc2dBoundingBox.elm: -------------------------------------------------------------------------------- 1 | module EllipticalArc2dBoundingBox exposing (main) 2 | 3 | import BoundingBox2dTest 4 | import Drawing2d 5 | import EllipticalArc2d 6 | import Random2d 7 | 8 | 9 | main : BoundingBox2dTest.Program 10 | main = 11 | BoundingBox2dTest.program 12 | Random2d.ellipticalArc 13 | EllipticalArc2d.boundingBox 14 | (Drawing2d.ellipticalArc []) 15 | -------------------------------------------------------------------------------- /tests/Tests/Literals.elm: -------------------------------------------------------------------------------- 1 | module Tests.Literals exposing (just, ok) 2 | 3 | 4 | just : Maybe a -> a 5 | just maybe = 6 | case maybe of 7 | Just value -> 8 | value 9 | 10 | Nothing -> 11 | Debug.todo "Maybe is Nothing when it should be Just" 12 | 13 | 14 | ok : Result x a -> a 15 | ok result = 16 | case result of 17 | Ok value -> 18 | value 19 | 20 | Err _ -> 21 | Debug.todo "Result is Err when it should be Ok" 22 | -------------------------------------------------------------------------------- /doc/images/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Documentation images", 4 | "repository": "https://github.com/opensolid/geometry.git", 5 | "license": "MPL-2.0", 6 | "source-directories": [ 7 | ".", 8 | "../../src", 9 | "../../../svg/src", 10 | "../../../parametric/src", 11 | "../../../mesh/src" 12 | ], 13 | "exposed-modules": [], 14 | "dependencies": { 15 | "Skinney/elm-array-exploration": "2.0.5 <= v < 3.0.0", 16 | "elm-community/basics-extra": "2.2.0 <= v < 3.0.0", 17 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 18 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 19 | "elm-lang/svg": "2.0.0 <= v < 3.0.0" 20 | }, 21 | "elm-version": "0.18.0 <= v < 0.19.0" 22 | } 23 | -------------------------------------------------------------------------------- /tests/Tests/Triangle2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Triangle2d exposing (triangleContainsOwnCentroid) 2 | 3 | import Expect 4 | import Geometry.Random as Random 5 | import Quantity 6 | import Test exposing (Test) 7 | import Test.Random as Test 8 | import Triangle2d 9 | 10 | 11 | triangleContainsOwnCentroid : Test 12 | triangleContainsOwnCentroid = 13 | Test.check "non-zero area triangle contains its own centroid" 14 | Random.triangle2d 15 | (\triangle -> 16 | let 17 | centroid = 18 | Triangle2d.centroid triangle 19 | 20 | area = 21 | Triangle2d.area triangle 22 | in 23 | if area == Quantity.zero || Triangle2d.contains centroid triangle then 24 | Expect.pass 25 | 26 | else 27 | Expect.fail "non-zero area triangle did not contain its own centroid" 28 | ) 29 | -------------------------------------------------------------------------------- /doc/interactive/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Interactive documentation examples", 4 | "repository": "https://github.com/opensolid/geometry.git", 5 | "license": "MPL-2.0", 6 | "source-directories": [ 7 | ".", 8 | "../../src", 9 | "../../../svg/src", 10 | "../../../mesh/src", 11 | "../../../parametric/src" 12 | ], 13 | "exposed-modules": [], 14 | "dependencies": { 15 | "Skinney/elm-array-exploration": "2.0.5 <= v < 3.0.0", 16 | "debois/elm-dom": "1.2.3 <= v < 2.0.0", 17 | "elm-community/basics-extra": "2.2.0 <= v < 3.0.0", 18 | "elm-lang/animation-frame": "1.0.1 <= v < 2.0.0", 19 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 20 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 21 | "elm-lang/keyboard": "1.0.1 <= v < 2.0.0", 22 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0", 23 | "elm-lang/svg": "2.0.0 <= v < 3.0.0" 24 | }, 25 | "elm-version": "0.18.0 <= v < 0.19.0" 26 | } 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | This is a (likely incomplete) list of the people who have contributed to 2 | the elm-geometry package. If you submit a patch to this repository, please 3 | add your name to the end of this list! 4 | 5 | Ian Mackenzie 6 | Matthieu Pizenberg (intersections) 7 | Folkert de Vries (length parameterization, delaunay triangulation) 8 | Felix Scheinost (Sphere3d) 9 | Jakub Hampl (convex hull, point in polygon) 10 | Dave Cameron (centroids) 11 | Lalit Umbarkar (bounding box expand/offset) 12 | Sebastian Kazenbroot-Guppy (circle-bbox intersections) 13 | Andrey Kuzmin (arc midpoint, polygon centroid, interval distances for line segments, Cone3d) 14 | Suzanne Wood (intersection of axis and sphere) 15 | Guilherme Riekes Belmonte (Ellipsoid3d, scaleTo and physics for Vector2d and Vector3d) 16 | Olaf Delgado-Friedrichs (axis/triangle intersection) 17 | -------------------------------------------------------------------------------- /sandbox/src/Arc2dBoundingBox.elm: -------------------------------------------------------------------------------- 1 | module Arc2dBoundingBox exposing (main) 2 | 3 | import Angle 4 | import Arc2d exposing (Arc2d) 5 | import BoundingBox2d exposing (BoundingBox2d) 6 | import BoundingBox2dTest 7 | import Drawing2d 8 | import LineSegment2d 9 | import Random exposing (Generator) 10 | import Random2d 11 | 12 | 13 | arcGenerator : BoundingBox2d units coordinates -> Generator (Arc2d units coordinates) 14 | arcGenerator bounds = 15 | Random.weighted 16 | ( 0.8, Random2d.circularArc bounds ) 17 | [ ( 0.2 18 | , Random2d.lineSegment bounds 19 | |> Random.map 20 | (\lineSegment -> 21 | Arc2d.from 22 | (LineSegment2d.startPoint lineSegment) 23 | (LineSegment2d.endPoint lineSegment) 24 | (Angle.degrees 0) 25 | ) 26 | ) 27 | ] 28 | |> Random.andThen identity 29 | 30 | 31 | main : BoundingBox2dTest.Program 32 | main = 33 | BoundingBox2dTest.program 34 | arcGenerator 35 | Arc2d.boundingBox 36 | (Drawing2d.arc []) 37 | -------------------------------------------------------------------------------- /src/Unsafe/Direction2d.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module Unsafe.Direction2d exposing (unsafeXyIn) 11 | 12 | import Geometry.Types exposing (..) 13 | 14 | 15 | unsafeXyIn : Frame2d units globalCoordinates { defines : localCoordinates } -> Float -> Float -> Direction2d globalCoordinates 16 | unsafeXyIn (Frame2d frame) x y = 17 | let 18 | (Direction2d i) = 19 | frame.xDirection 20 | 21 | (Direction2d j) = 22 | frame.yDirection 23 | in 24 | Direction2d 25 | { x = x * i.x + y * j.x 26 | , y = x * i.y + y * j.y 27 | } 28 | -------------------------------------------------------------------------------- /tests/Tests/Frame2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Frame2d exposing (globalToGlobal, localToLocal) 2 | 3 | import Frame2d 4 | import Geometry.Expect as Expect 5 | import Geometry.Random as Random 6 | import Point2d 7 | import Test exposing (Test) 8 | import Test.Random as Test 9 | 10 | 11 | globalToGlobal : Test 12 | globalToGlobal = 13 | let 14 | description = 15 | "Global -> local -> global conversion round-trips properly" 16 | 17 | expectation frame point = 18 | point 19 | |> Point2d.relativeTo frame 20 | |> Point2d.placeIn frame 21 | |> Expect.point2d point 22 | in 23 | Test.check2 description Random.frame2d Random.point2d expectation 24 | 25 | 26 | localToLocal : Test 27 | localToLocal = 28 | let 29 | description = 30 | "Local -> global -> local conversion round-trips properly" 31 | 32 | expectation frame point = 33 | point 34 | |> Point2d.placeIn frame 35 | |> Point2d.relativeTo frame 36 | |> Expect.point2d point 37 | in 38 | Test.check2 description Random.frame2d Random.point2d expectation 39 | -------------------------------------------------------------------------------- /sandbox/src/ReleaseNotes/DefaultParameterization.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module ReleaseNotes.DefaultParameterization exposing (main) 11 | 12 | import CubicSpline2d 13 | import Drawing2d 14 | import Geometry.Parameter as Parameter 15 | import Html exposing (Html) 16 | import ReleaseNotes.Common exposing (..) 17 | 18 | 19 | main : Html Never 20 | main = 21 | let 22 | points = 23 | CubicSpline2d.pointsOn spline (Parameter.numSteps numSegments) 24 | in 25 | Drawing2d.toHtml renderBounds 26 | [] 27 | [ Drawing2d.cubicSpline spline 28 | , Drawing2d.dots points 29 | ] 30 | -------------------------------------------------------------------------------- /benchmarks/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | ".", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm/random": "1.0.0", 14 | "elm-explorations/benchmark": "1.0.2", 15 | "ianmackenzie/elm-float-extra": "1.1.0", 16 | "ianmackenzie/elm-interval": "2.0.0", 17 | "ianmackenzie/elm-triangular-mesh": "1.0.4", 18 | "ianmackenzie/elm-units": "2.7.0", 19 | "ianmackenzie/elm-units-interval": "2.1.0" 20 | }, 21 | "indirect": { 22 | "BrianHicks/elm-trend": "2.1.3", 23 | "elm/json": "1.1.3", 24 | "elm/regex": "1.0.0", 25 | "elm/time": "1.0.0", 26 | "elm/url": "1.0.0", 27 | "elm/virtual-dom": "1.0.2", 28 | "mdgriffith/style-elements": "5.0.2", 29 | "robinheghan/murmur3": "1.0.0" 30 | } 31 | }, 32 | "test-dependencies": { 33 | "direct": {}, 34 | "indirect": {} 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Tests/SketchPlane3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.SketchPlane3d exposing 2 | ( normalDirectionIsValid 3 | , randomlyGeneratedSketchPlanesAreValid 4 | ) 5 | 6 | import Direction3d 7 | import Expect 8 | import Geometry.Expect as Expect 9 | import Geometry.Random as Random 10 | import SketchPlane3d 11 | import Test exposing (Test) 12 | import Test.Random as Test 13 | 14 | 15 | randomlyGeneratedSketchPlanesAreValid : Test 16 | randomlyGeneratedSketchPlanesAreValid = 17 | Test.check "Randomly generated sketch planes are valid" 18 | Random.sketchPlane3d 19 | Expect.validSketchPlane3d 20 | 21 | 22 | normalDirectionIsValid : Test 23 | normalDirectionIsValid = 24 | Test.check "Sketch plane normal direction is valid and is perpendicular to both basis directions" 25 | Random.sketchPlane3d 26 | (\sketchPlane -> 27 | SketchPlane3d.normalDirection sketchPlane 28 | |> Expect.all 29 | [ Expect.validDirection3d 30 | , Expect.direction3dPerpendicularTo (SketchPlane3d.xDirection sketchPlane) 31 | , Expect.direction3dPerpendicularTo (SketchPlane3d.yDirection sketchPlane) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /src/Quantity/Extra.elm: -------------------------------------------------------------------------------- 1 | module Quantity.Extra exposing 2 | ( aXbY 3 | , lOverTheta 4 | , rCosTheta 5 | , rSinTheta 6 | , rTheta 7 | , scaleAbout 8 | ) 9 | 10 | import Angle exposing (Angle) 11 | import Quantity exposing (Quantity(..)) 12 | 13 | 14 | aXbY : Float -> Quantity Float units -> Float -> Quantity Float units -> Quantity Float units 15 | aXbY a (Quantity x) b (Quantity y) = 16 | Quantity (a * x + b * y) 17 | 18 | 19 | scaleAbout : Quantity number units -> number -> Quantity number units -> Quantity number units 20 | scaleAbout (Quantity x0) scale (Quantity x) = 21 | Quantity (x0 + scale * (x - x0)) 22 | 23 | 24 | rTheta : Quantity Float units -> Angle -> Quantity Float units 25 | rTheta (Quantity r) (Quantity theta) = 26 | Quantity (r * theta) 27 | 28 | 29 | lOverTheta : Quantity Float units -> Angle -> Quantity Float units 30 | lOverTheta (Quantity l) (Quantity theta) = 31 | Quantity (l / theta) 32 | 33 | 34 | rCosTheta : Quantity Float units -> Angle -> Quantity Float units 35 | rCosTheta r theta = 36 | r |> Quantity.multiplyBy (Angle.cos theta) 37 | 38 | 39 | rSinTheta : Quantity Float units -> Angle -> Quantity Float units 40 | rSinTheta r theta = 41 | r |> Quantity.multiplyBy (Angle.sin theta) 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff/ 3 | 4 | # elm-repl generated files 5 | repl-temp-* 6 | 7 | *.sublime-workspace 8 | elm.js 9 | documentation.json 10 | 11 | # Idea directory created by Intellij Webstorm 12 | .idea/ 13 | 14 | # elm-verify-examples generated tests 15 | tests/VerifyExamples 16 | 17 | # Local Elm compiler 18 | elm.exe 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | # Windows 50 | # ========================= 51 | 52 | # Windows image file caches 53 | Thumbs.db 54 | ehthumbs.db 55 | 56 | # Folder config file 57 | Desktop.ini 58 | 59 | # Recycle Bin used on file shares 60 | $RECYCLE.BIN/ 61 | 62 | # Windows Installer files 63 | *.cab 64 | *.msi 65 | *.msm 66 | *.msp 67 | 68 | # Windows shortcuts 69 | *.lnk 70 | *.exe 71 | -------------------------------------------------------------------------------- /src/Curve.elm: -------------------------------------------------------------------------------- 1 | module Curve exposing (arcApproximationSegments, numApproximationSegments) 2 | 3 | import Angle exposing (Angle) 4 | import Quantity exposing (Quantity) 5 | 6 | 7 | arcApproximationSegments : { maxError : Quantity Float units, radius : Quantity Float units, sweptAngle : Angle } -> Int 8 | arcApproximationSegments { maxError, radius, sweptAngle } = 9 | if sweptAngle == Quantity.zero then 10 | 1 11 | 12 | else if maxError |> Quantity.lessThanOrEqualTo Quantity.zero then 13 | 0 14 | 15 | else if maxError |> Quantity.greaterThanOrEqualTo (Quantity.twice radius) then 16 | 1 17 | 18 | else 19 | let 20 | maxSegmentAngle = 21 | Quantity.twice (Angle.acos (1 - Quantity.ratio maxError radius)) 22 | in 23 | ceiling (Quantity.ratio (Quantity.abs sweptAngle) maxSegmentAngle) 24 | 25 | 26 | numApproximationSegments : { maxError : Quantity Float units, maxSecondDerivativeMagnitude : Quantity Float units } -> Int 27 | numApproximationSegments { maxError, maxSecondDerivativeMagnitude } = 28 | if maxError |> Quantity.greaterThan Quantity.zero then 29 | let 30 | computedNumSegments = 31 | sqrt (Quantity.ratio maxSecondDerivativeMagnitude (Quantity.multiplyBy 8 maxError)) 32 | in 33 | max (ceiling computedNumSegments) 1 34 | 35 | else 36 | 0 37 | -------------------------------------------------------------------------------- /doc/images/FillsAndStrokes.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module FillsAndStrokes exposing 11 | ( blackStroke 12 | , dashed 13 | , greyFill 14 | , greyStroke 15 | , noFill 16 | , whiteFill 17 | ) 18 | 19 | import Svg 20 | import Svg.Attributes as Attributes 21 | 22 | 23 | whiteFill : Svg.Attribute msg 24 | whiteFill = 25 | Attributes.fill "white" 26 | 27 | 28 | greyFill : Svg.Attribute msg 29 | greyFill = 30 | Attributes.fill "grey" 31 | 32 | 33 | noFill : Svg.Attribute msg 34 | noFill = 35 | Attributes.fill "none" 36 | 37 | 38 | blackStroke : Svg.Attribute msg 39 | blackStroke = 40 | Attributes.stroke "black" 41 | 42 | 43 | greyStroke : Svg.Attribute msg 44 | greyStroke = 45 | Attributes.stroke "grey" 46 | 47 | 48 | dashed : Svg.Attribute msg 49 | dashed = 50 | Attributes.strokeDasharray "5 5" 51 | -------------------------------------------------------------------------------- /sandbox/src/VisualTest.elm: -------------------------------------------------------------------------------- 1 | module VisualTest exposing (Msg(..), onKeyDown, randomValue, update) 2 | 3 | import Browser 4 | import Browser.Events 5 | import Json.Decode as Decode 6 | import Random exposing (Generator) 7 | 8 | 9 | type Msg 10 | = Next 11 | | Previous 12 | 13 | 14 | update : Msg -> Int -> Int 15 | update message current = 16 | case message of 17 | Next -> 18 | current + 1 19 | 20 | Previous -> 21 | max (current - 1) 0 22 | 23 | 24 | onKeyDown : Sub Msg 25 | onKeyDown = 26 | Browser.Events.onKeyDown 27 | (Decode.field "key" Decode.string 28 | |> Decode.andThen 29 | (\key -> 30 | case key of 31 | "ArrowLeft" -> 32 | Decode.succeed Previous 33 | 34 | "ArrowRight" -> 35 | Decode.succeed Next 36 | 37 | "ArrowUp" -> 38 | Decode.succeed Previous 39 | 40 | "ArrowDown" -> 41 | Decode.succeed Next 42 | 43 | _ -> 44 | Decode.fail "Unrecognized key" 45 | ) 46 | ) 47 | 48 | 49 | randomValue : Generator a -> Int -> a 50 | randomValue generator current = 51 | Tuple.first (Random.step generator (Random.initialSeed current)) 52 | -------------------------------------------------------------------------------- /sandbox/src/ReleaseNotes/Common.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module ReleaseNotes.Common exposing (numSegments, renderBounds, spline) 11 | 12 | import BoundingBox2d exposing (BoundingBox2d) 13 | import CubicSpline2d exposing (CubicSpline2d) 14 | import Point2d exposing (Point2d) 15 | 16 | 17 | spline : CubicSpline2d 18 | spline = 19 | CubicSpline2d.fromControlPoints 20 | ( Point2d.fromCoordinates ( 100, 100 ) 21 | , Point2d.fromCoordinates ( 250, 300 ) 22 | , Point2d.fromCoordinates ( 150, 0 ) 23 | , Point2d.fromCoordinates ( 300, 200 ) 24 | ) 25 | 26 | 27 | numSegments : Int 28 | numSegments = 29 | 24 30 | 31 | 32 | renderBounds : BoundingBox2d 33 | renderBounds = 34 | BoundingBox2d.fromExtrema 35 | { minX = 80 36 | , maxX = 320 37 | , minY = 80 38 | , maxY = 220 39 | } 40 | -------------------------------------------------------------------------------- /tests/Tests/Frame3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Frame3d exposing (frameDirectionsAreOrthonormal) 2 | 3 | import Direction3d 4 | import Frame3d 5 | import Geometry.Expect as Expect 6 | import Geometry.Random as Random 7 | import Quantity 8 | import Test exposing (Test) 9 | import Test.Random as Test 10 | import Vector3d 11 | 12 | 13 | frameDirectionsAreOrthonormal : Test 14 | frameDirectionsAreOrthonormal = 15 | Test.check "Frame3d basis directions are orthonormal" 16 | Random.frame3d 17 | (\frame -> 18 | let 19 | xDirectionVector = 20 | Direction3d.toVector (Frame3d.xDirection frame) 21 | 22 | yDirectionVector = 23 | Direction3d.toVector (Frame3d.yDirection frame) 24 | 25 | zDirectionVector = 26 | Direction3d.toVector (Frame3d.zDirection frame) 27 | 28 | tripleProduct = 29 | xDirectionVector 30 | |> Vector3d.cross yDirectionVector 31 | |> Vector3d.dot zDirectionVector 32 | 33 | expectedTripleProduct = 34 | if Frame3d.isRightHanded frame then 35 | Quantity.cubed (Quantity.float 1) 36 | 37 | else 38 | Quantity.cubed (Quantity.float -1) 39 | in 40 | Expect.quantity expectedTripleProduct tripleProduct 41 | ) 42 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Pull request checklist: 2 | 3 | - Is the pull request from a dedicated branch in your fork instead of 4 | `master`? (Preferred, but not mandatory.) 5 | - Have you added yourself to the AUTHORS file? Not mandatory if you'd prefer 6 | to stay anonymous, but don't be shy! 7 | - Is "Allow edits from maintainers" enabled? (There should be a checkbox for 8 | this when you open the pull request.) 9 | - Is the code formatted using the same version of `elm-format` as the rest of 10 | `elm-geometry`? (If running `elm-format` doesn't change any existing code, 11 | then the answer is probably 'yes'.) 12 | - Is code (mostly) wrapped to 80 columns? (It's OK if type annotations and 13 | occasional things like string literals are longer.) 14 | - BONUS POINTS: Have you added tests for new functionality? 15 | - EXTRA BONUS POINTS: Have you added documentation for new functionality? 16 | (Don't worry too much about documentation text - I like that to have a 17 | consistent style, which means writing most of it myself - but if you can 18 | come up with some code examples to use in documentation that would be 19 | fantastic.) 20 | 21 | Don't worry too much if you're not sure if one of the above is true, or if you 22 | know it's not true but aren't sure how to fix it - just go ahead and open the 23 | pull request and we'll sort it out. The intent is for this list to be helpful, 24 | not intimidating! 25 | -------------------------------------------------------------------------------- /sandbox/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src", 6 | "../../elm-geometry/tests", 7 | "../../elm-2d-drawing/src" 8 | ], 9 | "elm-version": "0.19.1", 10 | "dependencies": { 11 | "direct": { 12 | "avh4/elm-color": "1.0.0", 13 | "dawehner/elm-colorbrewer": "4.1.1", 14 | "debois/elm-dom": "1.3.0", 15 | "elm/browser": "1.0.2", 16 | "elm/core": "1.0.5", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/random": "1.0.0", 20 | "elm/svg": "1.0.1", 21 | "elm/virtual-dom": "1.0.2", 22 | "ianmackenzie/elm-1d-parameter": "1.0.1", 23 | "ianmackenzie/elm-float-extra": "1.1.0", 24 | "ianmackenzie/elm-interval": "2.0.0", 25 | "ianmackenzie/elm-triangular-mesh": "1.0.4", 26 | "ianmackenzie/elm-units": "2.9.0", 27 | "ianmackenzie/elm-units-interval": "2.3.0", 28 | "mdgriffith/elm-ui": "1.1.8", 29 | "mpizenberg/elm-pointer-events": "4.0.2", 30 | "robinheghan/murmur3": "1.0.0" 31 | }, 32 | "indirect": { 33 | "elm/bytes": "1.0.8", 34 | "elm/file": "1.0.5", 35 | "elm/time": "1.0.0", 36 | "elm/url": "1.0.0" 37 | } 38 | }, 39 | "test-dependencies": { 40 | "direct": {}, 41 | "indirect": {} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sandbox/src/Ellipse2dDistanceAlongAxis.elm: -------------------------------------------------------------------------------- 1 | module Ellipse2dDistanceAlongAxis exposing (main) 2 | 3 | import BoundingBox2d exposing (BoundingBox2d) 4 | import Browser 5 | import DistanceAlongAxis2d 6 | import Drawing2d 7 | import Ellipse2d 8 | import Html exposing (Html) 9 | import Pixels exposing (Pixels) 10 | import Point2d exposing (Point2d) 11 | import Random2d 12 | import Rectangle2d exposing (Rectangle2d) 13 | import VisualTest 14 | 15 | 16 | main : Program () Int VisualTest.Msg 17 | main = 18 | Browser.element 19 | { init = always ( 0, Cmd.none ) 20 | , update = \msg index -> ( VisualTest.update msg index, Cmd.none ) 21 | , subscriptions = always VisualTest.onKeyDown 22 | , view = view 23 | } 24 | 25 | 26 | viewBounds : BoundingBox2d Pixels coordinates 27 | viewBounds = 28 | BoundingBox2d.from Point2d.origin (Point2d.pixels 800 800) 29 | 30 | 31 | view : Int -> Html msg 32 | view index = 33 | let 34 | ellipse = 35 | VisualTest.randomValue (Random2d.ellipse viewBounds) index 36 | 37 | axis = 38 | VisualTest.randomValue (Random2d.axis viewBounds) index 39 | 40 | distanceInterval = 41 | Ellipse2d.signedDistanceAlong axis ellipse 42 | in 43 | Drawing2d.toHtml 44 | { viewBox = Rectangle2d.fromBoundingBox viewBounds 45 | , size = Drawing2d.fixed 46 | } 47 | [] 48 | [ DistanceAlongAxis2d.drawProjection axis distanceInterval 49 | , Drawing2d.ellipse [] ellipse 50 | ] 51 | -------------------------------------------------------------------------------- /sandbox/src/EllipticalArc2dDistanceAlongAxis.elm: -------------------------------------------------------------------------------- 1 | module EllipticalArc2dDistanceAlongAxis exposing (main) 2 | 3 | import BoundingBox2d exposing (BoundingBox2d) 4 | import Browser 5 | import DistanceAlongAxis2d 6 | import Drawing2d 7 | import EllipticalArc2d 8 | import Html exposing (Html) 9 | import Pixels exposing (Pixels) 10 | import Point2d exposing (Point2d) 11 | import Random2d 12 | import Rectangle2d exposing (Rectangle2d) 13 | import VisualTest 14 | 15 | 16 | main : Program () Int VisualTest.Msg 17 | main = 18 | Browser.element 19 | { init = always ( 0, Cmd.none ) 20 | , update = \msg index -> ( VisualTest.update msg index, Cmd.none ) 21 | , subscriptions = always VisualTest.onKeyDown 22 | , view = view 23 | } 24 | 25 | 26 | viewBounds : BoundingBox2d Pixels coordinates 27 | viewBounds = 28 | BoundingBox2d.from Point2d.origin (Point2d.pixels 800 800) 29 | 30 | 31 | view : Int -> Html msg 32 | view index = 33 | let 34 | arc = 35 | VisualTest.randomValue (Random2d.ellipticalArc viewBounds) index 36 | 37 | axis = 38 | VisualTest.randomValue (Random2d.axis viewBounds) index 39 | 40 | distanceInterval = 41 | EllipticalArc2d.signedDistanceAlong axis arc 42 | in 43 | Drawing2d.toHtml 44 | { viewBox = Rectangle2d.fromBoundingBox viewBounds 45 | , size = Drawing2d.fixed 46 | } 47 | [] 48 | [ DistanceAlongAxis2d.drawProjection axis distanceInterval 49 | , Drawing2d.ellipticalArc [] arc 50 | ] 51 | -------------------------------------------------------------------------------- /src/Unsafe/Direction3d.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module Unsafe.Direction3d exposing (unsafeCrossProduct, unsafeXyOn) 11 | 12 | import Geometry.Types exposing (..) 13 | 14 | 15 | unsafeCrossProduct : Direction3d coordinates -> Direction3d coordinates -> Direction3d coordinates 16 | unsafeCrossProduct (Direction3d d1) (Direction3d d2) = 17 | Direction3d 18 | { x = d1.y * d2.z - d1.z * d2.y 19 | , y = d1.z * d2.x - d1.x * d2.z 20 | , z = d1.x * d2.y - d1.y * d2.x 21 | } 22 | 23 | 24 | unsafeXyOn : SketchPlane3d units coordinates { defines : localCoordinates } -> Float -> Float -> Direction3d coordinates 25 | unsafeXyOn (SketchPlane3d sketchPlane) x y = 26 | let 27 | (Direction3d i) = 28 | sketchPlane.xDirection 29 | 30 | (Direction3d j) = 31 | sketchPlane.yDirection 32 | in 33 | Direction3d 34 | { x = x * i.x + y * j.x 35 | , y = x * i.y + y * j.y 36 | , z = x * i.z + y * j.z 37 | } 38 | -------------------------------------------------------------------------------- /sandbox/src/ReleaseNotes/ArcLengthParameterization.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module ReleaseNotes.ArcLengthParameterization exposing (main) 11 | 12 | import CubicSpline2d 13 | import Drawing2d 14 | import Geometry.Accuracy as Accuracy 15 | import Html exposing (Html) 16 | import ReleaseNotes.Common exposing (..) 17 | 18 | 19 | main : Html Never 20 | main = 21 | let 22 | parameterizedSpline = 23 | CubicSpline2d.arcLengthParameterized (Accuracy.maxError 0.5) spline 24 | 25 | overallArcLength = 26 | CubicSpline2d.arcLength parameterizedSpline 27 | 28 | arcLengths = 29 | List.range 0 numSegments 30 | |> List.map 31 | (\n -> 32 | let 33 | fraction = 34 | toFloat n 35 | / toFloat numSegments 36 | in 37 | fraction * overallArcLength 38 | ) 39 | 40 | points = 41 | arcLengths 42 | |> List.filterMap (CubicSpline2d.pointAlong parameterizedSpline) 43 | in 44 | Drawing2d.toHtml renderBounds 45 | [] 46 | [ Drawing2d.cubicSpline spline 47 | , Drawing2d.dots points 48 | ] 49 | -------------------------------------------------------------------------------- /sandbox/src/NurbsCircle.elm: -------------------------------------------------------------------------------- 1 | module NurbsCircle exposing (main) 2 | 3 | import Circle2d 4 | import Color 5 | import Drawing2d 6 | import Html exposing (Html) 7 | import Pixels 8 | import Point2d 9 | import Polyline2d 10 | import Quantity 11 | import RationalQuadraticSpline2d 12 | import Rectangle2d 13 | 14 | 15 | main : Html Never 16 | main = 17 | let 18 | circle = 19 | Circle2d.withRadius (Pixels.float 300) Point2d.origin 20 | 21 | w = 22 | 1 / sqrt 2 23 | 24 | splineSegments = 25 | RationalQuadraticSpline2d.bSplineSegments 26 | [ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 ] 27 | [ ( Point2d.pixels 300 0, 1 ) 28 | , ( Point2d.pixels 300 300, w ) 29 | , ( Point2d.pixels 0 300, 1 ) 30 | , ( Point2d.pixels -300 300, w ) 31 | , ( Point2d.pixels -300 0, 1 ) 32 | , ( Point2d.pixels -300 -300, w ) 33 | , ( Point2d.pixels 0 -300, 1 ) 34 | , ( Point2d.pixels 300 -300, w ) 35 | , ( Point2d.pixels 300 0, 1 ) 36 | ] 37 | 38 | polylines = 39 | List.map (RationalQuadraticSpline2d.segments 100) splineSegments 40 | in 41 | Drawing2d.toHtml 42 | { viewBox = 43 | Rectangle2d.from 44 | (Point2d.pixels -400 -400) 45 | (Point2d.pixels 400 400) 46 | , size = Drawing2d.fixed 47 | } 48 | [] 49 | [ Drawing2d.group 50 | [ Drawing2d.strokeColor Color.lightBlue 51 | , Drawing2d.strokeWidth (Pixels.float 6) 52 | ] 53 | (List.map (Drawing2d.polyline []) polylines) 54 | , Drawing2d.circle 55 | [ Drawing2d.strokeColor Color.orange 56 | , Drawing2d.strokeWidth (Pixels.float 2) 57 | , Drawing2d.noFill 58 | ] 59 | circle 60 | ] 61 | -------------------------------------------------------------------------------- /doc/images/HermiteCubicSpline.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module HermiteCubicSpline2d exposing (main) 11 | 12 | import BoundingBox2d exposing (BoundingBox2d) 13 | import CubicSpline2d exposing (CubicSpline2d) 14 | import FillsAndStrokes exposing (..) 15 | import Html exposing (Html) 16 | import Point2d exposing (Point2d) 17 | import Svg exposing (Svg) 18 | import Vector2d exposing (Vector2d) 19 | 20 | 21 | main : Html Never 22 | main = 23 | let 24 | bounds = 25 | BoundingBox2d.fromExtrema 26 | { minX = 30 27 | , maxX = 220 28 | , minY = 30 29 | , maxY = 190 30 | } 31 | 32 | p1 = 33 | Point2d.fromCoordinates ( 50, 100 ) 34 | 35 | p2 = 36 | Point2d.fromCoordinates ( 150, 100 ) 37 | 38 | v1 = 39 | Vector2d.fromComponents ( 75, 75 ) 40 | 41 | v2 = 42 | Vector2d.fromComponents ( 50, -50 ) 43 | 44 | spline = 45 | CubicSpline2d.hermite ( p1, v1 ) ( p2, v2 ) 46 | 47 | svg = 48 | Svg.g [] 49 | [ Svg.cubicSpline2d [ blackStroke, noFill ] spline 50 | , Svg.vector2d [ greyStroke, greyFill ] p1 v1 51 | , Svg.vector2d [ greyStroke, greyFill ] p2 v2 52 | , Svg.point2d [ whiteFill, blackStroke ] p1 53 | , Svg.point2d [ whiteFill, blackStroke ] p2 54 | ] 55 | in 56 | Svg.render2d bounds svg 57 | -------------------------------------------------------------------------------- /benchmarks/PointSet.elm: -------------------------------------------------------------------------------- 1 | module PointSet exposing (..) 2 | 3 | import Benchmark exposing (Benchmark) 4 | import Benchmark.Runner exposing (BenchmarkProgram) 5 | import BoundingBox2d exposing (BoundingBox2d) 6 | import Html exposing (small) 7 | import Length exposing (Meters) 8 | import Point2d exposing (Point2d) 9 | import Random 10 | import Set2d exposing (Set2d) 11 | 12 | 13 | type WorldCoordinates 14 | = WorldCoordinates 15 | 16 | 17 | worldBounds : BoundingBox2d Meters WorldCoordinates 18 | worldBounds = 19 | BoundingBox2d.from 20 | Point2d.origin 21 | (Point2d.meters 100 100) 22 | 23 | 24 | smallSearchBox : BoundingBox2d Meters WorldCoordinates 25 | smallSearchBox = 26 | BoundingBox2d.from (Point2d.meters 20 20) (Point2d.meters 30 30) 27 | 28 | 29 | pointList : List (Point2d Meters WorldCoordinates) 30 | pointList = 31 | Random.step (Random.list 1000 (BoundingBox2d.randomPoint worldBounds)) 32 | (Random.initialSeed 1) 33 | |> Tuple.first 34 | 35 | 36 | pointSet : Set2d Meters WorldCoordinates (Point2d Meters WorldCoordinates) 37 | pointSet = 38 | Set2d.fromList BoundingBox2d.singleton pointList 39 | 40 | 41 | pointInBox : BoundingBox2d Meters WorldCoordinates -> Point2d Meters WorldCoordinates -> Bool 42 | pointInBox searchBox point = 43 | BoundingBox2d.contains point searchBox 44 | 45 | 46 | suite : Benchmark 47 | suite = 48 | Benchmark.describe "Point search" 49 | [ Benchmark.compare "Small search box" 50 | "List.filter" 51 | (\() -> List.filter (pointInBox smallSearchBox) pointList) 52 | "Set2d.search" 53 | (\() -> Set2d.search (Set2d.overlapping smallSearchBox) (always True) pointSet) 54 | , Benchmark.compare "Large search box" 55 | "List.filter" 56 | (\() -> List.filter (pointInBox worldBounds) pointList) 57 | "Set2d.search" 58 | (\() -> Set2d.search (Set2d.overlapping worldBounds) (always True) pointSet) 59 | ] 60 | 61 | 62 | main : BenchmarkProgram 63 | main = 64 | Benchmark.Runner.program suite 65 | -------------------------------------------------------------------------------- /tests/Tests/LineSegment3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.LineSegment3d exposing 2 | ( signedDistanceAlongContainsDistanceForAnyPoint 3 | , signedDistanceFromContainsDistanceForAnyPoint 4 | ) 5 | 6 | import Axis3d 7 | import Expect 8 | import Geometry.Random as Random 9 | import LineSegment3d 10 | import Point3d 11 | import Quantity.Interval as Interval 12 | import Test exposing (Test) 13 | import Test.Random as Test 14 | 15 | 16 | signedDistanceFromContainsDistanceForAnyPoint : Test 17 | signedDistanceFromContainsDistanceForAnyPoint = 18 | Test.check3 "signedDistanceFrom contains distance for any point on the line segment" 19 | Random.lineSegment3d 20 | Random.parameterValue 21 | Random.plane3d 22 | (\lineSegment t plane -> 23 | let 24 | point = 25 | LineSegment3d.interpolate lineSegment t 26 | in 27 | if 28 | LineSegment3d.signedDistanceFrom plane lineSegment 29 | |> Interval.contains (Point3d.signedDistanceFrom plane point) 30 | then 31 | Expect.pass 32 | 33 | else 34 | Expect.fail "Interval should contain distance for any point on the line segment" 35 | ) 36 | 37 | 38 | signedDistanceAlongContainsDistanceForAnyPoint : Test 39 | signedDistanceAlongContainsDistanceForAnyPoint = 40 | Test.check3 "signedDistanceAlong contains distance for any point on the line segment" 41 | Random.lineSegment3d 42 | Random.parameterValue 43 | Random.axis3d 44 | (\lineSegment t axis -> 45 | let 46 | point = 47 | LineSegment3d.interpolate lineSegment t 48 | in 49 | if 50 | LineSegment3d.signedDistanceAlong axis lineSegment 51 | |> Interval.contains (Point3d.signedDistanceAlong axis point) 52 | then 53 | Expect.pass 54 | 55 | else 56 | Expect.fail "Interval should contain distance for any point on the line segment" 57 | ) 58 | -------------------------------------------------------------------------------- /sandbox/src/DistanceAlongAxis2d.elm: -------------------------------------------------------------------------------- 1 | module DistanceAlongAxis2d exposing (drawProjection) 2 | 3 | import Angle exposing (Angle) 4 | import Axis2d exposing (Axis2d) 5 | import Axis3d exposing (Axis3d) 6 | import BoundingBox2d exposing (BoundingBox2d) 7 | import BoundingBox3d exposing (BoundingBox3d) 8 | import Browser 9 | import Browser.Events 10 | import Circle3d 11 | import Color exposing (Color) 12 | import Direction2d exposing (Direction2d) 13 | import Direction3d exposing (perpendicularTo) 14 | import Drawing2d 15 | import Html exposing (Html) 16 | import Json.Decode as Decode exposing (Decoder) 17 | import LineSegment2d exposing (LineSegment2d) 18 | import LineSegment3d exposing (LineSegment3d) 19 | import Pixels exposing (Pixels) 20 | import Point2d exposing (Point2d) 21 | import Point3d exposing (Point3d) 22 | import Quantity.Interval as Interval exposing (Interval) 23 | import Random exposing (Generator) 24 | import Random2d 25 | import Rectangle2d exposing (Rectangle2d) 26 | 27 | 28 | drawProjection : Axis2d Pixels coordinates -> Interval Float Pixels -> Drawing2d.Element Pixels coordinates event 29 | drawProjection axis interval = 30 | let 31 | perpendicularDirection = 32 | Direction2d.perpendicularTo (Axis2d.direction axis) 33 | 34 | minAxis = 35 | Axis2d.through (Point2d.along axis (Interval.minValue interval)) 36 | perpendicularDirection 37 | 38 | maxAxis = 39 | Axis2d.through (Point2d.along axis (Interval.maxValue interval)) 40 | perpendicularDirection 41 | in 42 | Drawing2d.group [] 43 | [ drawAxis Color.blue axis 44 | , drawAxis Color.green minAxis 45 | , drawAxis Color.red maxAxis 46 | ] 47 | 48 | 49 | drawAxis : Color -> Axis2d Pixels coordinates -> Drawing2d.Element Pixels coordinates event 50 | drawAxis color axis = 51 | let 52 | originPoint = 53 | Axis2d.originPoint axis 54 | in 55 | Drawing2d.group [] 56 | [ Drawing2d.lineSegment [ Drawing2d.strokeColor color ] 57 | (LineSegment2d.along axis (Pixels.float -1000) (Pixels.float 1000)) 58 | ] 59 | -------------------------------------------------------------------------------- /doc/images/EllipticalArc2d/With1.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module EllipticalArc2d.With1 exposing (main) 11 | 12 | import BoundingBox2d exposing (BoundingBox2d) 13 | import Direction2d exposing (Direction2d) 14 | import EllipticalArc2d exposing (EllipticalArc2d) 15 | import FillsAndStrokes exposing (..) 16 | import Html exposing (Html) 17 | import Point2d exposing (Point2d) 18 | import Svg exposing (Svg) 19 | 20 | 21 | main : Html Never 22 | main = 23 | let 24 | centerPoint = 25 | Point2d.origin 26 | 27 | xDirection = 28 | Direction2d.x 29 | 30 | arc = 31 | EllipticalArc2d.with 32 | { centerPoint = centerPoint 33 | , xDirection = xDirection 34 | , xRadius = 200 35 | , yRadius = 100 36 | , startAngle = 0 37 | , sweptAngle = degrees 90 38 | } 39 | 40 | svg = 41 | Svg.g [ blackStroke, whiteFill ] 42 | [ Svg.direction2d [] centerPoint xDirection 43 | , Svg.point2d [] centerPoint 44 | , Svg.ellipticalArc2d [ blackStroke, noFill ] arc 45 | , Svg.point2d [] (EllipticalArc2d.startPoint arc) 46 | , Svg.point2d [] (EllipticalArc2d.endPoint arc) 47 | ] 48 | 49 | bounds = 50 | BoundingBox2d.fromExtrema 51 | { minX = -10 52 | , maxX = 210 53 | , minY = -10 54 | , maxY = 110 55 | } 56 | in 57 | Svg.render2d bounds svg 58 | -------------------------------------------------------------------------------- /tests/Tests/Vector3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Vector3d exposing (perpendicularTo, sum, vectorScaling) 2 | 3 | import Expect 4 | import Geometry.Expect as Expect 5 | import Geometry.Random as Random 6 | import Quantity 7 | import Random 8 | import Test exposing (Test) 9 | import Test.Random as Test 10 | import Vector3d 11 | 12 | 13 | sum : Test 14 | sum = 15 | Test.check "sum is consistent with plus" (Random.smallList Random.vector3d) <| 16 | \vectors -> 17 | Vector3d.sum vectors 18 | |> Expect.vector3d 19 | (List.foldl Vector3d.plus Vector3d.zero vectors) 20 | 21 | 22 | vectorScaling : Test 23 | vectorScaling = 24 | Test.describe "scaling 3d vectors" 25 | [ Test.check "scaling a zero vector results in a zero vector" Random.length <| 26 | \len -> 27 | Expect.equal Vector3d.zero (Vector3d.scaleTo len Vector3d.zero) 28 | , Test.check2 "scaleTo has a consistent length" Random.length Random.vector3d <| 29 | \scale vector -> 30 | if vector == Vector3d.zero then 31 | Vector3d.scaleTo scale vector 32 | |> Expect.equal Vector3d.zero 33 | 34 | else 35 | Vector3d.scaleTo scale vector 36 | |> Vector3d.length 37 | |> Expect.quantity (Quantity.abs scale) 38 | , Test.check "normalize has a consistent length" Random.vector3d <| 39 | \vector -> 40 | if vector == Vector3d.zero then 41 | Vector3d.normalize vector 42 | |> Expect.equal Vector3d.zero 43 | 44 | else 45 | Vector3d.normalize vector 46 | |> Vector3d.length 47 | |> Expect.quantity (Quantity.float 1) 48 | ] 49 | 50 | 51 | perpendicularTo : Test 52 | perpendicularTo = 53 | Test.check "perpendicularTo works properly" Random.vector3d <| 54 | \vector -> 55 | Vector3d.perpendicularTo vector 56 | |> Vector3d.dot vector 57 | |> Expect.quantity Quantity.zero 58 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1697157458, 24 | "narHash": "sha256-TN/A1FQJ2iztnic4mG/LCuRgh/cntOwxEcUoFm+y9uw=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "1db3ebde866b8e5725781152f98656aaaefab4ec", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs-elm0190": { 37 | "flake": false, 38 | "locked": { 39 | "lastModified": 1571667155, 40 | "narHash": "sha256-XegNnVm7Dk4dBpO7WGPyPF+JD2KYUtOcQXZDfh8T5mQ=", 41 | "owner": "nixos", 42 | "repo": "nixpkgs", 43 | "rev": "22b0be560914b738e5342148caebd5c575b5a0b9", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nixos", 48 | "repo": "nixpkgs", 49 | "rev": "22b0be560914b738e5342148caebd5c575b5a0b9", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "nixpkgs-elm0190": "nixpkgs-elm0190" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "ianmackenzie/elm-geometry", 4 | "summary": "2D/3D geometric data types and operations", 5 | "license": "MPL-2.0", 6 | "version": "4.0.0", 7 | "exposed-modules": [ 8 | "Arc2d", 9 | "Arc3d", 10 | "ArcLength", 11 | "Axis2d", 12 | "Axis3d", 13 | "Block3d", 14 | "BoundingBox2d", 15 | "BoundingBox3d", 16 | "Circle2d", 17 | "Circle3d", 18 | "Cone3d", 19 | "CubicSpline2d", 20 | "CubicSpline3d", 21 | "Cylinder3d", 22 | "DelaunayTriangulation2d", 23 | "Direction2d", 24 | "Direction3d", 25 | "Ellipse2d", 26 | "Ellipse3d", 27 | "Ellipsoid3d", 28 | "EllipticalArc2d", 29 | "EllipticalArc3d", 30 | "Frame2d", 31 | "Frame3d", 32 | "LineSegment2d", 33 | "LineSegment3d", 34 | "Plane3d", 35 | "Point2d", 36 | "Point3d", 37 | "Polygon2d", 38 | "Polyline2d", 39 | "Polyline3d", 40 | "QuadraticSpline2d", 41 | "QuadraticSpline3d", 42 | "RationalCubicSpline2d", 43 | "RationalCubicSpline3d", 44 | "RationalQuadraticSpline2d", 45 | "RationalQuadraticSpline3d", 46 | "Rectangle2d", 47 | "Rectangle3d", 48 | "SketchPlane3d", 49 | "Sphere3d", 50 | "SweptAngle", 51 | "Triangle2d", 52 | "Triangle3d", 53 | "Vector2d", 54 | "Vector3d", 55 | "VectorBoundingBox2d", 56 | "VectorBoundingBox3d", 57 | "VoronoiDiagram2d", 58 | "Spline2d", 59 | "Spline3d" 60 | ], 61 | "elm-version": "0.19.0 <= v < 0.20.0", 62 | "dependencies": { 63 | "elm/core": "1.0.0 <= v < 2.0.0", 64 | "elm/random": "1.0.0 <= v < 2.0.0", 65 | "ianmackenzie/elm-1d-parameter": "1.0.1 <= v < 2.0.0", 66 | "ianmackenzie/elm-float-extra": "1.1.0 <= v < 2.0.0", 67 | "ianmackenzie/elm-interval": "3.1.0 <= v < 4.0.0", 68 | "ianmackenzie/elm-triangular-mesh": "1.1.0 <= v < 2.0.0", 69 | "ianmackenzie/elm-units": "2.9.0 <= v < 3.0.0", 70 | "ianmackenzie/elm-units-interval": "3.2.0 <= v < 4.0.0" 71 | }, 72 | "test-dependencies": { 73 | "elm-community/list-extra": "8.0.0 <= v < 9.0.0", 74 | "elm-community/random-extra": "3.2.0 <= v < 4.0.0", 75 | "elm-explorations/test": "2.1.1 <= v < 3.0.0", 76 | "ianmackenzie/elm-random-test": "1.0.0 <= v < 2.0.0" 77 | } 78 | } -------------------------------------------------------------------------------- /src/SweptAngle.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module SweptAngle exposing 11 | ( SweptAngle 12 | , smallPositive, smallNegative, largePositive, largeNegative 13 | ) 14 | 15 | {-| When constructing circular or elliptical arcs, it is sometimes necessary to 16 | specify which of several possible arcs you want. For example, if you ask for a 17 | circular arc from the point (1, 0) to the point (0, 1) with a radius of 1, there 18 | are four possible solutions: 19 | 20 | - An arc with a swept angle of 90 degrees, with center point at (0, 0) 21 | - An arc with a swept angle of -270 degrees, with center point at (0, 0) 22 | - An arc with a swept angle of -90 degrees, with center point at (1, 1) 23 | - An Arc with a swept angle of 270 degrees, with center point at (1, 1) 24 | 25 | The `SweptAngle` type is used in these cases to specify which arc you want. 26 | 27 | @docs SweptAngle 28 | @docs smallPositive, smallNegative, largePositive, largeNegative 29 | 30 | -} 31 | 32 | import Geometry.Types as Types 33 | 34 | 35 | {-| Indicate which of four possible arcs you would like to construct. Used by 36 | [`Arc2d.withRadius`](Arc2d#withRadius) and [`EllipticalArc2d.fromEndpoints`](EllipticalArc2d#fromEndpoints). 37 | -} 38 | type alias SweptAngle = 39 | Types.SweptAngle 40 | 41 | 42 | {-| Construct a counterclockwise arc with a swept angle between 0 and 180 43 | degrees. 44 | -} 45 | smallPositive : SweptAngle 46 | smallPositive = 47 | Types.SmallPositive 48 | 49 | 50 | {-| Construct a clockwise arc with a swept angle between 0 and -180 degrees. 51 | -} 52 | smallNegative : SweptAngle 53 | smallNegative = 54 | Types.SmallNegative 55 | 56 | 57 | {-| Construct a counterclockwise arc with a swept angle between 180 and 360 58 | degrees. 59 | -} 60 | largePositive : SweptAngle 61 | largePositive = 62 | Types.LargePositive 63 | 64 | 65 | {-| Construct a clockwise arc with a swept angle between -180 and -360 degrees. 66 | -} 67 | largeNegative : SweptAngle 68 | largeNegative = 69 | Types.LargeNegative 70 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | # old nixpkgs with elm 0.19.0 6 | nixpkgs-elm0190.url = "github:nixos/nixpkgs?rev=22b0be560914b738e5342148caebd5c575b5a0b9"; 7 | nixpkgs-elm0190.flake = false; 8 | }; 9 | 10 | outputs = { self, nixpkgs, nixpkgs-elm0190, flake-utils }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let pkgs = nixpkgs.legacyPackages.${system}; 13 | in { 14 | devShells = { 15 | default = pkgs.mkShell { 16 | buildInputs = [ 17 | pkgs.gh 18 | pkgs.elmPackages.elm 19 | pkgs.elmPackages.elm-format 20 | pkgs.elmPackages.elm-test 21 | ]; 22 | }; 23 | # elm-0.19.1 cannot be used to publish elm-geometry, 24 | # because the generated `docs.json` is very large. 25 | # We workaround this using a dev shell with elm-0.19.0 26 | # `nix develop .#publish` 27 | publish = 28 | let 29 | elmNixpkgsPrefix = "${nixpkgs-elm0190}/pkgs/development/compilers/elm"; 30 | elm0190 = with pkgs.haskell.lib.compose; justStaticExecutables (overrideCabal 31 | (drv: { 32 | enableParallelBuilding = false; 33 | jailbreak = true; 34 | preConfigure = pkgs.callPackage "${elmNixpkgsPrefix}/fetchElmDeps.nix" { } { 35 | elmPackages = import "${elmNixpkgsPrefix}/packages/elm-srcs.nix"; 36 | versionsDat = "${elmNixpkgsPrefix}/versions.dat"; 37 | }; 38 | patches = [ 39 | (pkgs.fetchpatch { 40 | url = "https://github.com/elm/compiler/pull/1886/commits/39d86a735e28da514be185d4c3256142c37c2a8a.patch"; 41 | sha256 = "0nni5qx1523rjz1ja42z6z9pijxvi3fgbw1dhq5qi11mh1nb9ay7"; 42 | }) 43 | ]; 44 | }) 45 | (with pkgs.haskell.packages.ghc810; callPackage "${elmNixpkgsPrefix}/packages/elm.nix" { 46 | # re-add `lib` to `stdenv` for backwards compatibility with old nixpkgs 47 | stdenv = pkgs.stdenv // { lib = pkgs.lib; }; 48 | # fix "Module ‘Control.Monad.Logic’ does not export ‘lift’" 49 | logict = callHackage "logict" "0.6.0.3" { }; 50 | })); 51 | in 52 | pkgs.mkShell { 53 | buildInputs = [ elm0190 ]; 54 | }; 55 | }; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /doc/images/EllipticalArc2d/With2.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module EllipticalArc2d.With2 exposing (main) 11 | 12 | import BoundingBox2d exposing (BoundingBox2d) 13 | import Direction2d exposing (Direction2d) 14 | import EllipticalArc2d exposing (EllipticalArc2d) 15 | import FillsAndStrokes exposing (..) 16 | import Html exposing (Html) 17 | import Point2d exposing (Point2d) 18 | import Svg exposing (Svg) 19 | 20 | 21 | main : Html Never 22 | main = 23 | let 24 | centerPoint = 25 | Point2d.origin 26 | 27 | xDirection = 28 | Direction2d.fromAngle (degrees 30) 29 | 30 | arc = 31 | EllipticalArc2d.with 32 | { centerPoint = centerPoint 33 | , xDirection = xDirection 34 | , xRadius = 200 35 | , yRadius = 100 36 | , startAngle = degrees -90 37 | , sweptAngle = degrees 180 38 | } 39 | 40 | svg = 41 | Svg.g [ blackStroke, whiteFill ] 42 | [ Svg.direction2d [] centerPoint xDirection 43 | , Svg.point2d [] centerPoint 44 | , Svg.ellipticalArc2d [ blackStroke, noFill ] arc 45 | , Svg.point2d [] (EllipticalArc2d.startPoint arc) 46 | , Svg.point2d [] (EllipticalArc2d.endPoint arc) 47 | ] 48 | 49 | ellipsePoints = 50 | List.range 0 100 51 | |> List.map 52 | (\n -> EllipticalArc2d.pointOn arc (toFloat n / 100)) 53 | 54 | { minX, maxX, minY, maxY } = 55 | List.map BoundingBox2d.singleton ellipsePoints 56 | |> List.foldl BoundingBox2d.hull 57 | (BoundingBox2d.singleton centerPoint) 58 | |> BoundingBox2d.extrema 59 | 60 | padding = 61 | 10 62 | 63 | bounds = 64 | BoundingBox2d.fromExtrema 65 | { minX = minX - padding 66 | , maxX = maxX + padding 67 | , minY = minY - padding 68 | , maxY = maxY + padding 69 | } 70 | in 71 | Svg.render2d bounds svg 72 | -------------------------------------------------------------------------------- /tests/Tests/Circle3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Circle3d exposing (boundingBoxContainsCenter, throughPoints) 2 | 3 | import Area 4 | import BoundingBox3d 5 | import Circle3d 6 | import Expect 7 | import Geometry.Expect as Expect 8 | import Geometry.Random as Random 9 | import Point3d 10 | import Quantity 11 | import Test exposing (Test) 12 | import Test.Random as Test 13 | import Triangle3d 14 | 15 | 16 | throughPoints : Test 17 | throughPoints = 18 | Test.describe "throughPoints" 19 | [ Test.check3 "All given points lie on the circle constructed using `throughPoints`" 20 | Random.point3d 21 | Random.point3d 22 | Random.point3d 23 | (\p1 p2 p3 -> 24 | let 25 | -- if the first three points are collinear it is not possible to construct a circle 26 | -- that passes through them. 27 | -- three points are collinear if the area of the triangle they form is zero. 28 | isValidInput = 29 | let 30 | triangleArea = 31 | Triangle3d.area (Triangle3d.from p1 p2 p3) 32 | in 33 | triangleArea |> Quantity.greaterThan (Area.squareMeters 1.0e-6) 34 | 35 | maybeCircle = 36 | Circle3d.throughPoints p1 p2 p3 37 | 38 | liesOnCircle point circle = 39 | Point3d.distanceFrom point (Circle3d.centerPoint circle) 40 | |> Expect.quantity (Circle3d.radius circle) 41 | in 42 | case maybeCircle of 43 | Just circle -> 44 | Expect.all (List.map liesOnCircle [ p1, p2, p3 ]) circle 45 | 46 | Nothing -> 47 | if isValidInput then 48 | Expect.fail "throughPoints returned Nothing on valid input" 49 | 50 | else 51 | Expect.pass 52 | ) 53 | ] 54 | 55 | 56 | boundingBoxContainsCenter : Test 57 | boundingBoxContainsCenter = 58 | Test.check "A circle's bounding box contains its center point" 59 | Random.circle3d 60 | (\circle -> 61 | let 62 | boundingBox = 63 | Circle3d.boundingBox circle 64 | 65 | centerPoint = 66 | Circle3d.centerPoint circle 67 | in 68 | if BoundingBox3d.contains centerPoint boundingBox then 69 | Expect.pass 70 | 71 | else 72 | Expect.fail "Circle bounding box does not contain the center point" 73 | ) 74 | -------------------------------------------------------------------------------- /sandbox/src/RegionTriangulation.elm: -------------------------------------------------------------------------------- 1 | module RegionTriangulation exposing (main) 2 | 3 | import Angle exposing (Angle) 4 | import Arc2d exposing (Arc2d) 5 | import Circle2d exposing (Circle2d) 6 | import Color 7 | import Curve2d exposing (Curve2d) 8 | import Drawing2d 9 | import Html exposing (Html) 10 | import Length exposing (Meters) 11 | import LineSegment2d exposing (LineSegment2d) 12 | import Pixels exposing (Pixels) 13 | import Point2d exposing (Point2d) 14 | import Polygon2d 15 | import Quantity exposing (Quantity) 16 | import Rectangle2d exposing (Rectangle2d) 17 | import Region2d exposing (Region2d) 18 | import Triangle2d 19 | import TriangularMesh exposing (TriangularMesh) 20 | 21 | 22 | type DrawingCoordinates 23 | = DrawingCoordinates 24 | 25 | 26 | region : Region2d Meters DrawingCoordinates 27 | region = 28 | let 29 | p1 = 30 | Point2d.origin 31 | 32 | p2 = 33 | Point2d.centimeters 7 0 34 | 35 | p3 = 36 | Point2d.centimeters 7 2 37 | 38 | p4 = 39 | Point2d.centimeters 5 4 40 | 41 | p5 = 42 | Point2d.centimeters 0 4 43 | in 44 | Region2d.withHoles 45 | [ [ Curve2d.circle (Circle2d.withRadius (Length.centimeters 1) (Point2d.centimeters 2 2)) ] 46 | , [ Curve2d.circle (Circle2d.withRadius (Length.centimeters 1) (Point2d.centimeters 5 2)) ] 47 | ] 48 | [ Curve2d.lineSegment (LineSegment2d.from p1 p2) 49 | , Curve2d.lineSegment (LineSegment2d.from p2 p3) 50 | , Curve2d.arc (Arc2d.from p3 p4 (Angle.degrees 90)) 51 | , Curve2d.lineSegment (LineSegment2d.from p4 p5) 52 | , Curve2d.lineSegment (LineSegment2d.from p5 p1) 53 | ] 54 | 55 | 56 | main : Html msg 57 | main = 58 | let 59 | resolution = 60 | Pixels.float 100 |> Quantity.per Length.centimeter 61 | 62 | polygon = 63 | Region2d.approximate (Pixels.float 0.5) (Region2d.at resolution region) 64 | 65 | mesh = 66 | Polygon2d.triangulate polygon 67 | 68 | lineSegments = 69 | List.map LineSegment2d.fromEndpoints (TriangularMesh.edgeVertices mesh) 70 | 71 | triangles = 72 | List.map Triangle2d.fromVertices (TriangularMesh.faceVertices mesh) 73 | in 74 | Drawing2d.toHtml 75 | { size = Drawing2d.fixed 76 | , viewBox = Rectangle2d.from (Point2d.pixels -50 -50) (Point2d.pixels 750 750) 77 | } 78 | [] 79 | [ Drawing2d.group [ Drawing2d.fillColor Color.lightGrey, Drawing2d.noBorder ] 80 | (List.map (Drawing2d.triangle []) triangles) 81 | , Drawing2d.group [ Drawing2d.strokeColor Color.darkGrey ] 82 | (List.map (Drawing2d.lineSegment []) lineSegments) 83 | ] 84 | -------------------------------------------------------------------------------- /benchmarks/ArcLengthParameterization.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module ArcLengthParameterization exposing (main) 11 | 12 | import Benchmark exposing (Benchmark) 13 | import Benchmark.Runner exposing (BenchmarkProgram) 14 | import CubicSpline2d exposing (CubicSpline2d) 15 | import CubicSpline3d exposing (CubicSpline3d) 16 | import Point2d exposing (Point2d) 17 | import Point3d exposing (Point3d) 18 | import QuadraticSpline2d exposing (QuadraticSpline2d) 19 | import QuadraticSpline3d exposing (QuadraticSpline3d) 20 | 21 | 22 | tolerance : Float 23 | tolerance = 24 | 1.0e-3 25 | 26 | 27 | testQuadraticSpline2d : QuadraticSpline2d 28 | testQuadraticSpline2d = 29 | QuadraticSpline2d.fromControlPoints 30 | ( Point2d.fromCoordinates ( 0, 0 ) 31 | , Point2d.fromCoordinates ( 10, 10 ) 32 | , Point2d.fromCoordinates ( 20, 0 ) 33 | ) 34 | 35 | 36 | testQuadraticSpline3d : QuadraticSpline3d 37 | testQuadraticSpline3d = 38 | QuadraticSpline3d.fromControlPoints 39 | ( Point3d.fromCoordinates ( 0, 0, 0 ) 40 | , Point3d.fromCoordinates ( 10, 10, 0 ) 41 | , Point3d.fromCoordinates ( 20, 0, 0 ) 42 | ) 43 | 44 | 45 | testCubicSpline2d : CubicSpline2d 46 | testCubicSpline2d = 47 | CubicSpline2d.fromControlPoints 48 | ( Point2d.fromCoordinates ( 0, 0 ) 49 | , Point2d.fromCoordinates ( 10, 0 ) 50 | , Point2d.fromCoordinates ( 0, 10 ) 51 | , Point2d.fromCoordinates ( 10, 10 ) 52 | ) 53 | 54 | 55 | testCubicSpline3d : CubicSpline3d 56 | testCubicSpline3d = 57 | CubicSpline3d.fromControlPoints 58 | ( Point3d.fromCoordinates ( 0, 0, 0 ) 59 | , Point3d.fromCoordinates ( 10, 0, 0 ) 60 | , Point3d.fromCoordinates ( 10, 10, 0 ) 61 | , Point3d.fromCoordinates ( 10, 10, 10 ) 62 | ) 63 | 64 | 65 | suite : Benchmark 66 | suite = 67 | Benchmark.describe "Arc length parameterization" 68 | [ Benchmark.benchmark2 "QuadraticSpline2d" 69 | QuadraticSpline2d.arcLengthParameterized 70 | tolerance 71 | testQuadraticSpline2d 72 | , Benchmark.benchmark2 "QuadraticSpline3d" 73 | QuadraticSpline3d.arcLengthParameterized 74 | tolerance 75 | testQuadraticSpline3d 76 | , Benchmark.benchmark2 "CubicSpline2d" 77 | CubicSpline2d.arcLengthParameterized 78 | tolerance 79 | testCubicSpline2d 80 | , Benchmark.benchmark2 "CubicSpline3d" 81 | CubicSpline3d.arcLengthParameterized 82 | tolerance 83 | testCubicSpline3d 84 | ] 85 | 86 | 87 | main : BenchmarkProgram 88 | main = 89 | Benchmark.Runner.program suite 90 | -------------------------------------------------------------------------------- /tests/Tests/Circle2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Circle2d exposing (boundingBoxContainsCenter, intersectsBoundingBox) 2 | 3 | import BoundingBox2d 4 | import Circle2d 5 | import Expect 6 | import Geometry.Random as Random 7 | import Length 8 | import Point2d 9 | import Test exposing (Test, test) 10 | import Test.Random as Test 11 | 12 | 13 | boundingBoxContainsCenter : Test 14 | boundingBoxContainsCenter = 15 | Test.check "A circle's bounding box contains its center point" 16 | Random.circle2d 17 | (\circle -> 18 | let 19 | boundingBox = 20 | Circle2d.boundingBox circle 21 | 22 | centerPoint = 23 | Circle2d.centerPoint circle 24 | in 25 | if BoundingBox2d.contains centerPoint boundingBox then 26 | Expect.pass 27 | 28 | else 29 | Expect.fail "Circle bounding box does not contain the center point" 30 | ) 31 | 32 | 33 | intersectsBoundingBox : Test 34 | intersectsBoundingBox = 35 | let 36 | someBox x1 x2 y1 y2 = 37 | BoundingBox2d.fromExtrema 38 | { minX = Length.meters x1 39 | , maxX = Length.meters x2 40 | , minY = Length.meters y1 41 | , maxY = Length.meters y2 42 | } 43 | 44 | someCircle r center = 45 | Circle2d.withRadius (Length.meters r) center 46 | 47 | noIntersectionFound = 48 | "Expected an intersection to be found" 49 | 50 | unexpectedIntersection = 51 | "Expected no intersection to be found" 52 | in 53 | Test.describe "Intersection between a circle and a bounding box" 54 | [ test "Detects intersection when overlapping in both X and Y" <| 55 | \_ -> 56 | let 57 | box = 58 | someBox 1 5 1 5 59 | 60 | circle = 61 | someCircle 2 Point2d.origin 62 | in 63 | if Circle2d.intersectsBoundingBox box circle then 64 | Expect.pass 65 | 66 | else 67 | Expect.fail noIntersectionFound 68 | , test "Detects no intersection when not overlapping" <| 69 | \_ -> 70 | let 71 | box = 72 | someBox 20 22 30 40 73 | 74 | circle = 75 | Point2d.meters -20 -20 76 | |> someCircle 5 77 | in 78 | if Circle2d.intersectsBoundingBox box circle then 79 | Expect.fail unexpectedIntersection 80 | 81 | else 82 | Expect.pass 83 | , test "Detects intersects when box and circle touch by exactly one pixel" <| 84 | \_ -> 85 | let 86 | box = 87 | someBox 1 1 0 0 88 | 89 | circle = 90 | someCircle 1 Point2d.origin 91 | in 92 | if Circle2d.intersectsBoundingBox box circle then 93 | Expect.pass 94 | 95 | else 96 | Expect.fail noIntersectionFound 97 | ] 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ian.e.mackenzie@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /tests/Tests/EllipticalArc3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.EllipticalArc3d exposing 2 | ( evaluateOneIsEndPoint 3 | , evaluateZeroIsStartPoint 4 | , projectInto 5 | , signedDistanceAlong 6 | ) 7 | 8 | import Angle 9 | import EllipticalArc2d 10 | import EllipticalArc3d exposing (EllipticalArc3d) 11 | import Geometry.Expect as Expect 12 | import Geometry.Random as Random 13 | import Length exposing (Meters) 14 | import Point3d 15 | import Test exposing (Test) 16 | import Test.Random as Test 17 | import Tests.Generic.Curve3d 18 | 19 | 20 | evaluateZeroIsStartPoint : Test 21 | evaluateZeroIsStartPoint = 22 | Test.check "Evaluating at t=0 returns start point" 23 | Random.ellipticalArc3d 24 | (\arc -> EllipticalArc3d.pointOn arc 0 |> Expect.point3d (EllipticalArc3d.startPoint arc)) 25 | 26 | 27 | evaluateOneIsEndPoint : Test 28 | evaluateOneIsEndPoint = 29 | Test.check "Evaluating at t=1 returns end point" 30 | Random.ellipticalArc3d 31 | (\arc -> EllipticalArc3d.pointOn arc 1 |> Expect.point3d (EllipticalArc3d.endPoint arc)) 32 | 33 | 34 | projectInto : Test 35 | projectInto = 36 | Test.check3 "Projecting an arc works properly" 37 | Random.ellipticalArc3d 38 | Random.sketchPlane3d 39 | Random.parameterValue 40 | (\arc sketchPlane parameterValue -> 41 | let 42 | projectedArc = 43 | EllipticalArc3d.projectInto sketchPlane arc 44 | 45 | pointOnOriginalArc = 46 | EllipticalArc3d.pointOn arc parameterValue 47 | 48 | pointOnProjectedArc = 49 | EllipticalArc2d.pointOn projectedArc parameterValue 50 | 51 | projectedPoint = 52 | pointOnOriginalArc |> Point3d.projectInto sketchPlane 53 | in 54 | pointOnProjectedArc |> Expect.point2d projectedPoint 55 | ) 56 | 57 | 58 | curveOperations : Tests.Generic.Curve3d.Operations (EllipticalArc3d Meters coordinates) coordinates 59 | curveOperations = 60 | { generator = Random.ellipticalArc3d 61 | , pointOn = EllipticalArc3d.pointOn 62 | , boundingBox = EllipticalArc3d.boundingBox 63 | , firstDerivative = EllipticalArc3d.firstDerivative 64 | , firstDerivativeBoundingBox = EllipticalArc3d.firstDerivativeBoundingBox 65 | , scaleAbout = EllipticalArc3d.scaleAbout 66 | , translateBy = EllipticalArc3d.translateBy 67 | , rotateAround = EllipticalArc3d.rotateAround 68 | , mirrorAcross = EllipticalArc3d.mirrorAcross 69 | , numApproximationSegments = EllipticalArc3d.numApproximationSegments 70 | } 71 | 72 | 73 | genericTests : Test 74 | genericTests = 75 | Tests.Generic.Curve3d.tests 76 | curveOperations 77 | curveOperations 78 | EllipticalArc3d.placeIn 79 | EllipticalArc3d.relativeTo 80 | 81 | 82 | signedDistanceAlong : Test 83 | signedDistanceAlong = 84 | Test.check3 "signedDistanceAlong" 85 | Random.ellipticalArc3d 86 | Random.axis3d 87 | Random.parameterValue 88 | (\arc axis parameterValue -> 89 | let 90 | distanceInterval = 91 | EllipticalArc3d.signedDistanceAlong axis arc 92 | 93 | projectedDistance = 94 | Point3d.signedDistanceAlong axis 95 | (EllipticalArc3d.pointOn arc parameterValue) 96 | in 97 | projectedDistance |> Expect.quantityContainedIn distanceInterval 98 | ) 99 | -------------------------------------------------------------------------------- /benchmarks/DelaunayTriangulation.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module DelaunayTriangulation exposing (main) 11 | 12 | import Array exposing (Array) 13 | import Benchmark exposing (Benchmark) 14 | import Benchmark.Runner exposing (BenchmarkProgram) 15 | import Browser 16 | import DelaunayTriangulation2d 17 | import Html exposing (Html) 18 | import Point2d exposing (Point2d) 19 | import Quantity exposing (Unitless) 20 | import Random 21 | import Task exposing (Task) 22 | import Time 23 | 24 | 25 | testPoints : Array (Point2d Unitless coordinates) 26 | testPoints = 27 | let 28 | pointGenerator = 29 | Random.map2 Point2d.unitless 30 | (Random.float 0 100) 31 | (Random.float 0 100) 32 | 33 | arrayGenerator = 34 | Random.list 200 pointGenerator 35 | in 36 | Random.step arrayGenerator (Random.initialSeed 1) 37 | |> Tuple.first 38 | |> Array.fromList 39 | 40 | 41 | triangulateNTimes : Int -> () 42 | triangulateNTimes count = 43 | if count > 0 then 44 | let 45 | _ = 46 | DelaunayTriangulation2d.fromPoints testPoints 47 | in 48 | triangulateNTimes (count - 1) 49 | 50 | else 51 | () 52 | 53 | 54 | task : Task x Float 55 | task = 56 | Time.now 57 | |> Task.andThen 58 | (\startTime -> 59 | let 60 | _ = 61 | triangulateNTimes 100 62 | in 63 | Time.now 64 | |> Task.map 65 | (\endTime -> 66 | toFloat (Time.posixToMillis endTime - Time.posixToMillis startTime) / 1000 67 | ) 68 | ) 69 | 70 | 71 | suite : Benchmark 72 | suite = 73 | Benchmark.benchmark "Delaunay triangulation" 74 | (\() -> DelaunayTriangulation2d.fromPoints testPoints) 75 | 76 | 77 | benchmarkProgram : BenchmarkProgram 78 | benchmarkProgram = 79 | Benchmark.Runner.program suite 80 | 81 | 82 | type TaskModel 83 | = Running 84 | | Complete Float 85 | 86 | 87 | taskView : TaskModel -> Browser.Document msg 88 | taskView taskModel = 89 | { title = "Delaunay Triangulation Benchmark" 90 | , body = 91 | [ Html.text <| 92 | case taskModel of 93 | Running -> 94 | "Running" 95 | 96 | Complete time -> 97 | "Elapsed: " ++ String.fromFloat time 98 | ] 99 | } 100 | 101 | 102 | type alias TaskProgram = 103 | Program () TaskModel Float 104 | 105 | 106 | taskProgram : TaskProgram 107 | taskProgram = 108 | Browser.document 109 | { init = \() -> ( Running, Task.perform identity task ) 110 | , view = taskView 111 | , update = \value model -> ( Complete value, Cmd.none ) 112 | , subscriptions = always Sub.none 113 | } 114 | 115 | 116 | main : TaskProgram 117 | main = 118 | taskProgram 119 | -------------------------------------------------------------------------------- /sandbox/src/ConvexHull.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module ConvexHull exposing (main) 11 | 12 | import BoundingBox2d exposing (BoundingBox2d) 13 | import Browser 14 | import Circle2d 15 | import Drawing2d 16 | import Html exposing (Html) 17 | import Html.Events 18 | import Pixels exposing (Pixels) 19 | import Point2d exposing (Point2d) 20 | import Polygon2d 21 | import Quantity.Interval as Interval 22 | import Random exposing (Generator) 23 | import Rectangle2d 24 | 25 | 26 | type ScreenCoordinates 27 | = ScreenCoordinates 28 | 29 | 30 | type alias Model = 31 | { points : List (Point2d Pixels ScreenCoordinates) 32 | } 33 | 34 | 35 | type Msg 36 | = Click 37 | | NewRandomPoints (List (Point2d Pixels ScreenCoordinates)) 38 | 39 | 40 | renderBounds : BoundingBox2d Pixels ScreenCoordinates 41 | renderBounds = 42 | BoundingBox2d.from Point2d.origin (Point2d.pixels 300 300) 43 | 44 | 45 | pointsGenerator : Generator (List (Point2d Pixels ScreenCoordinates)) 46 | pointsGenerator = 47 | let 48 | ( xInterval, yInterval ) = 49 | BoundingBox2d.intervals renderBounds 50 | 51 | parameterGenerator = 52 | Random.float 0.05 0.95 53 | 54 | pointGenerator = 55 | Random.map2 Point2d.xy 56 | (Random.map (Interval.interpolate xInterval) parameterGenerator) 57 | (Random.map (Interval.interpolate yInterval) parameterGenerator) 58 | in 59 | Random.int 2 32 60 | |> Random.andThen 61 | (\listSize -> Random.list listSize pointGenerator) 62 | 63 | 64 | generateNewPoints : Cmd Msg 65 | generateNewPoints = 66 | Random.generate NewRandomPoints pointsGenerator 67 | 68 | 69 | init : () -> ( Model, Cmd Msg ) 70 | init () = 71 | ( { points = [] }, generateNewPoints ) 72 | 73 | 74 | update : Msg -> Model -> ( Model, Cmd Msg ) 75 | update message model = 76 | case message of 77 | Click -> 78 | ( model, generateNewPoints ) 79 | 80 | NewRandomPoints points -> 81 | ( { model | points = points }, Cmd.none ) 82 | 83 | 84 | view : Model -> Html Msg 85 | view model = 86 | let 87 | convexHull = 88 | Polygon2d.convexHull model.points 89 | in 90 | Html.div [ Html.Events.onClick Click ] 91 | [ Drawing2d.toHtml 92 | { size = Drawing2d.fixed 93 | , viewBox = Rectangle2d.fromBoundingBox renderBounds 94 | } 95 | [] 96 | [ Drawing2d.polygon [] convexHull 97 | , Drawing2d.group [] <| 98 | (model.points 99 | |> List.map 100 | (\point -> Drawing2d.circle [] (Circle2d.withRadius (Pixels.float 3) point)) 101 | ) 102 | ] 103 | ] 104 | 105 | 106 | main : Program () Model Msg 107 | main = 108 | Browser.element 109 | { init = init 110 | , update = update 111 | , view = view 112 | , subscriptions = always Sub.none 113 | } 114 | -------------------------------------------------------------------------------- /tests/Tests/Arc3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Arc3d exposing 2 | ( evaluateHalfIsMidpoint 3 | , evaluateOneIsEndPoint 4 | , evaluateZeroIsStartPoint 5 | , genericTests 6 | , projectInto 7 | , reverseFlipsDirection 8 | , reverseKeepsMidpoint 9 | ) 10 | 11 | import Angle 12 | import Arc3d exposing (Arc3d) 13 | import EllipticalArc2d 14 | import Geometry.Expect as Expect 15 | import Geometry.Random as Random 16 | import Length exposing (Meters) 17 | import Point3d 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Tests.Generic.Curve3d 21 | 22 | 23 | evaluateZeroIsStartPoint : Test 24 | evaluateZeroIsStartPoint = 25 | Test.check "Evaluating at t=0 returns start point" 26 | Random.arc3d 27 | (\arc -> Arc3d.pointOn arc 0 |> Expect.point3d (Arc3d.startPoint arc)) 28 | 29 | 30 | evaluateOneIsEndPoint : Test 31 | evaluateOneIsEndPoint = 32 | Test.check "Evaluating at t=1 returns end point" 33 | Random.arc3d 34 | (\arc -> Arc3d.pointOn arc 1 |> Expect.point3d (Arc3d.endPoint arc)) 35 | 36 | 37 | evaluateHalfIsMidpoint : Test 38 | evaluateHalfIsMidpoint = 39 | Test.check "Evaluating at t=0.5 returns midpoint" 40 | Random.arc3d 41 | (\arc -> Arc3d.pointOn arc 0.5 |> Expect.point3d (Arc3d.midpoint arc)) 42 | 43 | 44 | reverseKeepsMidpoint : Test 45 | reverseKeepsMidpoint = 46 | Test.check "Reversing an arc keeps the midpoint" 47 | Random.arc3d 48 | (\arc -> 49 | Arc3d.midpoint (Arc3d.reverse arc) 50 | |> Expect.point3d (Arc3d.midpoint arc) 51 | ) 52 | 53 | 54 | reverseFlipsDirection : Test 55 | reverseFlipsDirection = 56 | Test.check2 "Reversing an arc is consistent with reversed evaluation" 57 | Random.arc3d 58 | Random.parameterValue 59 | (\arc parameterValue -> 60 | Arc3d.pointOn (Arc3d.reverse arc) parameterValue 61 | |> Expect.point3d 62 | (Arc3d.pointOn arc (1 - parameterValue)) 63 | ) 64 | 65 | 66 | projectInto : Test 67 | projectInto = 68 | Test.check3 "Projecting an arc works properly" 69 | Random.arc3d 70 | Random.sketchPlane3d 71 | Random.parameterValue 72 | (\arc sketchPlane parameterValue -> 73 | let 74 | projectedArc = 75 | Arc3d.projectInto sketchPlane arc 76 | 77 | pointOnOriginalArc = 78 | Arc3d.pointOn arc parameterValue 79 | 80 | pointOnProjectedArc = 81 | EllipticalArc2d.pointOn projectedArc parameterValue 82 | 83 | projectedPoint = 84 | pointOnOriginalArc |> Point3d.projectInto sketchPlane 85 | in 86 | pointOnProjectedArc |> Expect.point2d projectedPoint 87 | ) 88 | 89 | 90 | curveOperations : Tests.Generic.Curve3d.Operations (Arc3d Meters coordinates) coordinates 91 | curveOperations = 92 | { generator = Random.arc3d 93 | , pointOn = Arc3d.pointOn 94 | , boundingBox = Arc3d.boundingBox 95 | , firstDerivative = Arc3d.firstDerivative 96 | , firstDerivativeBoundingBox = Arc3d.firstDerivativeBoundingBox 97 | , scaleAbout = Arc3d.scaleAbout 98 | , translateBy = Arc3d.translateBy 99 | , rotateAround = Arc3d.rotateAround 100 | , mirrorAcross = Arc3d.mirrorAcross 101 | , numApproximationSegments = Arc3d.numApproximationSegments 102 | } 103 | 104 | 105 | genericTests : Test 106 | genericTests = 107 | Tests.Generic.Curve3d.tests 108 | curveOperations 109 | curveOperations 110 | Arc3d.placeIn 111 | Arc3d.relativeTo 112 | -------------------------------------------------------------------------------- /src/Polygon2d/EdgeSet.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module Polygon2d.EdgeSet exposing 11 | ( Edge 12 | , EdgeSet 13 | , empty 14 | , insert 15 | , leftOf 16 | , remove 17 | ) 18 | 19 | import LineSegment2d exposing (LineSegment2d) 20 | import Point2d exposing (Point2d) 21 | import Quantity 22 | import Quantity.Extra as Quantity 23 | 24 | 25 | type alias Edge units coordinates = 26 | ( Int, LineSegment2d units coordinates ) 27 | 28 | 29 | type EdgeSet units coordinates 30 | = EdgeSet (List (Edge units coordinates)) 31 | 32 | 33 | empty : EdgeSet units coordinates 34 | empty = 35 | EdgeSet [] 36 | 37 | 38 | leftOf : Point2d units coordinates -> EdgeSet units coordinates -> Maybe Int 39 | leftOf point (EdgeSet edges) = 40 | let 41 | x = 42 | Point2d.xCoordinate point 43 | 44 | y = 45 | Point2d.yCoordinate point 46 | in 47 | edges 48 | |> List.foldl 49 | (\edge current -> 50 | let 51 | ( p1, p2 ) = 52 | LineSegment2d.endpoints (Tuple.second edge) 53 | 54 | x1 = 55 | Point2d.xCoordinate p1 56 | 57 | y1 = 58 | Point2d.yCoordinate p1 59 | 60 | x2 = 61 | Point2d.xCoordinate p2 62 | 63 | y2 = 64 | Point2d.yCoordinate p2 65 | 66 | dx = 67 | if y1 == y2 then 68 | x |> Quantity.minus (Quantity.max x1 x2) 69 | 70 | else 71 | let 72 | ratio = 73 | Quantity.ratio 74 | (y |> Quantity.minus y1) 75 | (y2 |> Quantity.minus y1) 76 | in 77 | x 78 | |> Quantity.minus 79 | (x1 80 | |> Quantity.plus 81 | (Quantity.multiplyBy ratio 82 | (x2 |> Quantity.minus x1) 83 | ) 84 | ) 85 | in 86 | if dx |> Quantity.greaterThanOrEqualTo Quantity.zero then 87 | case current of 88 | Nothing -> 89 | Just ( dx, edge ) 90 | 91 | Just ( currentDx, currentEdge ) -> 92 | if dx |> Quantity.lessThanOrEqualTo currentDx then 93 | Just ( dx, edge ) 94 | 95 | else 96 | current 97 | 98 | else 99 | current 100 | ) 101 | Nothing 102 | |> Maybe.map (\( dx, ( index, segment ) ) -> index) 103 | 104 | 105 | insert : Edge units coordinates -> EdgeSet units coordinates -> EdgeSet units coordinates 106 | insert edge (EdgeSet edges) = 107 | EdgeSet (edge :: edges) 108 | 109 | 110 | remove : Edge units coordinates -> EdgeSet units coordinates -> EdgeSet units coordinates 111 | remove edge (EdgeSet edges) = 112 | EdgeSet (List.filter ((/=) edge) edges) 113 | -------------------------------------------------------------------------------- /src/Surface3d.elm: -------------------------------------------------------------------------------- 1 | module Surface3d exposing 2 | ( Surface3d 3 | , triangle, rectangle, circle, ellipse, extrusion, revolution, planar 4 | , flip 5 | --, translateBy, scaleAbout, rotateAround, mirrorAcross 6 | -- , placeIn, relativeTo 7 | -- , toMesh 8 | ) 9 | 10 | {-| 11 | 12 | @docs Surface3d 13 | 14 | @docs triangle, rectangle, circle, ellipse, extrusion, revolution, planar 15 | 16 | @docs toMesh 17 | 18 | @docs flip, translateBy, scaleAbout, rotateAround, mirrorAcross 19 | 20 | @docs placeIn, relativeTo 21 | 22 | -} 23 | 24 | import Angle exposing (Angle) 25 | import Axis3d exposing (Axis3d) 26 | import Circle3d exposing (Circle3d) 27 | import Curve3d exposing (Curve3d) 28 | import Ellipse3d exposing (Ellipse3d) 29 | import Frame2d 30 | import Frame3d exposing (Frame3d) 31 | import Geometry.Types as Types 32 | import Plane3d exposing (Plane3d) 33 | import Point3d exposing (Point3d) 34 | import Rectangle3d exposing (Rectangle3d) 35 | import Region2d exposing (Region2d) 36 | import SketchPlane3d exposing (SketchPlane3d) 37 | import Triangle3d exposing (Triangle3d) 38 | import Vector3d exposing (Vector3d) 39 | 40 | 41 | type alias Surface3d units coordinates = 42 | Types.Surface3d units coordinates 43 | 44 | 45 | triangle : Triangle3d units coordinates -> Surface3d units coordinates 46 | triangle = 47 | Types.TriangularSurface Types.RightHandedSurface 48 | 49 | 50 | rectangle : Rectangle3d units coordinates -> Surface3d units coordinates 51 | rectangle = 52 | Types.RectangularSurface Types.RightHandedSurface 53 | 54 | 55 | circle : Circle3d units coordinates -> Surface3d units coordinates 56 | circle = 57 | Types.CircularSurface 58 | 59 | 60 | ellipse : Ellipse3d units coordinates -> Surface3d units coordinates 61 | ellipse = 62 | Types.EllipticalSurface Types.RightHandedSurface 63 | 64 | 65 | extrusion : Curve3d units coordinates -> Vector3d units coordinates -> Surface3d units coordinates 66 | extrusion = 67 | Types.ExtrusionSurface Types.RightHandedSurface 68 | 69 | 70 | revolution : Curve3d units coordinates -> Axis3d units coordinates -> Angle -> Surface3d units coordinates 71 | revolution = 72 | Types.RevolutionSurface Types.RightHandedSurface 73 | 74 | 75 | planar : Region2d units sketchCoordinates -> SketchPlane3d units coordinates { defines : sketchCoordinates } -> Surface3d units coordinates 76 | planar givenRegion givenSketchPlane = 77 | Types.PlanarSurface Types.RightHandedSurface 78 | (givenRegion |> Region2d.placeIn Frame2d.atOrigin) 79 | (Frame2d.atOrigin |> SketchPlane3d.on givenSketchPlane) 80 | 81 | 82 | toggle : Types.SurfaceHandedness -> Types.SurfaceHandedness 83 | toggle givenHandedness = 84 | case givenHandedness of 85 | Types.RightHandedSurface -> 86 | Types.LeftHandedSurface 87 | 88 | Types.LeftHandedSurface -> 89 | Types.RightHandedSurface 90 | 91 | 92 | flip : Surface3d units coordinates -> Surface3d units coordinates 93 | flip givenSurface = 94 | case givenSurface of 95 | Types.TriangularSurface handedness givenTriangle -> 96 | Types.TriangularSurface (toggle handedness) givenTriangle 97 | 98 | Types.RectangularSurface handedness givenRectangle -> 99 | Types.RectangularSurface (toggle handedness) givenRectangle 100 | 101 | Types.CircularSurface givenCircle -> 102 | Types.CircularSurface (Circle3d.flip givenCircle) 103 | 104 | Types.EllipticalSurface handedness givenEllipse -> 105 | Types.EllipticalSurface (toggle handedness) givenEllipse 106 | 107 | Types.ExtrusionSurface handedness profile displacement -> 108 | Types.ExtrusionSurface (toggle handedness) profile displacement 109 | 110 | Types.RevolutionSurface handedness profile axis angle -> 111 | Types.RevolutionSurface (toggle handedness) profile axis angle 112 | 113 | Types.PlanarSurface handedness region sketchPlane -> 114 | Types.PlanarSurface (toggle handedness) region sketchPlane 115 | -------------------------------------------------------------------------------- /benchmarks/PolygonTriangulation.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module PolygonTriangulation exposing (main) 11 | 12 | import Benchmark exposing (Benchmark) 13 | import Benchmark.Runner exposing (BenchmarkProgram) 14 | import BoundingBox2d exposing (BoundingBox2d) 15 | import Point2d exposing (Point2d) 16 | import Polygon2d exposing (Polygon2d) 17 | import Random.Pcg as Random 18 | import Vector2d exposing (Vector2d) 19 | 20 | 21 | generator : Random.Generator Polygon2d 22 | generator = 23 | let 24 | boundingBox = 25 | BoundingBox2d.fromExtrema 26 | { minX = 0 27 | , maxX = 100 28 | , minY = 0 29 | , maxY = 100 30 | } 31 | 32 | centerPoint = 33 | BoundingBox2d.centerPoint boundingBox 34 | 35 | ( width, height ) = 36 | BoundingBox2d.dimensions boundingBox 37 | 38 | minRadius = 39 | 0.05 * min width height 40 | 41 | maxRadius = 42 | 0.5 * min width height - minRadius 43 | 44 | midRadius = 45 | (minRadius + maxRadius) / 2 46 | 47 | innerRadiusGenerator = 48 | Random.float minRadius (midRadius - 5) 49 | 50 | outerRadiusGenerator = 51 | Random.float (midRadius + 5) maxRadius 52 | 53 | numPoints = 54 | 250 55 | in 56 | Random.list numPoints 57 | (Random.pair innerRadiusGenerator outerRadiusGenerator) 58 | |> Random.map 59 | (List.indexedMap 60 | (\index ( innerRadius, outerRadius ) -> 61 | let 62 | angle = 63 | turns 1 64 | * toFloat index 65 | / toFloat numPoints 66 | 67 | innerRadialVector = 68 | Vector2d.fromPolarComponents 69 | ( innerRadius 70 | , angle 71 | ) 72 | 73 | outerRadialVector = 74 | Vector2d.fromPolarComponents 75 | ( outerRadius 76 | , angle 77 | ) 78 | 79 | innerPoint = 80 | centerPoint 81 | |> Point2d.translateBy 82 | innerRadialVector 83 | 84 | outerPoint = 85 | centerPoint 86 | |> Point2d.translateBy 87 | outerRadialVector 88 | in 89 | ( innerPoint, outerPoint ) 90 | ) 91 | ) 92 | |> Random.map List.unzip 93 | |> Random.map 94 | (\( innerLoop, outerLoop ) -> 95 | Polygon2d.with 96 | { outerLoop = outerLoop 97 | , innerLoops = [ List.reverse innerLoop ] 98 | } 99 | ) 100 | 101 | 102 | testPolygon : Polygon2d 103 | testPolygon = 104 | let 105 | ( result, updatedSeed ) = 106 | Random.step generator (Random.initialSeed 3) 107 | in 108 | result 109 | 110 | 111 | suite : Benchmark 112 | suite = 113 | Benchmark.benchmark "Polygon triangulation" 114 | (\() -> Polygon2d.triangulate testPolygon) 115 | 116 | 117 | main : BenchmarkProgram 118 | main = 119 | Benchmark.Runner.program suite 120 | -------------------------------------------------------------------------------- /sandbox/src/BoundingBox2dTest.elm: -------------------------------------------------------------------------------- 1 | module BoundingBox2dTest exposing (Program, program) 2 | 3 | import Angle exposing (Angle) 4 | import Arc2d exposing (Arc2d) 5 | import BoundingBox2d exposing (BoundingBox2d) 6 | import Browser 7 | import Browser.Events 8 | import Color 9 | import Drawing2d 10 | import Html exposing (Html) 11 | import Json.Decode as Decode exposing (Decoder) 12 | import Pixels exposing (Pixels) 13 | import Point2d exposing (Point2d) 14 | import Random exposing (Generator) 15 | import Random2d 16 | import Rectangle2d exposing (Rectangle2d) 17 | 18 | 19 | type DrawingCoordinates 20 | = DrawingCoordinates 21 | 22 | 23 | type alias DrawingEvent = 24 | Drawing2d.Event DrawingCoordinates Msg 25 | 26 | 27 | type alias Model = 28 | { drawValue : Int -> Drawing2d.Element Pixels DrawingCoordinates DrawingEvent 29 | , drawBounds : Int -> Drawing2d.Element Pixels DrawingCoordinates DrawingEvent 30 | , debugText : Int -> String 31 | , index : Int 32 | } 33 | 34 | 35 | type Msg 36 | = Next 37 | | Previous 38 | 39 | 40 | update : Msg -> Model -> ( Model, Cmd Msg ) 41 | update message model = 42 | case message of 43 | Next -> 44 | ( { model | index = model.index + 1 }, Cmd.none ) 45 | 46 | Previous -> 47 | ( { model | index = max (model.index - 1) 0 }, Cmd.none ) 48 | 49 | 50 | viewBounds : BoundingBox2d Pixels DrawingCoordinates 51 | viewBounds = 52 | BoundingBox2d.from Point2d.origin (Point2d.pixels 400 400) 53 | 54 | 55 | view : Model -> Html Msg 56 | view model = 57 | Html.div [] 58 | [ Html.text ("Test case " ++ String.fromInt model.index) 59 | , Drawing2d.toHtml 60 | { viewBox = Rectangle2d.fromBoundingBox viewBounds 61 | , size = Drawing2d.fixed 62 | } 63 | [] 64 | [ model.drawBounds model.index 65 | , model.drawValue model.index 66 | ] 67 | , Html.text (model.debugText model.index) 68 | ] 69 | 70 | 71 | subscriptions : Sub Msg 72 | subscriptions = 73 | Browser.Events.onKeyDown 74 | (Decode.field "key" Decode.string 75 | |> Decode.andThen 76 | (\key -> 77 | case key of 78 | "ArrowLeft" -> 79 | Decode.succeed Previous 80 | 81 | "ArrowRight" -> 82 | Decode.succeed Next 83 | 84 | "ArrowUp" -> 85 | Decode.succeed Previous 86 | 87 | "ArrowDown" -> 88 | Decode.succeed Next 89 | 90 | _ -> 91 | Decode.fail "Unrecognized key" 92 | ) 93 | ) 94 | 95 | 96 | randomValue : Generator a -> Int -> a 97 | randomValue generator current = 98 | Tuple.first (Random.step generator (Random.initialSeed current)) 99 | 100 | 101 | type alias GeneratorFunction a = 102 | BoundingBox2d Pixels DrawingCoordinates -> Generator a 103 | 104 | 105 | type alias BoundsFunction a = 106 | a -> BoundingBox2d Pixels DrawingCoordinates 107 | 108 | 109 | type alias DrawFunction a = 110 | a -> Drawing2d.Element Pixels DrawingCoordinates (Drawing2d.Event DrawingCoordinates Msg) 111 | 112 | 113 | type alias Program = 114 | Platform.Program () Model Msg 115 | 116 | 117 | program : GeneratorFunction a -> BoundsFunction a -> DrawFunction a -> Program 118 | program generatorFunction boundsFunction drawFunction = 119 | let 120 | generator = 121 | generatorFunction viewBounds 122 | in 123 | Browser.element 124 | { init = 125 | \() -> 126 | ( { drawValue = randomValue generator >> drawFunction 127 | , drawBounds = randomValue generator >> boundsFunction >> Drawing2d.boundingBox [ Drawing2d.strokeColor Color.green ] 128 | , debugText = randomValue generator >> Debug.toString 129 | , index = 0 130 | } 131 | , Cmd.none 132 | ) 133 | , update = update 134 | , view = view 135 | , subscriptions = always subscriptions 136 | } 137 | -------------------------------------------------------------------------------- /benchmarks/CoordinateAccess.elm: -------------------------------------------------------------------------------- 1 | module CoordinateAccess exposing (main) 2 | 3 | import Benchmark exposing (Benchmark) 4 | import Benchmark.Runner exposing (BenchmarkProgram) 5 | import Geometry.Types as Types 6 | import Point2d exposing (Point2d) 7 | import Quantity exposing (Quantity(..), Unitless) 8 | import Random 9 | 10 | 11 | testPoints : List (Point2d Unitless coordinates) 12 | testPoints = 13 | let 14 | pointGenerator = 15 | Random.map2 Point2d.unitless 16 | (Random.float 0 100) 17 | (Random.float 0 100) 18 | 19 | listGenerator = 20 | Random.list 200 pointGenerator 21 | in 22 | Random.step listGenerator (Random.initialSeed 1) 23 | |> Tuple.first 24 | 25 | 26 | centroid : Point2d units coordinates -> List (Point2d units coordinates) -> Point2d units coordinates 27 | centroid (Types.Point2d p0) rest = 28 | centroidHelp p0.x p0.y 1 0 0 rest 29 | 30 | 31 | centroidHelp : Float -> Float -> Float -> Float -> Float -> List (Point2d units coordinates) -> Point2d units coordinates 32 | centroidHelp x0 y0 count dx dy points = 33 | case points of 34 | (Types.Point2d p) :: remaining -> 35 | centroidHelp 36 | x0 37 | y0 38 | (count + 1) 39 | (dx + (p.x - x0)) 40 | (dy + (p.y - y0)) 41 | remaining 42 | 43 | [] -> 44 | Types.Point2d 45 | { x = x0 + dx / count 46 | , y = y0 + dy / count 47 | } 48 | 49 | 50 | centroid1 : 51 | Point2d Unitless coordinates 52 | -> List (Point2d Unitless coordinates) 53 | -> Point2d Unitless coordinates 54 | centroid1 p0 rest = 55 | let 56 | (Quantity x0) = 57 | Point2d.xCoordinate p0 58 | 59 | (Quantity y0) = 60 | Point2d.yCoordinate p0 61 | in 62 | centroidHelp1 x0 y0 1 0 0 rest 63 | 64 | 65 | centroidHelp1 : 66 | Float 67 | -> Float 68 | -> Float 69 | -> Float 70 | -> Float 71 | -> List (Point2d Unitless coordinates) 72 | -> Point2d Unitless coordinates 73 | centroidHelp1 x0 y0 count dx dy points = 74 | case points of 75 | point :: remaining -> 76 | let 77 | (Quantity x) = 78 | Point2d.xCoordinate point 79 | 80 | (Quantity y) = 81 | Point2d.yCoordinate point 82 | in 83 | centroidHelp1 84 | x0 85 | y0 86 | (count + 1) 87 | (dx + (x - x0)) 88 | (dy + (y - y0)) 89 | remaining 90 | 91 | [] -> 92 | Point2d.unitless 93 | (x0 + dx / count) 94 | (y0 + dy / count) 95 | 96 | 97 | centroid2 : 98 | Point2d Unitless coordinates 99 | -> List (Point2d Unitless coordinates) 100 | -> Point2d Unitless coordinates 101 | centroid2 p0 rest = 102 | let 103 | ( Quantity x0, Quantity y0 ) = 104 | Point2d.coordinates p0 105 | in 106 | centroidHelp2 x0 y0 1 0 0 rest 107 | 108 | 109 | centroidHelp2 : 110 | Float 111 | -> Float 112 | -> Float 113 | -> Float 114 | -> Float 115 | -> List (Point2d Unitless coordinates) 116 | -> Point2d Unitless coordinates 117 | centroidHelp2 x0 y0 count dx dy points = 118 | case points of 119 | point :: remaining -> 120 | let 121 | ( Quantity x, Quantity y ) = 122 | Point2d.coordinates point 123 | in 124 | centroidHelp2 125 | x0 126 | y0 127 | (count + 1) 128 | (dx + (x - x0)) 129 | (dy + (y - y0)) 130 | remaining 131 | 132 | [] -> 133 | Point2d.unitless 134 | (x0 + dx / count) 135 | (y0 + dy / count) 136 | 137 | 138 | suite : Benchmark 139 | suite = 140 | Benchmark.compare "Coordinate access" 141 | -- "Default" 142 | -- (\() -> centroid Point2d.origin testPoints) 143 | "Individual" 144 | (\() -> centroid1 Point2d.origin testPoints) 145 | "Tuple" 146 | (\() -> centroid2 Point2d.origin testPoints) 147 | 148 | 149 | main : BenchmarkProgram 150 | main = 151 | Benchmark.Runner.program suite 152 | -------------------------------------------------------------------------------- /sandbox/src/BSplines.elm: -------------------------------------------------------------------------------- 1 | module BSplines exposing (main) 2 | 3 | import Circle2d exposing (Circle2d) 4 | import Color exposing (Color) 5 | import Drawing2d 6 | import Frame2d 7 | import Html exposing (Html) 8 | import Interval 9 | import Length 10 | import LineSegment2d 11 | import Pixels exposing (Pixels) 12 | import Point2d exposing (Point2d) 13 | import Polyline2d exposing (Polyline2d) 14 | import QuadraticSpline2d exposing (QuadraticSpline2d) 15 | import Quantity 16 | import RationalQuadraticSpline2d exposing (RationalQuadraticSpline2d) 17 | import Rectangle2d exposing (Rectangle2d) 18 | import Triangle2d 19 | import Vector2d 20 | 21 | 22 | main : Html Never 23 | main = 24 | let 25 | knots = 26 | [ 0, 0, 2, 3, 4, 5, 6, 8, 12, 12 ] 27 | 28 | weightedControlPoints = 29 | [ ( Point2d.meters 1 8, 2 ) 30 | , ( Point2d.meters 4 5, 3 ) 31 | , ( Point2d.meters 2 4, 1 ) 32 | , ( Point2d.meters 4 1, 3 ) 33 | , ( Point2d.meters 8 2, 1 ) 34 | , ( Point2d.meters 5 6, 5 ) 35 | , ( Point2d.meters 8 9, 1 ) 36 | , ( Point2d.meters 9 7, 2 ) 37 | , ( Point2d.meters 9 4, 1 ) 38 | ] 39 | |> List.map (Tuple.mapFirst (Point2d.at (Pixels.float 80 |> Quantity.per Length.meter))) 40 | |> List.map (Tuple.mapSecond identity) 41 | 42 | dot point = 43 | Drawing2d.circle [] (Circle2d.withRadius (Pixels.float 3) point) 44 | 45 | points = 46 | List.map Tuple.first weightedControlPoints 47 | 48 | segments = 49 | RationalQuadraticSpline2d.bSplineSegments knots weightedControlPoints 50 | 51 | knotIntervals = 52 | RationalQuadraticSpline2d.bSplineIntervals knots 53 | 54 | arrow attributes point vector = 55 | let 56 | endPoint = 57 | point |> Point2d.translateBy vector 58 | in 59 | case Vector2d.direction vector of 60 | Just direction -> 61 | let 62 | tipFrame = 63 | Frame2d.withXDirection direction endPoint 64 | in 65 | Drawing2d.group attributes 66 | [ Drawing2d.lineSegment [] (LineSegment2d.from point endPoint) 67 | , Drawing2d.placeIn tipFrame <| 68 | Drawing2d.triangle [] <| 69 | Triangle2d.from 70 | (Point2d.pixels -5 -3) 71 | Point2d.origin 72 | (Point2d.pixels -5 3) 73 | ] 74 | 75 | Nothing -> 76 | Drawing2d.empty 77 | 78 | drawSegment segment knotInterval = 79 | let 80 | startPoint = 81 | RationalQuadraticSpline2d.startPoint segment 82 | 83 | endPoint = 84 | RationalQuadraticSpline2d.endPoint segment 85 | 86 | startDerivative = 87 | RationalQuadraticSpline2d.startDerivative segment 88 | |> Vector2d.scaleBy (1 / Interval.width knotInterval) 89 | |> Vector2d.scaleBy 0.5 90 | 91 | endDerivative = 92 | RationalQuadraticSpline2d.endDerivative segment 93 | |> Vector2d.scaleBy (1 / Interval.width knotInterval) 94 | |> Vector2d.scaleBy 0.5 95 | in 96 | Drawing2d.group [ Drawing2d.blackFill ] 97 | [ Drawing2d.polyline [] (RationalQuadraticSpline2d.segments 100 segment) 98 | , dot (RationalQuadraticSpline2d.startPoint segment) 99 | , dot (RationalQuadraticSpline2d.endPoint segment) 100 | , arrow [ Drawing2d.strokeColor Color.blue, Drawing2d.fillColor Color.blue ] startPoint startDerivative 101 | , arrow [ Drawing2d.strokeColor Color.orange, Drawing2d.fillColor Color.orange ] endPoint endDerivative 102 | ] 103 | in 104 | Drawing2d.toHtml 105 | { viewBox = Rectangle2d.from (Point2d.pixels -100 0) (Point2d.pixels 800 800) 106 | , size = Drawing2d.fixed 107 | } 108 | [] 109 | [ Drawing2d.polyline [ Drawing2d.strokeColor Color.grey ] (Polyline2d.fromVertices points) 110 | , Drawing2d.group [] (List.map2 drawSegment segments knotIntervals) 111 | , Drawing2d.group [] (List.map dot points) 112 | ] 113 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Thanks for your interest in `elm-geometry`! Currently, the best way to 4 | contribute is simply to [open a new issue](https://github.com/ianmackenzie/elm-geometry/issues) 5 | for any new features you're interested in or any bugs you notice (including 6 | things like misleading or confusing documentation - documentation issues are 7 | just as important as code issues!). If you're willing to help fix the 8 | bug/implement the feature, please mention that in the issue, but it's certainly 9 | not a requirement! 10 | 11 | Another great way to contribute is to look for [issues marked with *question*](https://github.com/ianmackenzie/elm-geometry/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion), 12 | which are ones where I'm actively seeking feedback from the community on whether 13 | something should be done or what the right approach is. Feedback is welcome on 14 | any issue, though! 15 | 16 | In general, I try to follow [the Elm guidelines](https://twitter.com/czaplic/status/928359033844539393) 17 | and ask that you try to as well: 18 | 19 | - Be kind. 20 | - Learn from everyone. 21 | - Collaboration requires communication. 22 | - Not every problem should be solved with code. 23 | - Communication _is_ contribution. 24 | - Understand the problem. 25 | - Explore all possible solutions. 26 | - Pick one. 27 | - Simplicity is not just for beginners. 28 | - It's better to do it _right_ than to do it _right now_. 29 | - It's not done until the docs are great. 30 | - Take responsibility for user experiences. 31 | - Make impossible states impossible. 32 | - There are worse things than being explicit... 33 | 34 | # Contributing changes 35 | 36 | If you _are_ interested in contributing changes to `elm-geometry`, please fork 37 | this repository, make a new branch in your fork, commit your changes to that 38 | branch and then make a pull request from that branch (although please open an 39 | issue first for major contributions before writing too much code, so we can 40 | discuss different potential approaches). As part of your pull request, make sure 41 | that you add yourself to the [AUTHORS](https://github.com/opensolid/geometry/blob/master/AUTHORS) 42 | file! Definitely reach out on the [Elm Slack](http://elmlang.herokuapp.com/) if 43 | you have questions (I'm **ianmackenzie**). 44 | 45 | ## Writing code 46 | 47 | When writing your code, try to follow existing code style as much as possible - 48 | in particular, this means: 49 | 50 | - Use [`elm-format`](https://github.com/avh4/elm-format) to format your code. 51 | I currently use `elm-format` version 0.8.0. 52 | - Wrap code (mostly) to 80 columns (type annotations and string literals can 53 | be longer if you want). 54 | 55 | Don't worry too much about writing documentation - small fixes for things like 56 | typos and formatting are certainly welcome, but I would prefer to write the bulk 57 | of the documentation myself to ensure a consistent style and tone throughout. 58 | 59 | ## Testing 60 | 61 | During development, please run the existing tests periodically to make sure you 62 | haven't accidentally broken anything! To run the tests: 63 | 64 | - Install [`elm-test`](https://github.com/rtfeldman/node-test-runner) by running `npm install -g elm-test` 65 | - Run `elm-test` from the root directory of this repository 66 | 67 | If you are working on fixing a bug, please first add a test that catches the bug 68 | to the relevant file in the `tests` subdirectory, then add your fix and verify 69 | that the test now passes. If you are adding a new feature, writing tests for 70 | your feature is appreciated but not mandatory. 71 | 72 | ## Committing 73 | 74 | Git commits should generally be as small and focused as possible, so that they 75 | can be reviewed individually. Commit messages should follow [the seven rules of 76 | a great Git commit message](https://chris.beams.io/posts/git-commit/#seven-rules): 77 | 78 | - Separate subject from body with a blank line 79 | - Limit the subject line to 50 characters 80 | - Capitalize the subject line 81 | - Do not end the subject line with a period 82 | - Use the imperative mood in the subject line 83 | - Wrap the body at 72 characters 84 | - Use the body to explain what and why vs. how 85 | 86 | Here are some sample commits to use as examples: 87 | 88 | - [Fix bug in Arc2d.fromEndpoints](https://github.com/ianmackenzie/elm-geometry/commit/593039e1223727afe04c53b3af170dfa2b9725b0) 89 | - [Test Frame3d a bit more precisely](https://github.com/ianmackenzie/elm-geometry/commit/bcf22c03ede5b7594dbcbde02a49430311d53679) 90 | - [Tweak triangle test to avoid spurious failure](https://github.com/ianmackenzie/elm-geometry/commit/bce5df26e5646f14577cd60472fab03101346a74) 91 | - [Add [Point,Vector]#d.interpolateFrom](https://github.com/ianmackenzie/elm-geometry/commit/0c91e5eaf4089d94783c28f2a10ece3005be89e4) 92 | -------------------------------------------------------------------------------- /tests/Tests/Block3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Block3d exposing 2 | ( containmentIsConsistent 3 | , verticesAreConsistent 4 | ) 5 | 6 | import Angle exposing (Angle) 7 | import Axis3d exposing (Axis3d) 8 | import Block3d exposing (Block3d) 9 | import Expect 10 | import Frame3d 11 | import Geometry.Expect as Expect 12 | import Geometry.Random as Random 13 | import Length exposing (Meters) 14 | import LineSegment3d exposing (LineSegment3d) 15 | import Plane3d exposing (Plane3d) 16 | import Point3d exposing (Point3d) 17 | import Random exposing (Generator) 18 | import Random.Extra 19 | import Test exposing (Test) 20 | import Test.Random as Test 21 | import Vector3d exposing (Vector3d) 22 | 23 | 24 | type alias Transformation coordinates = 25 | { block : Block3d Meters coordinates -> Block3d Meters coordinates 26 | , point : Point3d Meters coordinates -> Point3d Meters coordinates 27 | , lineSegment : LineSegment3d Meters coordinates -> LineSegment3d Meters coordinates 28 | , isZeroScale : Bool 29 | } 30 | 31 | 32 | rotation : Axis3d Meters coordinates -> Angle -> Transformation coordinates 33 | rotation axis angle = 34 | { block = Block3d.rotateAround axis angle 35 | , point = Point3d.rotateAround axis angle 36 | , lineSegment = LineSegment3d.rotateAround axis angle 37 | , isZeroScale = False 38 | } 39 | 40 | 41 | translation : Vector3d Meters coordinates -> Transformation coordinates 42 | translation displacement = 43 | { block = Block3d.translateBy displacement 44 | , point = Point3d.translateBy displacement 45 | , lineSegment = LineSegment3d.translateBy displacement 46 | , isZeroScale = False 47 | } 48 | 49 | 50 | scaling : Point3d Meters coordinates -> Float -> Transformation coordinates 51 | scaling centerPoint scale = 52 | { block = Block3d.scaleAbout centerPoint scale 53 | , point = Point3d.scaleAbout centerPoint scale 54 | , lineSegment = LineSegment3d.scaleAbout centerPoint scale 55 | , isZeroScale = scale == 0.0 56 | } 57 | 58 | 59 | mirroring : Plane3d Meters coordinates -> Transformation coordinates 60 | mirroring plane = 61 | { block = Block3d.mirrorAcross plane 62 | , point = Point3d.mirrorAcross plane 63 | , lineSegment = LineSegment3d.mirrorAcross plane 64 | , isZeroScale = False 65 | } 66 | 67 | 68 | transformationGenerator : Generator (Transformation coordinates) 69 | transformationGenerator = 70 | Random.Extra.choices 71 | (Random.map2 rotation Random.axis3d Random.angle) 72 | [ Random.map translation Random.vector3d 73 | , Random.map2 scaling Random.point3d Random.scale 74 | , Random.map mirroring Random.plane3d 75 | ] 76 | 77 | 78 | containmentIsConsistent : Test 79 | containmentIsConsistent = 80 | Test.check3 "Block/point containment is consistent through transformation" 81 | transformationGenerator 82 | Random.block3d 83 | Random.point3d 84 | (\transformation block point -> 85 | let 86 | initialContainment = 87 | Block3d.contains point block 88 | 89 | transformedPoint = 90 | transformation.point point 91 | 92 | transformedBlock = 93 | transformation.block block 94 | 95 | finalContainment = 96 | Block3d.contains transformedPoint transformedBlock 97 | in 98 | if transformation.isZeroScale then 99 | finalContainment |> Expect.equal True 100 | 101 | else 102 | finalContainment |> Expect.equal initialContainment 103 | ) 104 | 105 | 106 | verticesAreConsistent : Test 107 | verticesAreConsistent = 108 | let 109 | testVertex description accessor = 110 | Test.check2 description 111 | transformationGenerator 112 | Random.block3d 113 | (\transformation block -> 114 | let 115 | vertex = 116 | accessor block 117 | 118 | transformedBlock = 119 | transformation.block block 120 | 121 | transformedVertex = 122 | transformation.point vertex 123 | 124 | vertexOfTransformed = 125 | accessor transformedBlock 126 | in 127 | vertexOfTransformed |> Expect.point3d transformedVertex 128 | ) 129 | in 130 | Test.describe "Vertices are consistent through transformation" 131 | [ testVertex "Back top left" (\block -> Block3d.interpolate block 0 1 1) 132 | , testVertex "Back bottom right" (\block -> Block3d.interpolate block 0 0 0) 133 | , testVertex "Front bottom left" (\block -> Block3d.interpolate block 1 1 0) 134 | , testVertex "Front top right" (\block -> Block3d.interpolate block 1 0 1) 135 | ] 136 | -------------------------------------------------------------------------------- /tests/Tests/Rectangle2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Rectangle2d exposing 2 | ( containmentIsConsistent 3 | , verticesAreConsistent 4 | ) 5 | 6 | import Angle exposing (Angle) 7 | import Axis2d exposing (Axis2d) 8 | import Expect 9 | import Frame2d 10 | import Geometry.Expect as Expect 11 | import Geometry.Random as Random 12 | import Length exposing (Meters) 13 | import LineSegment2d exposing (LineSegment2d) 14 | import Point2d exposing (Point2d) 15 | import Random exposing (Generator) 16 | import Random.Extra 17 | import Rectangle2d exposing (Rectangle2d) 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Vector2d exposing (Vector2d) 21 | 22 | 23 | type alias Transformation coordinates = 24 | { rectangle : Rectangle2d Meters coordinates -> Rectangle2d Meters coordinates 25 | , point : Point2d Meters coordinates -> Point2d Meters coordinates 26 | , lineSegment : LineSegment2d Meters coordinates -> LineSegment2d Meters coordinates 27 | , isZeroScale : Bool 28 | } 29 | 30 | 31 | rotation : Point2d Meters coordinates -> Angle -> Transformation coordinates 32 | rotation centerPoint angle = 33 | { rectangle = Rectangle2d.rotateAround centerPoint angle 34 | , point = Point2d.rotateAround centerPoint angle 35 | , lineSegment = LineSegment2d.rotateAround centerPoint angle 36 | , isZeroScale = False 37 | } 38 | 39 | 40 | translation : Vector2d Meters coordinates -> Transformation coordinates 41 | translation displacement = 42 | { rectangle = Rectangle2d.translateBy displacement 43 | , point = Point2d.translateBy displacement 44 | , lineSegment = LineSegment2d.translateBy displacement 45 | , isZeroScale = False 46 | } 47 | 48 | 49 | scaling : Point2d Meters coordinates -> Float -> Transformation coordinates 50 | scaling centerPoint scale = 51 | { rectangle = Rectangle2d.scaleAbout centerPoint scale 52 | , point = Point2d.scaleAbout centerPoint scale 53 | , lineSegment = LineSegment2d.scaleAbout centerPoint scale 54 | , isZeroScale = scale == 0.0 55 | } 56 | 57 | 58 | mirroring : Axis2d Meters coordinates -> Transformation coordinates 59 | mirroring axis = 60 | { rectangle = Rectangle2d.mirrorAcross axis 61 | , point = Point2d.mirrorAcross axis 62 | , lineSegment = LineSegment2d.mirrorAcross axis 63 | , isZeroScale = False 64 | } 65 | 66 | 67 | transformationGenerator : Generator (Transformation coordinates) 68 | transformationGenerator = 69 | Random.Extra.choices 70 | (Random.map2 rotation Random.point2d Random.angle) 71 | [ Random.map translation Random.vector2d 72 | , Random.map2 scaling Random.point2d Random.scale 73 | , Random.map mirroring Random.axis2d 74 | ] 75 | 76 | 77 | containmentIsConsistent : Test 78 | containmentIsConsistent = 79 | Test.check3 "Rectangle/point containment is consistent through transformation" 80 | transformationGenerator 81 | Random.rectangle2d 82 | Random.point2d 83 | (\transformation rectangle point -> 84 | let 85 | initialContainment = 86 | Rectangle2d.contains point rectangle 87 | 88 | transformedPoint = 89 | transformation.point point 90 | 91 | transformedRectangle = 92 | transformation.rectangle rectangle 93 | 94 | finalContainment = 95 | Rectangle2d.contains transformedPoint transformedRectangle 96 | in 97 | if transformation.isZeroScale then 98 | finalContainment |> Expect.equal True 99 | 100 | else 101 | finalContainment |> Expect.equal initialContainment 102 | ) 103 | 104 | 105 | verticesAreConsistent : Test 106 | verticesAreConsistent = 107 | let 108 | testVertex description accessor = 109 | Test.check2 description 110 | transformationGenerator 111 | Random.rectangle2d 112 | (\transformation rectangle -> 113 | let 114 | vertex = 115 | accessor rectangle 116 | 117 | transformedRectangle = 118 | transformation.rectangle rectangle 119 | 120 | transformedVertex = 121 | transformation.point vertex 122 | 123 | vertexOfTransformed = 124 | accessor transformedRectangle 125 | in 126 | vertexOfTransformed |> Expect.point2d transformedVertex 127 | ) 128 | in 129 | Test.describe "Vertices are consistent through transformation" 130 | [ testVertex "Bottom left" (\rectangle -> Rectangle2d.interpolate rectangle 0 0) 131 | , testVertex "Bottom right" (\rectangle -> Rectangle2d.interpolate rectangle 1 0) 132 | , testVertex "Top left" (\rectangle -> Rectangle2d.interpolate rectangle 1 1) 133 | , testVertex "Top right" (\rectangle -> Rectangle2d.interpolate rectangle 0 1) 134 | ] 135 | -------------------------------------------------------------------------------- /sandbox/src/PointInPolygon.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module PointInPolygon exposing (main) 11 | 12 | import Angle exposing (Angle) 13 | import Browser 14 | import Circle2d 15 | import Color 16 | import Drawing2d 17 | import Element exposing (Element) 18 | import Element.Background as Background 19 | import Element.Events 20 | import Element.Input as Input 21 | import Html exposing (Html) 22 | import Html.Attributes 23 | import Html.Events 24 | import Pixels exposing (Pixels) 25 | import Point2d exposing (Point2d) 26 | import Polygon2d exposing (Polygon2d) 27 | import Polygon2d.Random as Random 28 | import Random exposing (Generator) 29 | import Rectangle2d exposing (Rectangle2d) 30 | 31 | 32 | type ScreenCoordinates 33 | = ScreenCoordinates 34 | 35 | 36 | type alias Model = 37 | { polygon : Polygon2d Pixels ScreenCoordinates 38 | , points : List (Point2d Pixels ScreenCoordinates) 39 | , angle : Angle 40 | } 41 | 42 | 43 | type Msg 44 | = Click 45 | | NewPolygon (Polygon2d Pixels ScreenCoordinates) 46 | | SetAngle Angle 47 | 48 | 49 | renderBounds : Rectangle2d Pixels ScreenCoordinates 50 | renderBounds = 51 | Rectangle2d.from Point2d.origin (Point2d.pixels 600 600) 52 | 53 | 54 | generateNewPolygon : Cmd Msg 55 | generateNewPolygon = 56 | Random.generate NewPolygon (Random.polygon2d (Rectangle2d.boundingBox renderBounds)) 57 | 58 | 59 | pointGenerator : Generator (Point2d Pixels ScreenCoordinates) 60 | pointGenerator = 61 | Random.map2 (Rectangle2d.interpolate renderBounds) 62 | (Random.float 0 1) 63 | (Random.float 0 1) 64 | 65 | 66 | init : () -> ( Model, Cmd Msg ) 67 | init () = 68 | ( { polygon = Polygon2d.singleLoop [] 69 | , angle = Angle.degrees 0 70 | , points = Tuple.first (Random.step (Random.list 500 pointGenerator) (Random.initialSeed 1234)) 71 | } 72 | , generateNewPolygon 73 | ) 74 | 75 | 76 | update : Msg -> Model -> ( Model, Cmd Msg ) 77 | update message model = 78 | case message of 79 | Click -> 80 | ( model, generateNewPolygon ) 81 | 82 | NewPolygon polygon -> 83 | ( { model | polygon = polygon }, Cmd.none ) 84 | 85 | SetAngle angle -> 86 | ( { model | angle = angle }, Cmd.none ) 87 | 88 | 89 | view : Model -> Html Msg 90 | view model = 91 | let 92 | ( width, height ) = 93 | Rectangle2d.dimensions renderBounds 94 | 95 | rotatedPolygon = 96 | Polygon2d.rotateAround (Rectangle2d.centerPoint renderBounds) 97 | model.angle 98 | model.polygon 99 | 100 | polygonElement = 101 | Drawing2d.polygon 102 | [ Drawing2d.noFill, Drawing2d.blackStroke ] 103 | rotatedPolygon 104 | 105 | drawPoint point = 106 | let 107 | color = 108 | if Polygon2d.contains point rotatedPolygon then 109 | Color.black 110 | 111 | else 112 | Color.lightGrey 113 | in 114 | Drawing2d.circle [ Drawing2d.noBorder, Drawing2d.fillColor color ] 115 | (Circle2d.withRadius (Pixels.float 2) point) 116 | in 117 | Element.layout [] <| 118 | Element.column [] 119 | [ Element.el [ Element.Events.onClick Click ] <| 120 | Element.html <| 121 | Drawing2d.toHtml 122 | { size = Drawing2d.fixed 123 | , viewBox = renderBounds 124 | } 125 | [] 126 | [ Drawing2d.group [] (List.map drawPoint model.points) 127 | , polygonElement 128 | ] 129 | , Input.slider 130 | [ Element.width (Element.px (round (Pixels.toFloat width))) 131 | , Element.height (Element.px 8) 132 | , Background.color (Element.rgb 0.75 0.75 0.75) 133 | ] 134 | { onChange = Angle.degrees >> SetAngle 135 | , min = -180 136 | , max = 180 137 | , step = Just 1 138 | , label = Input.labelHidden "Angle" 139 | , value = Angle.inDegrees model.angle 140 | , thumb = Input.defaultThumb 141 | } 142 | ] 143 | 144 | 145 | main : Program () Model Msg 146 | main = 147 | Browser.element 148 | { init = init 149 | , update = update 150 | , view = view 151 | , subscriptions = always Sub.none 152 | } 153 | -------------------------------------------------------------------------------- /tests/Tests/DelaunayTriangulation2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.DelaunayTriangulation2d exposing 2 | ( allDelaunayTrianglesHaveNonzeroArea 3 | , delaunayTriangleContainsOnlyItsVertices 4 | , failsOnCoincidentVertices 5 | ) 6 | 7 | import Array exposing (Array) 8 | import Circle2d 9 | import DelaunayTriangulation2d 10 | import Direction3d 11 | import Expect 12 | import Geometry.Expect as Expect 13 | import Geometry.Random as Random 14 | import Length exposing (Meters, inMeters, meters) 15 | import List.Extra 16 | import Plane3d 17 | import Point2d exposing (Point2d) 18 | import Point3d 19 | import Quantity 20 | import Random exposing (Generator) 21 | import SketchPlane3d 22 | import Test exposing (Test) 23 | import Test.Random as Test 24 | import Triangle2d 25 | import Vector3d 26 | 27 | 28 | uniquePoints : Generator (Array (Point2d Meters coordinates)) 29 | uniquePoints = 30 | Random.smallList Random.point2d 31 | |> Random.map (List.Extra.uniqueBy (Point2d.toTuple inMeters)) 32 | |> Random.map Array.fromList 33 | 34 | 35 | allDelaunayTrianglesHaveNonzeroArea : Test 36 | allDelaunayTrianglesHaveNonzeroArea = 37 | let 38 | description = 39 | "The delaunay triangulation only produces triangles with non-zero area" 40 | 41 | expectation points = 42 | case DelaunayTriangulation2d.fromPoints points of 43 | Err _ -> 44 | Expect.pass 45 | 46 | Ok triangulation -> 47 | let 48 | triangles = 49 | DelaunayTriangulation2d.triangles triangulation 50 | 51 | hasNonPositiveArea triangle = 52 | Triangle2d.area triangle 53 | |> Quantity.lessThanOrEqualTo Quantity.zero 54 | in 55 | case List.filter hasNonPositiveArea triangles of 56 | [] -> 57 | Expect.pass 58 | 59 | x :: xs -> 60 | Expect.fail ("DelaunayTriangulation2d produced a triangle with negative or zero area: " ++ Debug.toString x) 61 | in 62 | Test.check description uniquePoints expectation 63 | 64 | 65 | delaunayTriangleContainsOnlyItsVertices : Test 66 | delaunayTriangleContainsOnlyItsVertices = 67 | let 68 | description = 69 | "A delaunay triangle's circumcircle only contains its three vertices, no other points" 70 | 71 | expectation points = 72 | let 73 | check triangle = 74 | case Triangle2d.circumcircle triangle of 75 | Nothing -> 76 | Err ("A delaunay triangle is degenerate: " ++ Debug.toString triangle) 77 | 78 | Just circle -> 79 | let 80 | ( p1, p2, p3 ) = 81 | Triangle2d.vertices triangle 82 | 83 | predicate point = 84 | Circle2d.contains point circle 85 | && (point /= p1) 86 | && (point /= p2) 87 | && (point /= p3) 88 | 89 | containedPoints = 90 | Array.filter predicate points 91 | in 92 | if Array.isEmpty containedPoints then 93 | Ok () 94 | 95 | else 96 | Err ("A delaunay triangle circumcircle contains non-vertex points " ++ Debug.toString containedPoints) 97 | 98 | checkAll remainingTriangles = 99 | case remainingTriangles of 100 | [] -> 101 | Expect.pass 102 | 103 | triangle :: rest -> 104 | case check triangle of 105 | Ok _ -> 106 | checkAll rest 107 | 108 | Err errorMessage -> 109 | Expect.fail errorMessage 110 | in 111 | case DelaunayTriangulation2d.fromPoints points of 112 | Err _ -> 113 | Expect.pass 114 | 115 | Ok triangulation -> 116 | checkAll (DelaunayTriangulation2d.triangles triangulation) 117 | in 118 | Test.check description uniquePoints expectation 119 | 120 | 121 | failsOnCoincidentVertices : Test 122 | failsOnCoincidentVertices = 123 | let 124 | description = 125 | "Delaunay triangulation construction should fail when coincident vertices are given" 126 | 127 | expectation points = 128 | case points of 129 | [] -> 130 | Expect.pass 131 | 132 | x :: xs -> 133 | let 134 | pointsWithDuplicate = 135 | Array.fromList (x :: x :: xs) 136 | in 137 | DelaunayTriangulation2d.fromPoints pointsWithDuplicate 138 | |> Expect.err 139 | in 140 | Test.check description (Random.smallList Random.point2d) expectation 141 | -------------------------------------------------------------------------------- /tests/Tests/Cone3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Cone3d exposing (suite) 2 | 3 | import Angle exposing (Angle) 4 | import Axis3d exposing (Axis3d) 5 | import Circle3d exposing (Circle3d) 6 | import Cone3d exposing (Cone3d) 7 | import Expect 8 | import Frame3d exposing (Frame3d) 9 | import Geometry.Expect as Expect 10 | import Geometry.Random as Random 11 | import Length exposing (Meters) 12 | import Plane3d exposing (Plane3d) 13 | import Point3d exposing (Point3d) 14 | import Quantity exposing (Quantity) 15 | import Random exposing (Generator) 16 | import Random.Extra 17 | import Test exposing (Test) 18 | import Test.Random as Test 19 | import Vector3d exposing (Vector3d) 20 | 21 | 22 | type alias Transformation coordinates = 23 | { cone : Cone3d Meters coordinates -> Cone3d Meters coordinates 24 | , point : Point3d Meters coordinates -> Point3d Meters coordinates 25 | , circle : Circle3d Meters coordinates -> Circle3d Meters coordinates 26 | , isZeroScale : Bool 27 | } 28 | 29 | 30 | rotation : Axis3d Meters coordinates -> Angle -> Transformation coordinates 31 | rotation axis angle = 32 | { cone = Cone3d.rotateAround axis angle 33 | , point = Point3d.rotateAround axis angle 34 | , circle = Circle3d.rotateAround axis angle 35 | , isZeroScale = False 36 | } 37 | 38 | 39 | translation : Vector3d Meters coordinates -> Transformation coordinates 40 | translation displacement = 41 | { cone = Cone3d.translateBy displacement 42 | , point = Point3d.translateBy displacement 43 | , circle = Circle3d.translateBy displacement 44 | , isZeroScale = False 45 | } 46 | 47 | 48 | scaling : Point3d Meters coordinates -> Float -> Transformation coordinates 49 | scaling centerPoint scale = 50 | { cone = Cone3d.scaleAbout centerPoint scale 51 | , point = Point3d.scaleAbout centerPoint scale 52 | , circle = Circle3d.scaleAbout centerPoint scale 53 | , isZeroScale = scale == 0.0 54 | } 55 | 56 | 57 | mirroring : Plane3d Meters coordinates -> Transformation coordinates 58 | mirroring plane = 59 | { cone = Cone3d.mirrorAcross plane 60 | , point = Point3d.mirrorAcross plane 61 | , circle = Circle3d.mirrorAcross plane 62 | , isZeroScale = False 63 | } 64 | 65 | 66 | transformationGenerator : Generator (Transformation coordinates) 67 | transformationGenerator = 68 | Random.Extra.choices 69 | (Random.map2 rotation Random.axis3d Random.angle) 70 | [ Random.map translation Random.vector3d 71 | , Random.map2 scaling Random.point3d Random.scale 72 | , Random.map mirroring Random.plane3d 73 | ] 74 | 75 | 76 | coneAndPoint : Generator ( Cone3d Meters coordinates, Point3d Meters coordinates ) 77 | coneAndPoint = 78 | Random.map4 79 | (\cone u v theta -> 80 | let 81 | halfLength = 82 | Quantity.half (Cone3d.length cone) 83 | 84 | minZ = 85 | Quantity.multiplyBy -1.25 halfLength 86 | 87 | maxZ = 88 | Quantity.multiplyBy 1.25 halfLength 89 | 90 | radius = 91 | Cone3d.radius cone 92 | 93 | coneFrame = 94 | Frame3d.fromZAxis (Cone3d.axis cone) 95 | 96 | z = 97 | Quantity.interpolateFrom minZ maxZ u 98 | 99 | r = 100 | Quantity.sqrt (Quantity.multiplyBy (v * 1.25) (Quantity.squared radius)) 101 | 102 | x = 103 | r |> Quantity.multiplyBy (Angle.cos theta) 104 | 105 | y = 106 | r |> Quantity.multiplyBy (Angle.sin theta) 107 | in 108 | ( cone, Point3d.xyzIn coneFrame x y z ) 109 | ) 110 | Random.cone3d 111 | Random.parameterValue 112 | Random.parameterValue 113 | Random.angle 114 | 115 | 116 | suite : Test 117 | suite = 118 | Test.describe "Cone3d" 119 | [ Test.check2 "Point containment is consistent" 120 | coneAndPoint 121 | transformationGenerator 122 | (\( cone, point ) transformation -> 123 | let 124 | initialContainment = 125 | Cone3d.contains point cone 126 | 127 | transformedPoint = 128 | transformation.point point 129 | 130 | transformedCone = 131 | transformation.cone cone 132 | 133 | finalContainment = 134 | Cone3d.contains transformedPoint transformedCone 135 | in 136 | if transformation.isZeroScale then 137 | finalContainment |> Expect.equal True 138 | 139 | else 140 | finalContainment |> Expect.equal initialContainment 141 | ) 142 | , Test.check2 "Base is consistent through transformation" 143 | transformationGenerator 144 | Random.cone3d 145 | (\transformation cone -> 146 | let 147 | base = 148 | Cone3d.base cone 149 | 150 | transformedCone = 151 | transformation.cone cone 152 | 153 | transformedBase = 154 | transformation.circle base 155 | 156 | baseOfTransformed = 157 | Cone3d.base transformedCone 158 | in 159 | baseOfTransformed |> Expect.circle3d transformedBase 160 | ) 161 | ] 162 | -------------------------------------------------------------------------------- /tests/Tests/Arc2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Arc2d exposing 2 | ( evaluateHalfIsMidpoint 3 | , evaluateOneIsEndPoint 4 | , evaluateZeroIsStartPoint 5 | , from 6 | , genericTests 7 | , mirroredCenterPoint 8 | , reverseFlipsDirection 9 | , reverseKeepsMidpoint 10 | , withRadius 11 | ) 12 | 13 | import Angle 14 | import Arc2d exposing (Arc2d) 15 | import Expect 16 | import Geometry.Expect as Expect 17 | import Geometry.Random as Random 18 | import Length exposing (Meters, meters) 19 | import Point2d 20 | import Quantity exposing (zero) 21 | import Random 22 | import Random.Extra 23 | import SweptAngle 24 | import Test exposing (Test) 25 | import Test.Random as Test 26 | import Tests.Generic.Curve2d 27 | 28 | 29 | evaluateZeroIsStartPoint : Test 30 | evaluateZeroIsStartPoint = 31 | Test.check "Evaluating at t=0 returns start point" 32 | Random.arc2d 33 | (\arc -> Arc2d.pointOn arc 0 |> Expect.point2d (Arc2d.startPoint arc)) 34 | 35 | 36 | evaluateOneIsEndPoint : Test 37 | evaluateOneIsEndPoint = 38 | Test.check "Evaluating at t=1 returns end point" 39 | Random.arc2d 40 | (\arc -> Arc2d.pointOn arc 1 |> Expect.point2d (Arc2d.endPoint arc)) 41 | 42 | 43 | evaluateHalfIsMidpoint : Test 44 | evaluateHalfIsMidpoint = 45 | Test.check "Evaluating at t=0.5 returns midpoint" 46 | Random.arc2d 47 | (\arc -> Arc2d.pointOn arc 0.5 |> Expect.point2d (Arc2d.midpoint arc)) 48 | 49 | 50 | reverseKeepsMidpoint : Test 51 | reverseKeepsMidpoint = 52 | Test.check "Reversing an arc keeps the midpoint" 53 | Random.arc2d 54 | (\arc -> 55 | Arc2d.midpoint (Arc2d.reverse arc) 56 | |> Expect.point2d (Arc2d.midpoint arc) 57 | ) 58 | 59 | 60 | reverseFlipsDirection : Test 61 | reverseFlipsDirection = 62 | Test.check2 "Reversing an arc is consistent with reversed evaluation" 63 | Random.arc2d 64 | Random.parameterValue 65 | (\arc parameterValue -> 66 | Arc2d.pointOn (Arc2d.reverse arc) parameterValue 67 | |> Expect.point2d 68 | (Arc2d.pointOn arc (1 - parameterValue)) 69 | ) 70 | 71 | 72 | from : Test 73 | from = 74 | let 75 | validAngle = 76 | Random.map Angle.degrees <| 77 | Random.Extra.choices 78 | (Random.float -359 359) 79 | [ Random.float 361 719 80 | , Random.float -719 -361 81 | ] 82 | in 83 | Test.check3 "Arc2d.from produces the expected end point and swept angle" 84 | Random.point2d 85 | Random.point2d 86 | validAngle 87 | (\startPoint endPoint sweptAngle -> 88 | Arc2d.from startPoint endPoint sweptAngle 89 | |> Expect.all 90 | [ Arc2d.endPoint >> Expect.point2d endPoint 91 | , Arc2d.sweptAngle >> Expect.quantity sweptAngle 92 | ] 93 | ) 94 | 95 | 96 | withRadius : Test 97 | withRadius = 98 | let 99 | sweptAngleGenerator = 100 | Random.uniform 101 | SweptAngle.smallPositive 102 | [ SweptAngle.smallNegative 103 | , SweptAngle.largePositive 104 | , SweptAngle.largeNegative 105 | ] 106 | in 107 | Test.check4 "Arc2d.withRadius produces the expected end point" 108 | Random.positiveLength 109 | sweptAngleGenerator 110 | Random.point2d 111 | Random.point2d 112 | (\radius sweptAngleType startPoint endPoint -> 113 | case Arc2d.withRadius radius sweptAngleType startPoint endPoint of 114 | Just arc -> 115 | arc |> Arc2d.endPoint |> Expect.point2d endPoint 116 | 117 | Nothing -> 118 | let 119 | distance = 120 | Point2d.distanceFrom startPoint endPoint 121 | in 122 | if distance == zero then 123 | Expect.pass 124 | 125 | else 126 | distance |> Expect.quantityGreaterThan (Quantity.multiplyBy 2 radius) 127 | ) 128 | 129 | 130 | curveOperations : Tests.Generic.Curve2d.Operations (Arc2d Meters coordinates) coordinates 131 | curveOperations = 132 | { generator = Random.arc2d 133 | , pointOn = Arc2d.pointOn 134 | , boundingBox = Arc2d.boundingBox 135 | , firstDerivative = Arc2d.firstDerivative 136 | , firstDerivativeBoundingBox = Arc2d.firstDerivativeBoundingBox 137 | , scaleAbout = Arc2d.scaleAbout 138 | , translateBy = Arc2d.translateBy 139 | , rotateAround = Arc2d.rotateAround 140 | , mirrorAcross = Arc2d.mirrorAcross 141 | , numApproximationSegments = Arc2d.numApproximationSegments 142 | } 143 | 144 | 145 | genericTests : Test 146 | genericTests = 147 | Tests.Generic.Curve2d.tests 148 | curveOperations 149 | curveOperations 150 | Arc2d.placeIn 151 | Arc2d.relativeTo 152 | 153 | 154 | mirroredCenterPoint : Test 155 | mirroredCenterPoint = 156 | Test.check2 "Center point of a mirrored finite-radius arc is the mirror of the original center point" 157 | Random.arc2d 158 | Random.axis2d 159 | (\arc axis -> 160 | if Arc2d.radius arc |> Quantity.lessThan (meters 100) then 161 | let 162 | mirroredArc = 163 | Arc2d.mirrorAcross axis arc 164 | in 165 | Arc2d.centerPoint mirroredArc 166 | |> Expect.point2d 167 | (Point2d.mirrorAcross axis (Arc2d.centerPoint arc)) 168 | 169 | else 170 | Expect.pass 171 | ) 172 | -------------------------------------------------------------------------------- /src/QuadraticSpline1d.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module QuadraticSpline1d exposing 11 | ( QuadraticSpline1d 12 | , at 13 | , at_ 14 | , boundingBox 15 | , endDerivative 16 | , endPoint 17 | , firstControlPoint 18 | , firstDerivative 19 | , firstDerivativeBoundingBox 20 | , fromControlPoints 21 | , pointOn 22 | , secondControlPoint 23 | , secondDerivative 24 | , startDerivative 25 | , startPoint 26 | , thirdControlPoint 27 | ) 28 | 29 | import Geometry.Types as Types 30 | import Quantity exposing (Quantity(..), Rate) 31 | import Quantity.Interval as Interval exposing (Interval) 32 | 33 | 34 | type alias QuadraticSpline1d units = 35 | Types.QuadraticSpline1d units 36 | 37 | 38 | fromControlPoints : Quantity Float units -> Quantity Float units -> Quantity Float units -> QuadraticSpline1d units 39 | fromControlPoints p1 p2 p3 = 40 | Types.QuadraticSpline1d 41 | { firstControlPoint = p1 42 | , secondControlPoint = p2 43 | , thirdControlPoint = p3 44 | } 45 | 46 | 47 | at : Quantity Float (Rate units2 units1) -> QuadraticSpline1d units1 -> QuadraticSpline1d units2 48 | at rate (Types.QuadraticSpline1d spline) = 49 | Types.QuadraticSpline1d 50 | { firstControlPoint = Quantity.at rate spline.firstControlPoint 51 | , secondControlPoint = Quantity.at rate spline.secondControlPoint 52 | , thirdControlPoint = Quantity.at rate spline.thirdControlPoint 53 | } 54 | 55 | 56 | at_ : Quantity Float (Rate units1 units2) -> QuadraticSpline1d units1 -> QuadraticSpline1d units2 57 | at_ rate spline = 58 | at (Quantity.inverse rate) spline 59 | 60 | 61 | startPoint : QuadraticSpline1d units -> Quantity Float units 62 | startPoint (Types.QuadraticSpline1d spline) = 63 | spline.firstControlPoint 64 | 65 | 66 | endPoint : QuadraticSpline1d units -> Quantity Float units 67 | endPoint (Types.QuadraticSpline1d spline) = 68 | spline.thirdControlPoint 69 | 70 | 71 | firstControlPoint : QuadraticSpline1d units -> Quantity Float units 72 | firstControlPoint (Types.QuadraticSpline1d spline) = 73 | spline.firstControlPoint 74 | 75 | 76 | secondControlPoint : QuadraticSpline1d units -> Quantity Float units 77 | secondControlPoint (Types.QuadraticSpline1d spline) = 78 | spline.secondControlPoint 79 | 80 | 81 | thirdControlPoint : QuadraticSpline1d units -> Quantity Float units 82 | thirdControlPoint (Types.QuadraticSpline1d spline) = 83 | spline.thirdControlPoint 84 | 85 | 86 | startDerivative : QuadraticSpline1d units -> Quantity Float units 87 | startDerivative spline = 88 | Quantity.twice (secondControlPoint spline |> Quantity.minus (firstControlPoint spline)) 89 | 90 | 91 | endDerivative : QuadraticSpline1d units -> Quantity Float units 92 | endDerivative spline = 93 | Quantity.twice (thirdControlPoint spline |> Quantity.minus (secondControlPoint spline)) 94 | 95 | 96 | boundingBox : QuadraticSpline1d units -> Interval Float units 97 | boundingBox spline = 98 | Interval.hull3 99 | (firstControlPoint spline) 100 | (secondControlPoint spline) 101 | (thirdControlPoint spline) 102 | 103 | 104 | pointOn : QuadraticSpline1d units -> Float -> Quantity Float units 105 | pointOn spline parameterValue = 106 | let 107 | p1 = 108 | firstControlPoint spline 109 | 110 | p2 = 111 | secondControlPoint spline 112 | 113 | p3 = 114 | thirdControlPoint spline 115 | 116 | q1 = 117 | Quantity.interpolateFrom p1 p2 parameterValue 118 | 119 | q2 = 120 | Quantity.interpolateFrom p2 p3 parameterValue 121 | in 122 | Quantity.interpolateFrom q1 q2 parameterValue 123 | 124 | 125 | firstDerivative : QuadraticSpline1d units -> Float -> Quantity Float units 126 | firstDerivative spline parameterValue = 127 | let 128 | p1 = 129 | firstControlPoint spline 130 | 131 | p2 = 132 | secondControlPoint spline 133 | 134 | p3 = 135 | thirdControlPoint spline 136 | 137 | v1 = 138 | p2 |> Quantity.minus p1 139 | 140 | v2 = 141 | p3 |> Quantity.minus p2 142 | in 143 | Quantity.twice (Quantity.interpolateFrom v1 v2 parameterValue) 144 | 145 | 146 | {-| Get the bounds on the first derivative of a spline. 147 | -} 148 | firstDerivativeBoundingBox : QuadraticSpline1d units -> Interval Float units 149 | firstDerivativeBoundingBox spline = 150 | let 151 | p1 = 152 | firstControlPoint spline 153 | 154 | p2 = 155 | secondControlPoint spline 156 | 157 | p3 = 158 | thirdControlPoint spline 159 | 160 | v1 = 161 | Quantity.twice (p2 |> Quantity.minus p1) 162 | 163 | v2 = 164 | Quantity.twice (p3 |> Quantity.minus p2) 165 | in 166 | Interval.from v1 v2 167 | 168 | 169 | {-| Get the second derivative of a spline (for a quadratic spline, this is a 170 | constant). 171 | -} 172 | secondDerivative : QuadraticSpline1d units -> Quantity Float units 173 | secondDerivative spline = 174 | let 175 | p1 = 176 | firstControlPoint spline 177 | 178 | p2 = 179 | secondControlPoint spline 180 | 181 | p3 = 182 | thirdControlPoint spline 183 | 184 | v1 = 185 | p2 |> Quantity.minus p1 186 | 187 | v2 = 188 | p3 |> Quantity.minus p2 189 | in 190 | Quantity.twice (v2 |> Quantity.minus v1) 191 | -------------------------------------------------------------------------------- /tests/Tests/Cylinder3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Cylinder3d exposing (suite) 2 | 3 | import Angle exposing (Angle) 4 | import Axis3d exposing (Axis3d) 5 | import Circle3d exposing (Circle3d) 6 | import Cylinder3d exposing (Cylinder3d) 7 | import Expect 8 | import Frame3d exposing (Frame3d) 9 | import Geometry.Expect as Expect 10 | import Geometry.Random as Random 11 | import Length exposing (Meters) 12 | import Plane3d exposing (Plane3d) 13 | import Point3d exposing (Point3d) 14 | import Quantity exposing (Quantity) 15 | import Random exposing (Generator) 16 | import Random.Extra 17 | import Test exposing (Test) 18 | import Test.Random as Test 19 | import Vector3d exposing (Vector3d) 20 | 21 | 22 | type alias Transformation coordinates = 23 | { cylinder : Cylinder3d Meters coordinates -> Cylinder3d Meters coordinates 24 | , point : Point3d Meters coordinates -> Point3d Meters coordinates 25 | , circle : Circle3d Meters coordinates -> Circle3d Meters coordinates 26 | , isZeroScale : Bool 27 | } 28 | 29 | 30 | rotation : Axis3d Meters coordinates -> Angle -> Transformation coordinates 31 | rotation axis angle = 32 | { cylinder = Cylinder3d.rotateAround axis angle 33 | , point = Point3d.rotateAround axis angle 34 | , circle = Circle3d.rotateAround axis angle 35 | , isZeroScale = False 36 | } 37 | 38 | 39 | translation : Vector3d Meters coordinates -> Transformation coordinates 40 | translation displacement = 41 | { cylinder = Cylinder3d.translateBy displacement 42 | , point = Point3d.translateBy displacement 43 | , circle = Circle3d.translateBy displacement 44 | , isZeroScale = False 45 | } 46 | 47 | 48 | scaling : Point3d Meters coordinates -> Float -> Transformation coordinates 49 | scaling centerPoint scale = 50 | { cylinder = Cylinder3d.scaleAbout centerPoint scale 51 | , point = Point3d.scaleAbout centerPoint scale 52 | , circle = Circle3d.scaleAbout centerPoint scale 53 | , isZeroScale = scale == 0.0 54 | } 55 | 56 | 57 | mirroring : Plane3d Meters coordinates -> Transformation coordinates 58 | mirroring plane = 59 | { cylinder = Cylinder3d.mirrorAcross plane 60 | , point = Point3d.mirrorAcross plane 61 | , circle = Circle3d.mirrorAcross plane 62 | , isZeroScale = False 63 | } 64 | 65 | 66 | transformationGenerator : Generator (Transformation coordinates) 67 | transformationGenerator = 68 | Random.Extra.choices 69 | (Random.map2 rotation Random.axis3d Random.angle) 70 | [ Random.map translation Random.vector3d 71 | , Random.map2 scaling Random.point3d Random.scale 72 | , Random.map mirroring Random.plane3d 73 | ] 74 | 75 | 76 | cylinderAndPoint : Generator ( Cylinder3d Meters coordinates, Point3d Meters coordinates ) 77 | cylinderAndPoint = 78 | Random.map4 79 | (\cylinder u v theta -> 80 | let 81 | halfLength = 82 | Quantity.half (Cylinder3d.length cylinder) 83 | 84 | minZ = 85 | Quantity.multiplyBy -1.25 halfLength 86 | 87 | maxZ = 88 | Quantity.multiplyBy 1.25 halfLength 89 | 90 | radius = 91 | Cylinder3d.radius cylinder 92 | 93 | cylinderFrame = 94 | Frame3d.fromZAxis (Cylinder3d.axis cylinder) 95 | 96 | z = 97 | Quantity.interpolateFrom minZ maxZ u 98 | 99 | r = 100 | Quantity.sqrt (Quantity.multiplyBy (v * 1.25) (Quantity.squared radius)) 101 | 102 | x = 103 | r |> Quantity.multiplyBy (Angle.cos theta) 104 | 105 | y = 106 | r |> Quantity.multiplyBy (Angle.sin theta) 107 | in 108 | ( cylinder, Point3d.xyzIn cylinderFrame x y z ) 109 | ) 110 | Random.cylinder3d 111 | Random.parameterValue 112 | Random.parameterValue 113 | Random.angle 114 | 115 | 116 | suite : Test 117 | suite = 118 | Test.describe "Cylinder3d" 119 | [ Test.check2 "Point containment is consistent" 120 | cylinderAndPoint 121 | transformationGenerator 122 | (\( cylinder, point ) transformation -> 123 | let 124 | initialContainment = 125 | Cylinder3d.contains point cylinder 126 | 127 | transformedPoint = 128 | transformation.point point 129 | 130 | transformedCylinder = 131 | transformation.cylinder cylinder 132 | 133 | finalContainment = 134 | Cylinder3d.contains transformedPoint transformedCylinder 135 | in 136 | if transformation.isZeroScale then 137 | finalContainment |> Expect.equal True 138 | 139 | else 140 | finalContainment |> Expect.equal initialContainment 141 | ) 142 | , let 143 | testCap description accessor = 144 | Test.check2 description 145 | transformationGenerator 146 | Random.cylinder3d 147 | (\transformation cylinder -> 148 | let 149 | cap = 150 | accessor cylinder 151 | 152 | transformedCylinder = 153 | transformation.cylinder cylinder 154 | 155 | transformedCap = 156 | transformation.circle cap 157 | 158 | capOfTransformed = 159 | accessor transformedCylinder 160 | in 161 | capOfTransformed |> Expect.circle3d transformedCap 162 | ) 163 | in 164 | Test.describe "Caps are consistent through transformation" 165 | [ testCap "startCap" Cylinder3d.startCap 166 | , testCap "endCap" Cylinder3d.endCap 167 | ] 168 | ] 169 | -------------------------------------------------------------------------------- /tests/Tests/RationalCubicSpline3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.RationalCubicSpline3d exposing 2 | ( bSplines 3 | , genericTests 4 | , secondDerivativeBoundingBox 5 | , splitAt 6 | ) 7 | 8 | import CubicSpline3d 9 | import Expect exposing (Expectation, FloatingPointTolerance(..)) 10 | import Geometry.Expect as Expect 11 | import Geometry.Random as Random 12 | import Interval 13 | import Length exposing (Length, Meters) 14 | import Point3d exposing (Point3d) 15 | import Quantity 16 | import Random 17 | import RationalCubicSpline3d exposing (RationalCubicSpline3d) 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Tests.Generic.Curve3d 21 | import Vector3d 22 | 23 | 24 | curveOperations : Tests.Generic.Curve3d.Operations (RationalCubicSpline3d Meters coordinates) coordinates 25 | curveOperations = 26 | { generator = Random.rationalCubicSpline3d 27 | , pointOn = RationalCubicSpline3d.pointOn 28 | , boundingBox = RationalCubicSpline3d.boundingBox 29 | , firstDerivative = RationalCubicSpline3d.firstDerivative 30 | , firstDerivativeBoundingBox = RationalCubicSpline3d.firstDerivativeBoundingBox 31 | , scaleAbout = RationalCubicSpline3d.scaleAbout 32 | , translateBy = RationalCubicSpline3d.translateBy 33 | , rotateAround = RationalCubicSpline3d.rotateAround 34 | , mirrorAcross = RationalCubicSpline3d.mirrorAcross 35 | , numApproximationSegments = RationalCubicSpline3d.numApproximationSegments 36 | } 37 | 38 | 39 | genericTests : Test 40 | genericTests = 41 | Tests.Generic.Curve3d.tests 42 | curveOperations 43 | curveOperations 44 | RationalCubicSpline3d.placeIn 45 | RationalCubicSpline3d.relativeTo 46 | 47 | 48 | expectAll : List Expectation -> Expectation 49 | expectAll expectations = 50 | () |> Expect.all (List.map always expectations) 51 | 52 | 53 | forEach : (a -> Expectation) -> List a -> Expectation 54 | forEach toExpectation values = 55 | case values of 56 | [] -> 57 | Expect.pass 58 | 59 | _ -> 60 | () |> Expect.all (List.map (\value () -> toExpectation value) values) 61 | 62 | 63 | bSplines : Test 64 | bSplines = 65 | Test.test "B-splines are continuous" <| 66 | \() -> 67 | let 68 | knots = 69 | [ 0, 0, 0, 1, 2, 4, 4, 5, 8, 10, 10, 10 ] 70 | 71 | weightedControlPoints = 72 | [ ( Point3d.meters 0 0 1, 1 ) 73 | , ( Point3d.meters 1 8 2, 2 ) 74 | , ( Point3d.meters 4 4 3, 5 ) 75 | , ( Point3d.meters 2 4 2, 1 ) 76 | , ( Point3d.meters 4 1 1, 3 ) 77 | , ( Point3d.meters 8 2 2, 1 ) 78 | , ( Point3d.meters 5 6 3, 5 ) 79 | , ( Point3d.meters 8 9 2, 1 ) 80 | , ( Point3d.meters 9 7 1, 2 ) 81 | , ( Point3d.meters 9 4 2, 1 ) 82 | ] 83 | 84 | splines = 85 | RationalCubicSpline3d.bSplineSegments knots weightedControlPoints 86 | 87 | knotIntervals = 88 | RationalCubicSpline3d.bSplineIntervals knots 89 | 90 | segments = 91 | List.map2 Tuple.pair splines knotIntervals 92 | 93 | pairs = 94 | List.map2 Tuple.pair segments (List.drop 1 segments) 95 | in 96 | pairs 97 | |> forEach 98 | (\( ( firstSpline, firstInterval ), ( secondSpline, secondInterval ) ) -> 99 | let 100 | firstEndPoint = 101 | RationalCubicSpline3d.endPoint firstSpline 102 | 103 | secondStartPoint = 104 | RationalCubicSpline3d.startPoint secondSpline 105 | 106 | firstEndDerivative = 107 | RationalCubicSpline3d.endDerivative firstSpline 108 | |> Vector3d.scaleBy (1.0 / Interval.width firstInterval) 109 | 110 | secondStartDerivative = 111 | RationalCubicSpline3d.startDerivative secondSpline 112 | |> Vector3d.scaleBy (1.0 / Interval.width secondInterval) 113 | in 114 | expectAll 115 | [ firstEndPoint |> Expect.point3d secondStartPoint 116 | , firstEndDerivative |> Expect.vector3d secondStartDerivative 117 | ] 118 | ) 119 | 120 | 121 | splitAt : Test 122 | splitAt = 123 | Test.describe "splitAt" 124 | [ Test.check3 "first" 125 | Random.rationalCubicSpline3d 126 | Random.parameterValue 127 | Random.parameterValue 128 | (\spline t0 t1 -> 129 | let 130 | ( first, _ ) = 131 | RationalCubicSpline3d.splitAt t0 spline 132 | in 133 | RationalCubicSpline3d.pointOn first t1 134 | |> Expect.point3d 135 | (RationalCubicSpline3d.pointOn spline (t1 * t0)) 136 | ) 137 | , Test.check3 "second" 138 | Random.rationalCubicSpline3d 139 | Random.parameterValue 140 | Random.parameterValue 141 | (\spline t0 t1 -> 142 | let 143 | ( _, second ) = 144 | RationalCubicSpline3d.splitAt t0 spline 145 | in 146 | RationalCubicSpline3d.pointOn second t1 147 | |> Expect.point3d 148 | (RationalCubicSpline3d.pointOn spline (t0 + t1 * (1 - t0))) 149 | ) 150 | ] 151 | 152 | 153 | secondDerivativeBoundingBox : Test 154 | secondDerivativeBoundingBox = 155 | Tests.Generic.Curve3d.secondDerivativeBoundingBox 156 | { generator = Random.rationalCubicSpline3d 157 | , secondDerivative = RationalCubicSpline3d.secondDerivative 158 | , secondDerivativeBoundingBox = RationalCubicSpline3d.secondDerivativeBoundingBox 159 | } 160 | -------------------------------------------------------------------------------- /tests/Tests/RationalCubicSpline2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.RationalCubicSpline2d exposing 2 | ( bSplines 3 | , genericTests 4 | , secondDerivativeBoundingBox 5 | , splitAt 6 | ) 7 | 8 | import CubicSpline2d 9 | import Expect exposing (Expectation, FloatingPointTolerance(..)) 10 | import Geometry.Expect as Expect 11 | import Geometry.Random as Random 12 | import Interval 13 | import Length exposing (Length, Meters) 14 | import Point2d exposing (Point2d) 15 | import Quantity 16 | import Random 17 | import RationalCubicSpline2d exposing (RationalCubicSpline2d) 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Tests.Generic.Curve2d 21 | import Vector2d 22 | 23 | 24 | curveOperations : Tests.Generic.Curve2d.Operations (RationalCubicSpline2d Meters coordinates) coordinates 25 | curveOperations = 26 | { generator = Random.rationalCubicSpline2d 27 | , pointOn = RationalCubicSpline2d.pointOn 28 | , boundingBox = RationalCubicSpline2d.boundingBox 29 | , firstDerivative = RationalCubicSpline2d.firstDerivative 30 | , firstDerivativeBoundingBox = RationalCubicSpline2d.firstDerivativeBoundingBox 31 | , scaleAbout = RationalCubicSpline2d.scaleAbout 32 | , translateBy = RationalCubicSpline2d.translateBy 33 | , rotateAround = RationalCubicSpline2d.rotateAround 34 | , mirrorAcross = RationalCubicSpline2d.mirrorAcross 35 | , numApproximationSegments = RationalCubicSpline2d.numApproximationSegments 36 | } 37 | 38 | 39 | genericTests : Test 40 | genericTests = 41 | Tests.Generic.Curve2d.tests 42 | curveOperations 43 | curveOperations 44 | RationalCubicSpline2d.placeIn 45 | RationalCubicSpline2d.relativeTo 46 | 47 | 48 | expectAll : List Expectation -> Expectation 49 | expectAll expectations = 50 | () |> Expect.all (List.map always expectations) 51 | 52 | 53 | forEach : (a -> Expectation) -> List a -> Expectation 54 | forEach toExpectation values = 55 | case values of 56 | [] -> 57 | Expect.pass 58 | 59 | _ -> 60 | () |> Expect.all (List.map (\value () -> toExpectation value) values) 61 | 62 | 63 | bSplines : Test 64 | bSplines = 65 | Test.test "B-splines are continuous" <| 66 | \() -> 67 | let 68 | knots = 69 | [ 0, 0, 0, 1, 2, 4, 4, 5, 8, 10, 10, 10 ] 70 | 71 | weightedControlPoints = 72 | [ ( Point2d.meters 0 0, 1 ) 73 | , ( Point2d.meters 1 8, 2 ) 74 | , ( Point2d.meters 4 4, 5 ) 75 | , ( Point2d.meters 2 4, 1 ) 76 | , ( Point2d.meters 4 1, 3 ) 77 | , ( Point2d.meters 8 2, 1 ) 78 | , ( Point2d.meters 5 6, 5 ) 79 | , ( Point2d.meters 8 9, 1 ) 80 | , ( Point2d.meters 9 7, 2 ) 81 | , ( Point2d.meters 9 4, 1 ) 82 | ] 83 | 84 | splines = 85 | RationalCubicSpline2d.bSplineSegments knots weightedControlPoints 86 | 87 | knotIntervals = 88 | RationalCubicSpline2d.bSplineIntervals knots 89 | 90 | segments = 91 | List.map2 Tuple.pair splines knotIntervals 92 | 93 | pairs = 94 | List.map2 Tuple.pair segments (List.drop 1 segments) 95 | in 96 | pairs 97 | |> forEach 98 | (\( ( firstSpline, firstInterval ), ( secondSpline, secondInterval ) ) -> 99 | let 100 | firstEndPoint = 101 | RationalCubicSpline2d.endPoint firstSpline 102 | 103 | secondStartPoint = 104 | RationalCubicSpline2d.startPoint secondSpline 105 | 106 | firstEndDerivative = 107 | RationalCubicSpline2d.endDerivative firstSpline 108 | |> Vector2d.scaleBy (1.0 / Interval.width firstInterval) 109 | 110 | secondStartDerivative = 111 | RationalCubicSpline2d.startDerivative secondSpline 112 | |> Vector2d.scaleBy (1.0 / Interval.width secondInterval) 113 | in 114 | expectAll 115 | [ firstEndPoint |> Expect.point2d secondStartPoint 116 | , firstEndDerivative |> Expect.vector2d secondStartDerivative 117 | ] 118 | ) 119 | 120 | 121 | splitAt : Test 122 | splitAt = 123 | Test.describe "splitAt" 124 | [ Test.check3 125 | "first" 126 | Random.rationalCubicSpline2d 127 | Random.parameterValue 128 | Random.parameterValue 129 | (\spline t0 t1 -> 130 | let 131 | ( first, _ ) = 132 | RationalCubicSpline2d.splitAt t0 spline 133 | in 134 | RationalCubicSpline2d.pointOn first t1 135 | |> Expect.point2d 136 | (RationalCubicSpline2d.pointOn spline (t1 * t0)) 137 | ) 138 | , Test.check3 139 | "second" 140 | Random.rationalCubicSpline2d 141 | Random.parameterValue 142 | Random.parameterValue 143 | (\spline t0 t1 -> 144 | let 145 | ( _, second ) = 146 | RationalCubicSpline2d.splitAt t0 spline 147 | in 148 | RationalCubicSpline2d.pointOn second t1 149 | |> Expect.point2d 150 | (RationalCubicSpline2d.pointOn spline (t0 + t1 * (1 - t0))) 151 | ) 152 | ] 153 | 154 | 155 | secondDerivativeBoundingBox : Test 156 | secondDerivativeBoundingBox = 157 | Tests.Generic.Curve2d.secondDerivativeBoundingBox 158 | { generator = Random.rationalCubicSpline2d 159 | , secondDerivative = RationalCubicSpline2d.secondDerivative 160 | , secondDerivativeBoundingBox = RationalCubicSpline2d.secondDerivativeBoundingBox 161 | } 162 | -------------------------------------------------------------------------------- /tests/Tests/RationalQuadraticSpline3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.RationalQuadraticSpline3d exposing 2 | ( bSplines 3 | , genericTests 4 | , secondDerivativeBoundingBox 5 | , splitAt 6 | ) 7 | 8 | import Expect exposing (Expectation, FloatingPointTolerance(..)) 9 | import Geometry.Expect as Expect 10 | import Geometry.Random as Random 11 | import Interval 12 | import Length exposing (Meters) 13 | import Point3d 14 | import Quantity 15 | import Random 16 | import RationalQuadraticSpline3d exposing (RationalQuadraticSpline3d) 17 | import Test exposing (Test) 18 | import Test.Random as Test 19 | import Tests.Generic.Curve3d 20 | import Vector3d 21 | 22 | 23 | curveOperations : Tests.Generic.Curve3d.Operations (RationalQuadraticSpline3d Meters coordinates) coordinates 24 | curveOperations = 25 | { generator = Random.rationalQuadraticSpline3d 26 | , pointOn = RationalQuadraticSpline3d.pointOn 27 | , boundingBox = RationalQuadraticSpline3d.boundingBox 28 | , firstDerivative = RationalQuadraticSpline3d.firstDerivative 29 | , firstDerivativeBoundingBox = RationalQuadraticSpline3d.firstDerivativeBoundingBox 30 | , scaleAbout = RationalQuadraticSpline3d.scaleAbout 31 | , translateBy = RationalQuadraticSpline3d.translateBy 32 | , rotateAround = RationalQuadraticSpline3d.rotateAround 33 | , mirrorAcross = RationalQuadraticSpline3d.mirrorAcross 34 | , numApproximationSegments = RationalQuadraticSpline3d.numApproximationSegments 35 | } 36 | 37 | 38 | genericTests : Test 39 | genericTests = 40 | Tests.Generic.Curve3d.tests 41 | curveOperations 42 | curveOperations 43 | RationalQuadraticSpline3d.placeIn 44 | RationalQuadraticSpline3d.relativeTo 45 | 46 | 47 | expectAll : List Expectation -> Expectation 48 | expectAll expectations = 49 | () |> Expect.all (List.map always expectations) 50 | 51 | 52 | forEach : (a -> Expectation) -> List a -> Expectation 53 | forEach toExpectation values = 54 | case values of 55 | [] -> 56 | Expect.pass 57 | 58 | _ -> 59 | () |> Expect.all (List.map (\value () -> toExpectation value) values) 60 | 61 | 62 | bSplines : Test 63 | bSplines = 64 | Test.test "B-splines are continuous" <| 65 | \() -> 66 | let 67 | knots = 68 | [ 0, 0, 1, 2, 3, 4, 5, 8, 10, 10 ] 69 | 70 | weightedControlPoints = 71 | [ ( Point3d.meters 1 8 1, 2 ) 72 | , ( Point3d.meters 4 4 2, 5 ) 73 | , ( Point3d.meters 2 4 1, 1 ) 74 | , ( Point3d.meters 4 1 2, 3 ) 75 | , ( Point3d.meters 8 2 1, 1 ) 76 | , ( Point3d.meters 5 6 2, 5 ) 77 | , ( Point3d.meters 8 9 1, 1 ) 78 | , ( Point3d.meters 9 7 2, 2 ) 79 | , ( Point3d.meters 9 4 1, 1 ) 80 | ] 81 | 82 | splines = 83 | RationalQuadraticSpline3d.bSplineSegments knots weightedControlPoints 84 | 85 | knotIntervals = 86 | RationalQuadraticSpline3d.bSplineIntervals knots 87 | 88 | segments = 89 | List.map2 Tuple.pair splines knotIntervals 90 | 91 | pairs = 92 | List.map2 Tuple.pair segments (List.drop 1 segments) 93 | in 94 | pairs 95 | |> forEach 96 | (\( ( firstSpline, firstInterval ), ( secondSpline, secondInterval ) ) -> 97 | let 98 | firstEndPoint = 99 | RationalQuadraticSpline3d.endPoint firstSpline 100 | 101 | secondStartPoint = 102 | RationalQuadraticSpline3d.startPoint secondSpline 103 | 104 | firstEndDerivative = 105 | RationalQuadraticSpline3d.endDerivative firstSpline 106 | |> Vector3d.scaleBy (1.0 / Interval.width firstInterval) 107 | 108 | secondStartDerivative = 109 | RationalQuadraticSpline3d.startDerivative secondSpline 110 | |> Vector3d.scaleBy (1.0 / Interval.width secondInterval) 111 | in 112 | expectAll 113 | [ firstEndPoint |> Expect.point3d secondStartPoint 114 | , firstEndDerivative |> Expect.vector3d secondStartDerivative 115 | ] 116 | ) 117 | 118 | 119 | splitAt : Test 120 | splitAt = 121 | Test.describe "splitAt" 122 | [ Test.check3 "first" 123 | Random.rationalQuadraticSpline3d 124 | Random.parameterValue 125 | Random.parameterValue 126 | (\spline t0 t1 -> 127 | let 128 | ( first, _ ) = 129 | RationalQuadraticSpline3d.splitAt t0 spline 130 | in 131 | RationalQuadraticSpline3d.pointOn first t1 132 | |> Expect.point3d 133 | (RationalQuadraticSpline3d.pointOn spline (t1 * t0)) 134 | ) 135 | , Test.check3 "second" 136 | Random.rationalQuadraticSpline3d 137 | Random.parameterValue 138 | Random.parameterValue 139 | (\spline t0 t1 -> 140 | let 141 | ( _, second ) = 142 | RationalQuadraticSpline3d.splitAt t0 spline 143 | in 144 | RationalQuadraticSpline3d.pointOn second t1 145 | |> Expect.point3d 146 | (RationalQuadraticSpline3d.pointOn spline (t0 + t1 * (1 - t0))) 147 | ) 148 | ] 149 | 150 | 151 | secondDerivativeBoundingBox : Test 152 | secondDerivativeBoundingBox = 153 | Tests.Generic.Curve3d.secondDerivativeBoundingBox 154 | { generator = Random.rationalQuadraticSpline3d 155 | , secondDerivative = RationalQuadraticSpline3d.secondDerivative 156 | , secondDerivativeBoundingBox = RationalQuadraticSpline3d.secondDerivativeBoundingBox 157 | } 158 | -------------------------------------------------------------------------------- /tests/Tests/EllipticalArc2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.EllipticalArc2d exposing 2 | ( fromEndpointsReplicatesArc 3 | , genericTests 4 | , reproducibleArc 5 | , reverseKeepsMidpoint 6 | , signedDistanceAlong 7 | ) 8 | 9 | import Angle 10 | import Arc2d exposing (Arc2d) 11 | import EllipticalArc2d exposing (EllipticalArc2d) 12 | import Expect 13 | import Geometry.Expect as Expect 14 | import Geometry.Random as Random 15 | import Length exposing (Meters, meters) 16 | import Point2d 17 | import Quantity exposing (zero) 18 | import Random exposing (Generator) 19 | import Random.Extra 20 | import SweptAngle 21 | import Test exposing (Test) 22 | import Test.Random as Test 23 | import Tests.Generic.Curve2d 24 | import Vector2d 25 | 26 | 27 | reproducibleArc : Generator (Arc2d Meters coordinates) 28 | reproducibleArc = 29 | Random.map4 30 | (\centerPoint startDirection radius sweptAngle -> 31 | let 32 | startPoint = 33 | centerPoint |> Point2d.translateIn startDirection radius 34 | in 35 | startPoint |> Arc2d.sweptAround centerPoint sweptAngle 36 | ) 37 | Random.point2d 38 | Random.direction2d 39 | (Random.map meters (Random.float 0.1 10)) 40 | (Random.map Angle.degrees <| 41 | Random.Extra.choices 42 | (Random.float 1 179) 43 | [ Random.float 181 359 44 | , Random.float -179 -1 45 | , Random.float -359 -181 46 | ] 47 | ) 48 | 49 | 50 | fromEndpointsReplicatesArc : Test 51 | fromEndpointsReplicatesArc = 52 | Test.check2 "fromEndpoints accurately replicates circular arcs" 53 | reproducibleArc 54 | Random.direction2d 55 | (\arc xDirection -> 56 | let 57 | radius = 58 | Arc2d.radius arc 59 | 60 | arcSweptAngle = 61 | Arc2d.sweptAngle arc 62 | 63 | sweptAngle = 64 | if arcSweptAngle |> Quantity.greaterThanOrEqualTo (Angle.radians pi) then 65 | SweptAngle.largePositive 66 | 67 | else if arcSweptAngle |> Quantity.greaterThanOrEqualTo zero then 68 | SweptAngle.smallPositive 69 | 70 | else if arcSweptAngle |> Quantity.greaterThanOrEqualTo (Angle.radians -pi) then 71 | SweptAngle.smallNegative 72 | 73 | else 74 | SweptAngle.largeNegative 75 | 76 | result = 77 | EllipticalArc2d.fromEndpoints 78 | { startPoint = Arc2d.startPoint arc 79 | , endPoint = Arc2d.endPoint arc 80 | , xRadius = radius 81 | , yRadius = radius 82 | , xDirection = xDirection 83 | , sweptAngle = sweptAngle 84 | } 85 | in 86 | case result of 87 | Nothing -> 88 | Expect.fail "fromEndpoints could not reproduce arc" 89 | 90 | Just ellipticalArc -> 91 | EllipticalArc2d.centerPoint ellipticalArc 92 | |> Expect.point2d (Arc2d.centerPoint arc) 93 | ) 94 | 95 | 96 | reverseKeepsMidpoint : Test 97 | reverseKeepsMidpoint = 98 | Test.check "Reversing an elliptical arc keeps the midpoint" 99 | Random.ellipticalArc2d 100 | (\arc -> 101 | case 102 | ( EllipticalArc2d.nondegenerate arc 103 | , EllipticalArc2d.nondegenerate (EllipticalArc2d.reverse arc) 104 | ) 105 | of 106 | ( Ok nondegenerateArc, Ok nondegenerateReversedArc ) -> 107 | let 108 | parametrizedArc = 109 | nondegenerateArc 110 | |> EllipticalArc2d.arcLengthParameterized 111 | { maxError = meters 1.0e-3 } 112 | 113 | parametrizedReversedArc = 114 | nondegenerateReversedArc 115 | |> EllipticalArc2d.arcLengthParameterized 116 | { maxError = meters 1.0e-3 } 117 | in 118 | EllipticalArc2d.midpoint parametrizedArc 119 | |> Expect.point2dWithin (meters 1.0e-3) 120 | (EllipticalArc2d.midpoint parametrizedReversedArc) 121 | 122 | _ -> 123 | Expect.pass 124 | ) 125 | 126 | 127 | curveOperations : Tests.Generic.Curve2d.Operations (EllipticalArc2d Meters coordinates) coordinates 128 | curveOperations = 129 | { generator = Random.ellipticalArc2d 130 | , pointOn = EllipticalArc2d.pointOn 131 | , boundingBox = EllipticalArc2d.boundingBox 132 | , firstDerivative = EllipticalArc2d.firstDerivative 133 | , firstDerivativeBoundingBox = EllipticalArc2d.firstDerivativeBoundingBox 134 | , scaleAbout = EllipticalArc2d.scaleAbout 135 | , translateBy = EllipticalArc2d.translateBy 136 | , rotateAround = EllipticalArc2d.rotateAround 137 | , mirrorAcross = EllipticalArc2d.mirrorAcross 138 | , numApproximationSegments = EllipticalArc2d.numApproximationSegments 139 | } 140 | 141 | 142 | genericTests : Test 143 | genericTests = 144 | Tests.Generic.Curve2d.tests 145 | curveOperations 146 | curveOperations 147 | EllipticalArc2d.placeIn 148 | EllipticalArc2d.relativeTo 149 | 150 | 151 | signedDistanceAlong : Test 152 | signedDistanceAlong = 153 | Test.check3 "signedDistanceAlong" 154 | Random.ellipticalArc2d 155 | Random.axis2d 156 | Random.parameterValue 157 | (\arc axis parameterValue -> 158 | let 159 | distanceInterval = 160 | EllipticalArc2d.signedDistanceAlong axis arc 161 | 162 | projectedDistance = 163 | Point2d.signedDistanceAlong axis 164 | (EllipticalArc2d.pointOn arc parameterValue) 165 | in 166 | projectedDistance |> Expect.quantityContainedIn distanceInterval 167 | ) 168 | -------------------------------------------------------------------------------- /tests/Tests/RationalQuadraticSpline2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.RationalQuadraticSpline2d exposing 2 | ( bSplines 3 | , genericTests 4 | , secondDerivativeBoundingBox 5 | , splitAt 6 | ) 7 | 8 | import CubicSpline2d 9 | import Expect exposing (Expectation, FloatingPointTolerance(..)) 10 | import Geometry.Expect as Expect 11 | import Geometry.Random as Random 12 | import Interval 13 | import Length exposing (Length, Meters) 14 | import Point2d exposing (Point2d) 15 | import Quantity 16 | import Random 17 | import RationalQuadraticSpline2d exposing (RationalQuadraticSpline2d) 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Tests.Generic.Curve2d 21 | import Vector2d 22 | 23 | 24 | curveOperations : Tests.Generic.Curve2d.Operations (RationalQuadraticSpline2d Meters coordinates) coordinates 25 | curveOperations = 26 | { generator = Random.rationalQuadraticSpline2d 27 | , pointOn = RationalQuadraticSpline2d.pointOn 28 | , boundingBox = RationalQuadraticSpline2d.boundingBox 29 | , firstDerivative = RationalQuadraticSpline2d.firstDerivative 30 | , firstDerivativeBoundingBox = RationalQuadraticSpline2d.firstDerivativeBoundingBox 31 | , scaleAbout = RationalQuadraticSpline2d.scaleAbout 32 | , translateBy = RationalQuadraticSpline2d.translateBy 33 | , rotateAround = RationalQuadraticSpline2d.rotateAround 34 | , mirrorAcross = RationalQuadraticSpline2d.mirrorAcross 35 | , numApproximationSegments = RationalQuadraticSpline2d.numApproximationSegments 36 | } 37 | 38 | 39 | genericTests : Test 40 | genericTests = 41 | Tests.Generic.Curve2d.tests 42 | curveOperations 43 | curveOperations 44 | RationalQuadraticSpline2d.placeIn 45 | RationalQuadraticSpline2d.relativeTo 46 | 47 | 48 | expectAll : List Expectation -> Expectation 49 | expectAll expectations = 50 | () |> Expect.all (List.map always expectations) 51 | 52 | 53 | forEach : (a -> Expectation) -> List a -> Expectation 54 | forEach toExpectation values = 55 | case values of 56 | [] -> 57 | Expect.pass 58 | 59 | _ -> 60 | () |> Expect.all (List.map (\value () -> toExpectation value) values) 61 | 62 | 63 | bSplines : Test 64 | bSplines = 65 | Test.test "B-splines are continuous" <| 66 | \() -> 67 | let 68 | knots = 69 | [ 0, 0, 1, 2, 3, 4, 5, 8, 10, 10 ] 70 | 71 | weightedControlPoints = 72 | [ ( Point2d.meters 1 8, 2 ) 73 | , ( Point2d.meters 4 4, 5 ) 74 | , ( Point2d.meters 2 4, 1 ) 75 | , ( Point2d.meters 4 1, 3 ) 76 | , ( Point2d.meters 8 2, 1 ) 77 | , ( Point2d.meters 5 6, 5 ) 78 | , ( Point2d.meters 8 9, 1 ) 79 | , ( Point2d.meters 9 7, 2 ) 80 | , ( Point2d.meters 9 4, 1 ) 81 | ] 82 | 83 | splines = 84 | RationalQuadraticSpline2d.bSplineSegments knots weightedControlPoints 85 | 86 | knotIntervals = 87 | RationalQuadraticSpline2d.bSplineIntervals knots 88 | 89 | segments = 90 | List.map2 Tuple.pair splines knotIntervals 91 | 92 | pairs = 93 | List.map2 Tuple.pair segments (List.drop 1 segments) 94 | in 95 | pairs 96 | |> forEach 97 | (\( ( firstSpline, firstInterval ), ( secondSpline, secondInterval ) ) -> 98 | let 99 | firstEndPoint = 100 | RationalQuadraticSpline2d.endPoint firstSpline 101 | 102 | secondStartPoint = 103 | RationalQuadraticSpline2d.startPoint secondSpline 104 | 105 | firstEndDerivative = 106 | RationalQuadraticSpline2d.endDerivative firstSpline 107 | |> Vector2d.scaleBy (1.0 / Interval.width firstInterval) 108 | 109 | secondStartDerivative = 110 | RationalQuadraticSpline2d.startDerivative secondSpline 111 | |> Vector2d.scaleBy (1.0 / Interval.width secondInterval) 112 | in 113 | expectAll 114 | [ firstEndPoint |> Expect.point2d secondStartPoint 115 | , firstEndDerivative |> Expect.vector2d secondStartDerivative 116 | ] 117 | ) 118 | 119 | 120 | splitAt : Test 121 | splitAt = 122 | Test.describe "splitAt" 123 | [ Test.check3 "first" 124 | Random.rationalQuadraticSpline2d 125 | Random.parameterValue 126 | Random.parameterValue 127 | (\spline t0 t1 -> 128 | let 129 | ( first, _ ) = 130 | RationalQuadraticSpline2d.splitAt t0 spline 131 | in 132 | RationalQuadraticSpline2d.pointOn first t1 133 | |> Expect.point2d 134 | (RationalQuadraticSpline2d.pointOn spline (t1 * t0)) 135 | ) 136 | , Test.check3 "second" 137 | Random.rationalQuadraticSpline2d 138 | Random.parameterValue 139 | Random.parameterValue 140 | (\spline t0 t1 -> 141 | let 142 | ( _, second ) = 143 | RationalQuadraticSpline2d.splitAt t0 spline 144 | in 145 | RationalQuadraticSpline2d.pointOn second t1 146 | |> Expect.point2d 147 | (RationalQuadraticSpline2d.pointOn spline (t0 + t1 * (1 - t0))) 148 | ) 149 | ] 150 | 151 | 152 | secondDerivativeBoundingBox : Test 153 | secondDerivativeBoundingBox = 154 | Tests.Generic.Curve2d.secondDerivativeBoundingBox 155 | { generator = Random.rationalQuadraticSpline2d 156 | , secondDerivative = RationalQuadraticSpline2d.secondDerivative 157 | , secondDerivativeBoundingBox = RationalQuadraticSpline2d.secondDerivativeBoundingBox 158 | } 159 | -------------------------------------------------------------------------------- /tests/Tests/VoronoiDiagram2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.VoronoiDiagram2d exposing 2 | ( cellForEveryInputVertex 3 | , failsOnCoincidentVertices 4 | ) 5 | 6 | import Array exposing (Array) 7 | import BoundingBox2d 8 | import DelaunayTriangulation2d 9 | import Direction3d 10 | import Expect 11 | import Geometry.Expect as Expect 12 | import Geometry.Random as Random 13 | import Length exposing (Meters, inMeters, meters) 14 | import List.Extra 15 | import Plane3d 16 | import Point2d exposing (Point2d) 17 | import Point3d 18 | import Polygon2d exposing (Polygon2d) 19 | import Quantity 20 | import Random exposing (Generator) 21 | import SketchPlane3d 22 | import Test exposing (Test) 23 | import Test.Random as Test 24 | import Vector3d 25 | import VoronoiDiagram2d 26 | 27 | 28 | uniquePoints : Generator (Array (Point2d Meters coordinates)) 29 | uniquePoints = 30 | Random.smallList Random.point2d 31 | |> Random.map (List.Extra.uniqueBy (Point2d.toTuple inMeters)) 32 | |> Random.map Array.fromList 33 | 34 | 35 | cellForEveryInputVertex : Test 36 | cellForEveryInputVertex = 37 | let 38 | description = 39 | "A Voronoi diagram should include a cell for every input vertex (as long as the clipping bounding box contains all input vertices)" 40 | 41 | expectation points = 42 | case VoronoiDiagram2d.fromPoints points of 43 | Err _ -> 44 | Expect.pass 45 | 46 | Ok diagram -> 47 | case BoundingBox2d.hullN (Array.toList points) of 48 | Nothing -> 49 | let 50 | boundingBox = 51 | BoundingBox2d.fromExtrema 52 | { minX = meters 0 53 | , minY = meters 0 54 | , maxX = meters 100 55 | , maxY = meters 100 56 | } 57 | in 58 | VoronoiDiagram2d.polygons boundingBox diagram 59 | |> Expect.equal [] 60 | 61 | Just boundingBox -> 62 | diagram 63 | |> VoronoiDiagram2d.polygons (BoundingBox2d.expandBy (Length.centimeters 1) boundingBox) 64 | |> List.length 65 | |> Expect.equal (Array.length points) 66 | in 67 | Test.check description uniquePoints expectation 68 | 69 | 70 | failsOnCoincidentVertices : Test 71 | failsOnCoincidentVertices = 72 | let 73 | description = 74 | "Voronoi diagram construction should fail when coincident vertices are given" 75 | 76 | expectation points = 77 | case points of 78 | [] -> 79 | Expect.pass 80 | 81 | x :: xs -> 82 | let 83 | pointsWithDuplicate = 84 | Array.fromList (x :: x :: xs) 85 | in 86 | VoronoiDiagram2d.fromPoints pointsWithDuplicate 87 | |> Expect.err 88 | in 89 | -- use normal `Random.smallList`, more duplicates don't matter here 90 | Test.check description (Random.smallList Random.point2d) expectation 91 | 92 | 93 | 94 | -- pointInPolygon : Polygon2d Meters coordinates -> Fuzzer (Point2d Meters coordinates) 95 | -- pointInPolygon polygon = 96 | -- let 97 | -- vertices = 98 | -- Polygon2d.vertices polygon 99 | -- numVertices = 100 | -- List.length vertices 101 | -- in 102 | -- case vertices of 103 | -- [] -> 104 | -- Fuzz.invalid "Can't generate a point inside an empty polygon" 105 | -- first :: rest -> 106 | -- let 107 | -- -- Ensuring every vertex has a positive weight avoids a divide 108 | -- -- by zero and ensures that the result is strictly inside the 109 | -- -- polygon 110 | -- weightsGenerator = 111 | -- Fuzz.list numVertices (Fuzz.float 1 100) 112 | -- in 113 | -- weightsGenerator 114 | -- |> Fuzz.map 115 | -- (\weights -> 116 | -- let 117 | -- totalWeight = 118 | -- List.sum weights 119 | -- weightedXCoordinates = 120 | -- List.map2 121 | -- (\weight vertex -> 122 | -- Quantity.multiplyBy weight 123 | -- (Point2d.xCoordinate vertex) 124 | -- ) 125 | -- weights 126 | -- vertices 127 | -- weightedYCoordinates = 128 | -- List.map2 129 | -- (\weight vertex -> 130 | -- Quantity.multiplyBy weight 131 | -- (Point2d.yCoordinate vertex) 132 | -- ) 133 | -- weights 134 | -- vertices 135 | -- x = 136 | -- Quantity.sum weightedXCoordinates 137 | -- |> Quantity.divideBy totalWeight 138 | -- y = 139 | -- Quantity.sum weightedYCoordinates 140 | -- |> Quantity.divideBy totalWeight 141 | -- in 142 | -- Point2d.xy x y 143 | -- ) 144 | --pointInPolygonClosestToCorrespondingVertex : Test 145 | --pointInPolygonClosestToCorrespondingVertex = 146 | -- let 147 | -- description = 148 | -- "Every point inside a Voronoi region must be closer to the corresponding vertex than any other vertex" 149 | -- expectation points = 150 | -- Expect.fail "TODO" 151 | -- in 152 | -- Test.check1 uniquePoints description expectation 153 | -------------------------------------------------------------------------------- /tests/Tests/Vector2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Vector2d exposing 2 | ( components 3 | , dotProductWithSelfIsSquaredLength 4 | , mirrorAcrossNegatesPerpendicularComponent 5 | , mirrorAcrossPreservesParallelComponent 6 | , perpendicularVectorIsPerpendicular 7 | , rotateByPreservesLength 8 | , rotateByRotatesByTheCorrectAngle 9 | , sum 10 | , vectorScaling 11 | ) 12 | 13 | import Axis2d 14 | import Direction2d 15 | import Expect 16 | import Geometry.Expect as Expect 17 | import Geometry.Random as Random 18 | import Quantity 19 | import Random 20 | import Test exposing (Test) 21 | import Test.Random as Test 22 | import Vector2d 23 | 24 | 25 | perpendicularVectorIsPerpendicular : Test 26 | perpendicularVectorIsPerpendicular = 27 | Test.check "perpendicularTo actually returns a perpendicular vector" 28 | Random.vector2d 29 | (\vector -> 30 | vector 31 | |> Vector2d.perpendicularTo 32 | |> Vector2d.dot vector 33 | |> Expect.quantity Quantity.zero 34 | ) 35 | 36 | 37 | dotProductWithSelfIsSquaredLength : Test 38 | dotProductWithSelfIsSquaredLength = 39 | Test.check "Dot product of a vector with itself is its squared length" 40 | Random.vector2d 41 | (\vector -> 42 | (vector |> Vector2d.dot vector) 43 | |> Expect.quantity 44 | (Quantity.squared (Vector2d.length vector)) 45 | ) 46 | 47 | 48 | rotateByPreservesLength : Test 49 | rotateByPreservesLength = 50 | Test.check2 "Rotating a vector preserves its length" 51 | Random.vector2d 52 | Random.angle 53 | (\vector angle -> 54 | Vector2d.rotateBy angle vector 55 | |> Vector2d.length 56 | |> Expect.quantity (Vector2d.length vector) 57 | ) 58 | 59 | 60 | rotateByRotatesByTheCorrectAngle : Test 61 | rotateByRotatesByTheCorrectAngle = 62 | Test.check2 "Rotating a vector rotates by the correct angle" 63 | Random.vector2d 64 | Random.angle 65 | (\vector angle -> 66 | let 67 | rotatedVector = 68 | Vector2d.rotateBy angle vector 69 | in 70 | if vector == Vector2d.zero then 71 | rotatedVector |> Expect.vector2d vector 72 | 73 | else 74 | let 75 | direction = 76 | Vector2d.direction vector 77 | 78 | rotatedDirection = 79 | Vector2d.direction (Vector2d.rotateBy angle vector) 80 | 81 | measuredAngle = 82 | Maybe.map2 Direction2d.angleFrom direction rotatedDirection 83 | |> Maybe.withDefault Quantity.zero 84 | in 85 | Expect.angle angle measuredAngle 86 | ) 87 | 88 | 89 | mirrorAcrossPreservesParallelComponent : Test 90 | mirrorAcrossPreservesParallelComponent = 91 | Test.check2 "Mirroring a vector across an axis preserves component parallel to the axis" 92 | Random.vector2d 93 | Random.axis2d 94 | (\vector axis -> 95 | let 96 | parallelComponent = 97 | Vector2d.componentIn (Axis2d.direction axis) 98 | in 99 | vector 100 | |> Vector2d.mirrorAcross axis 101 | |> parallelComponent 102 | |> Expect.quantity (parallelComponent vector) 103 | ) 104 | 105 | 106 | mirrorAcrossNegatesPerpendicularComponent : Test 107 | mirrorAcrossNegatesPerpendicularComponent = 108 | Test.check2 "Mirroring a vector across an axis negates component perpendicular to the axis" 109 | Random.vector2d 110 | Random.axis2d 111 | (\vector axis -> 112 | let 113 | perpendicularDirection = 114 | Direction2d.perpendicularTo (Axis2d.direction axis) 115 | 116 | perpendicularComponent = 117 | Vector2d.componentIn perpendicularDirection 118 | in 119 | vector 120 | |> Vector2d.mirrorAcross axis 121 | |> perpendicularComponent 122 | |> Expect.quantity 123 | (Quantity.negate (perpendicularComponent vector)) 124 | ) 125 | 126 | 127 | components : Test 128 | components = 129 | Test.check "components and xComponent/yComponent are consistent" Random.vector2d <| 130 | \vector -> 131 | Expect.all 132 | [ Tuple.first >> Expect.quantity (Vector2d.xComponent vector) 133 | , Tuple.second >> Expect.quantity (Vector2d.yComponent vector) 134 | ] 135 | (Vector2d.components vector) 136 | 137 | 138 | sum : Test 139 | sum = 140 | Test.check "sum is consistent with plus" (Random.smallList Random.vector2d) <| 141 | \vectors -> 142 | Vector2d.sum vectors 143 | |> Expect.vector2d 144 | (List.foldl Vector2d.plus Vector2d.zero vectors) 145 | 146 | 147 | vectorScaling : Test 148 | vectorScaling = 149 | Test.describe "scaling vectors" 150 | [ Test.check "scaling a zero vector results in a zero vector" Random.length <| 151 | \len -> 152 | Expect.equal Vector2d.zero (Vector2d.scaleTo len Vector2d.zero) 153 | , Test.check "scaleTo has a consistent length" (Random.pair Random.length Random.vector2d) <| 154 | \( scale, vector ) -> 155 | if vector == Vector2d.zero then 156 | Vector2d.scaleTo scale vector 157 | |> Expect.equal Vector2d.zero 158 | 159 | else 160 | Vector2d.scaleTo scale vector 161 | |> Vector2d.length 162 | |> Expect.quantity (Quantity.abs scale) 163 | , Test.check "normalize has a consistent length" Random.vector2d <| 164 | \vector -> 165 | if vector == Vector2d.zero then 166 | Vector2d.normalize vector 167 | |> Expect.equal Vector2d.zero 168 | 169 | else 170 | Vector2d.normalize vector 171 | |> Vector2d.length 172 | |> Expect.quantity (Quantity.float 1) 173 | ] 174 | -------------------------------------------------------------------------------- /sandbox/src/DelaunayTriangulation.elm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- 3 | -- This Source Code Form is subject to the terms of the Mozilla Public -- 4 | -- License, v. 2.0. If a copy of the MPL was not distributed with this file, -- 5 | -- you can obtain one at http://mozilla.org/MPL/2.0/. -- 6 | -------------------------------------------------------------------------------- 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | module DelaunayTriangulation exposing (main) 11 | 12 | import Array 13 | import Axis2d 14 | import BoundingBox2d exposing (BoundingBox2d) 15 | import Browser 16 | import Circle2d 17 | import DelaunayTriangulation2d exposing (DelaunayTriangulation2d, Error(..)) 18 | import Geometry.Svg as Svg 19 | import Html exposing (Html) 20 | import Html.Attributes 21 | import Html.Events 22 | import Html.Events.Extra.Mouse as Mouse 23 | import LineSegment2d 24 | import Pixels exposing (Pixels) 25 | import Point2d exposing (Point2d) 26 | import Polygon2d exposing (Polygon2d) 27 | import Polyline2d exposing (Polyline2d) 28 | import Quantity.Interval as Interval 29 | import Random exposing (Generator) 30 | import Svg exposing (Svg) 31 | import Svg.Attributes 32 | import Triangle2d exposing (Triangle2d) 33 | import TriangularMesh exposing (TriangularMesh) 34 | 35 | 36 | type ScreenCoordinates 37 | = ScreenCoordinates 38 | 39 | 40 | type alias Point = 41 | Point2d Pixels ScreenCoordinates 42 | 43 | 44 | type alias Model = 45 | { baseTriangulation : Result (Error Point) (DelaunayTriangulation2d Point Pixels ScreenCoordinates) 46 | , mousePosition : Maybe Point 47 | } 48 | 49 | 50 | type Msg 51 | = Click 52 | | NewRandomPoints (List Point) 53 | | MouseMove Mouse.Event 54 | 55 | 56 | svgDimensions : ( Float, Float ) 57 | svgDimensions = 58 | ( 700, 700 ) 59 | 60 | 61 | renderBounds : BoundingBox2d Pixels ScreenCoordinates 62 | renderBounds = 63 | BoundingBox2d.from Point2d.origin (Point2d.pixels 700 700) 64 | 65 | 66 | pointsGenerator : Generator (List (Point2d Pixels ScreenCoordinates)) 67 | pointsGenerator = 68 | let 69 | ( xInterval, yInterval ) = 70 | BoundingBox2d.intervals renderBounds 71 | 72 | parameterGenerator = 73 | Random.float 0.05 0.95 74 | 75 | pointGenerator = 76 | Random.map2 Point2d.xy 77 | (Random.map (Interval.interpolate xInterval) parameterGenerator) 78 | (Random.map (Interval.interpolate yInterval) parameterGenerator) 79 | in 80 | Random.int 50 500 81 | |> Random.andThen 82 | (\listSize -> Random.list listSize pointGenerator) 83 | 84 | 85 | generateNewPoints : Cmd Msg 86 | generateNewPoints = 87 | Random.generate NewRandomPoints pointsGenerator 88 | 89 | 90 | init : ( Model, Cmd Msg ) 91 | init = 92 | ( { baseTriangulation = Ok DelaunayTriangulation2d.empty 93 | , mousePosition = Nothing 94 | } 95 | , generateNewPoints 96 | ) 97 | 98 | 99 | update : Msg -> Model -> ( Model, Cmd Msg ) 100 | update message model = 101 | case message of 102 | Click -> 103 | ( model, generateNewPoints ) 104 | 105 | NewRandomPoints points -> 106 | let 107 | triangulation = 108 | DelaunayTriangulation2d.fromPoints (Array.fromList points) 109 | in 110 | ( { model | baseTriangulation = triangulation }, Cmd.none ) 111 | 112 | MouseMove event -> 113 | let 114 | point = 115 | Point2d.fromTuple Pixels.float event.offsetPos 116 | in 117 | ( { model | mousePosition = Just point }, Cmd.none ) 118 | 119 | 120 | view : Model -> Browser.Document Msg 121 | view model = 122 | let 123 | triangulation = 124 | case model.mousePosition of 125 | Just point -> 126 | model.baseTriangulation 127 | |> Result.andThen 128 | (DelaunayTriangulation2d.insertPoint point) 129 | 130 | Nothing -> 131 | model.baseTriangulation 132 | 133 | triangles = 134 | triangulation 135 | |> Result.map DelaunayTriangulation2d.triangles 136 | |> Result.withDefault [] 137 | 138 | points = 139 | triangulation 140 | |> Result.map (DelaunayTriangulation2d.vertices >> Array.toList) 141 | |> Result.withDefault [] 142 | 143 | mousePointElement = 144 | case model.mousePosition of 145 | Just point -> 146 | Svg.circle2d [ Svg.Attributes.fill "blue" ] (Circle2d.withRadius (Pixels.float 2.5) point) 147 | 148 | Nothing -> 149 | Svg.text "" 150 | 151 | ( width, height ) = 152 | svgDimensions 153 | 154 | overlayPolygon = 155 | Polygon2d.singleLoop 156 | [ Point2d.pixels 0 0 157 | , Point2d.pixels width 0 158 | , Point2d.pixels width height 159 | , Point2d.pixels 0 height 160 | ] 161 | in 162 | { title = "Delaunay Triangulation" 163 | , body = 164 | [ Html.div [ Html.Events.onClick Click ] 165 | [ Svg.svg 166 | [ Svg.Attributes.width (String.fromFloat width) 167 | , Svg.Attributes.height (String.fromFloat height) 168 | , Svg.Attributes.fill "white" 169 | , Svg.Attributes.stroke "black" 170 | , Html.Attributes.style "border" "1px solid black" 171 | , Mouse.onMove MouseMove 172 | ] 173 | [ Svg.g [] (triangles |> List.map (Svg.triangle2d [])) 174 | , Svg.g [] (points |> List.map (Circle2d.withRadius (Pixels.float 2.5) >> Svg.circle2d [])) 175 | , mousePointElement 176 | , Svg.polygon2d [ Svg.Attributes.fill "transparent" ] overlayPolygon 177 | ] 178 | ] 179 | ] 180 | } 181 | 182 | 183 | main : Program () Model Msg 184 | main = 185 | Browser.document 186 | { init = always init 187 | , update = update 188 | , view = view 189 | , subscriptions = always Sub.none 190 | } 191 | -------------------------------------------------------------------------------- /tests/Tests/CubicSpline3d.elm: -------------------------------------------------------------------------------- 1 | module Tests.CubicSpline3d exposing 2 | ( arcLengthMatchesAnalytical 3 | , bSplineReproducesSpline 4 | , fromEndpointsReproducesSpline 5 | , genericTests 6 | , pointAtArcLengthIsEnd 7 | , pointAtZeroLengthIsStart 8 | , secondDerivativeBoundingBox 9 | ) 10 | 11 | import CubicSpline3d exposing (CubicSpline3d) 12 | import Expect 13 | import Geometry.Expect as Expect 14 | import Geometry.Random as Random 15 | import Geometry.Types exposing (CubicSpline3d) 16 | import Length exposing (Meters, meters) 17 | import Quantity exposing (zero) 18 | import Test exposing (Test) 19 | import Test.Random as Test 20 | import Tests.Generic.Curve3d 21 | import Tests.QuadraticSpline3d 22 | 23 | 24 | fromEndpointsReproducesSpline : Test 25 | fromEndpointsReproducesSpline = 26 | Test.check "CubicSpline3d.fromEndpoints reproduces original spline" 27 | Random.cubicSpline3d 28 | (\spline -> 29 | let 30 | startPoint = 31 | CubicSpline3d.startPoint spline 32 | 33 | endPoint = 34 | CubicSpline3d.endPoint spline 35 | 36 | startDerivative = 37 | CubicSpline3d.startDerivative spline 38 | 39 | endDerivative = 40 | CubicSpline3d.endDerivative spline 41 | in 42 | CubicSpline3d.fromEndpoints startPoint startDerivative endPoint endDerivative 43 | |> Expect.cubicSpline3d spline 44 | ) 45 | 46 | 47 | arcLengthMatchesAnalytical : Test 48 | arcLengthMatchesAnalytical = 49 | Test.check "arc length matches analytical formula" 50 | Tests.QuadraticSpline3d.curvedSpline 51 | (\quadraticSpline -> 52 | quadraticSpline 53 | |> CubicSpline3d.fromQuadraticSpline 54 | |> CubicSpline3d.nondegenerate 55 | |> Result.map 56 | (CubicSpline3d.arcLengthParameterized 57 | { maxError = meters 0.001 } 58 | >> CubicSpline3d.arcLength 59 | ) 60 | |> Result.withDefault zero 61 | |> Expect.quantityWithin (meters 0.001) 62 | (Tests.QuadraticSpline3d.analyticalLength quadraticSpline) 63 | ) 64 | 65 | 66 | pointAtZeroLengthIsStart : Test 67 | pointAtZeroLengthIsStart = 68 | Test.check "point along spline at zero length is start point" 69 | Random.cubicSpline3d 70 | (\spline -> 71 | case CubicSpline3d.nondegenerate spline of 72 | Ok nondegenerateSpline -> 73 | let 74 | parameterizedSpline = 75 | nondegenerateSpline 76 | |> CubicSpline3d.arcLengthParameterized 77 | { maxError = meters 1.0e-3 } 78 | in 79 | CubicSpline3d.pointAlong parameterizedSpline (meters 0) 80 | |> Expect.point3d (CubicSpline3d.startPoint spline) 81 | 82 | Err _ -> 83 | Expect.pass 84 | ) 85 | 86 | 87 | pointAtArcLengthIsEnd : Test 88 | pointAtArcLengthIsEnd = 89 | Test.check "point along spline at arc length is end point" 90 | Random.cubicSpline3d 91 | (\spline -> 92 | case CubicSpline3d.nondegenerate spline of 93 | Ok nondegenerateSpline -> 94 | let 95 | parameterizedSpline = 96 | nondegenerateSpline 97 | |> CubicSpline3d.arcLengthParameterized 98 | { maxError = meters 1.0e-3 } 99 | 100 | arcLength = 101 | CubicSpline3d.arcLength parameterizedSpline 102 | in 103 | CubicSpline3d.pointAlong parameterizedSpline arcLength 104 | |> Expect.point3d (CubicSpline3d.endPoint spline) 105 | 106 | Err _ -> 107 | Expect.pass 108 | ) 109 | 110 | 111 | bSplineReproducesSpline : Test 112 | bSplineReproducesSpline = 113 | Test.check "Can reconstruct a cubic spline with a B-spline with repeated knots" 114 | Random.cubicSpline3d 115 | (\spline -> 116 | let 117 | p1 = 118 | CubicSpline3d.firstControlPoint spline 119 | 120 | p2 = 121 | CubicSpline3d.secondControlPoint spline 122 | 123 | p3 = 124 | CubicSpline3d.thirdControlPoint spline 125 | 126 | p4 = 127 | CubicSpline3d.fourthControlPoint spline 128 | 129 | bSplineSegments = 130 | CubicSpline3d.bSplineSegments [ 0, 0, 0, 1, 1, 1 ] 131 | [ p1, p2, p3, p4 ] 132 | in 133 | case bSplineSegments of 134 | [ singleSegment ] -> 135 | singleSegment |> Expect.cubicSpline3d spline 136 | 137 | _ -> 138 | Expect.fail "Expected a single B-spline segment" 139 | ) 140 | 141 | 142 | curveOperations : Tests.Generic.Curve3d.Operations (CubicSpline3d Meters coordinates) coordinates 143 | curveOperations = 144 | { generator = Random.cubicSpline3d 145 | , pointOn = CubicSpline3d.pointOn 146 | , boundingBox = CubicSpline3d.boundingBox 147 | , firstDerivative = CubicSpline3d.firstDerivative 148 | , firstDerivativeBoundingBox = CubicSpline3d.firstDerivativeBoundingBox 149 | , scaleAbout = CubicSpline3d.scaleAbout 150 | , translateBy = CubicSpline3d.translateBy 151 | , rotateAround = CubicSpline3d.rotateAround 152 | , mirrorAcross = CubicSpline3d.mirrorAcross 153 | , numApproximationSegments = CubicSpline3d.numApproximationSegments 154 | } 155 | 156 | 157 | genericTests : Test 158 | genericTests = 159 | Tests.Generic.Curve3d.tests 160 | curveOperations 161 | curveOperations 162 | CubicSpline3d.placeIn 163 | CubicSpline3d.relativeTo 164 | 165 | 166 | secondDerivativeBoundingBox : Test 167 | secondDerivativeBoundingBox = 168 | Tests.Generic.Curve3d.secondDerivativeBoundingBox 169 | { generator = Random.cubicSpline3d 170 | , secondDerivative = CubicSpline3d.secondDerivative 171 | , secondDerivativeBoundingBox = CubicSpline3d.secondDerivativeBoundingBox 172 | } 173 | -------------------------------------------------------------------------------- /tests/Tests/Polyline2d.elm: -------------------------------------------------------------------------------- 1 | module Tests.Polyline2d exposing 2 | ( centroidIsWithinBoundingBox 3 | , centroidOfClosedSquare 4 | , centroidOfOpenSquare 5 | , centroidOfRightAngle 6 | , centroidOfSingleSegmentIsSameAsMidpoint 7 | , centroidOfStepShape 8 | , emptyPolylineHasNothingCentroid 9 | , zeroLengthPolylineHasItselfAsCentroid 10 | ) 11 | 12 | import BoundingBox2d 13 | import Expect 14 | import Geometry.Expect as Expect 15 | import Geometry.Random as Random 16 | import Length exposing (meters) 17 | import Point2d 18 | import Polyline2d 19 | import Random 20 | import Test exposing (Test) 21 | import Test.Random as Test 22 | 23 | 24 | emptyPolylineHasNothingCentroid : Test 25 | emptyPolylineHasNothingCentroid = 26 | Test.test "Centroid is Nothing if Polyline is empty" <| 27 | \() -> 28 | let 29 | emptyPolyline = 30 | Polyline2d.fromVertices [] 31 | in 32 | Polyline2d.centroid emptyPolyline 33 | |> Expect.equal Nothing 34 | 35 | 36 | zeroLengthPolylineHasItselfAsCentroid : Test 37 | zeroLengthPolylineHasItselfAsCentroid = 38 | Test.check2 "Centroid of zero length polyline is the same point" 39 | Random.point2d 40 | (Random.int 1 20) 41 | (\point reps -> 42 | let 43 | singlePointLine = 44 | List.repeat reps point 45 | |> Polyline2d.fromVertices 46 | in 47 | Polyline2d.centroid singlePointLine 48 | |> Expect.equal (Just point) 49 | ) 50 | 51 | 52 | centroidOfSingleSegmentIsSameAsMidpoint : Test 53 | centroidOfSingleSegmentIsSameAsMidpoint = 54 | Test.check2 "Centroid of single line segment is middle of endpoints" 55 | Random.point2d 56 | Random.point2d 57 | (\p1 p2 -> 58 | let 59 | monoline = 60 | Polyline2d.fromVertices [ p1, p2 ] 61 | 62 | expectedCentroid = 63 | Point2d.midpoint p1 p2 64 | in 65 | Polyline2d.centroid monoline 66 | |> Expect.just (Expect.point2d expectedCentroid) 67 | ) 68 | 69 | 70 | centroidOfRightAngle : Test 71 | centroidOfRightAngle = 72 | Test.check "Centroid of a right angle is between the two sides" (Random.float -10 10) <| 73 | \armLength -> 74 | let 75 | angle = 76 | Polyline2d.fromVertices 77 | [ Point2d.fromTuple meters ( 0, 0 ) 78 | , Point2d.fromTuple meters ( armLength, 0 ) 79 | , Point2d.fromTuple meters ( armLength, armLength ) 80 | ] 81 | 82 | expectedCentroid = 83 | Point2d.meters (0.75 * armLength) (0.25 * armLength) 84 | in 85 | Polyline2d.centroid angle 86 | |> Expect.just (Expect.point2d expectedCentroid) 87 | 88 | 89 | centroidOfStepShape : Test 90 | centroidOfStepShape = 91 | Test.check "Centroid of a step shape is halfway up the step" (Random.float -10 10) <| 92 | \armLength -> 93 | let 94 | angle = 95 | Polyline2d.fromVertices 96 | [ Point2d.fromTuple meters ( 0, 0 ) 97 | , Point2d.fromTuple meters ( armLength, 0 ) 98 | , Point2d.fromTuple meters ( armLength, armLength ) 99 | , Point2d.fromTuple meters ( 2 * armLength, armLength ) 100 | ] 101 | 102 | expectedCentroid = 103 | Point2d.meters armLength (armLength / 2) 104 | in 105 | Polyline2d.centroid angle 106 | |> Expect.just (Expect.point2d expectedCentroid) 107 | 108 | 109 | centroidOfOpenSquare : Test 110 | centroidOfOpenSquare = 111 | Test.check "Centroid of an open square is skewed to closed side" (Random.float -10 10) <| 112 | \sideLength -> 113 | let 114 | squareline = 115 | Polyline2d.fromVertices 116 | [ Point2d.fromTuple meters ( 0, 0 ) 117 | , Point2d.fromTuple meters ( 0, sideLength ) 118 | , Point2d.fromTuple meters ( sideLength, sideLength ) 119 | , Point2d.fromTuple meters ( sideLength, 0 ) 120 | ] 121 | 122 | expectedCentroid = 123 | Point2d.meters (sideLength / 2) (sideLength * 2 / 3) 124 | in 125 | Polyline2d.centroid squareline 126 | |> Expect.just (Expect.point2d expectedCentroid) 127 | 128 | 129 | centroidOfClosedSquare : Test 130 | centroidOfClosedSquare = 131 | Test.check "Centroid of a closed square is mid-point" (Random.float -10 10) <| 132 | \sideLength -> 133 | let 134 | squareline = 135 | Polyline2d.fromVertices 136 | [ Point2d.fromTuple meters ( 0, 0 ) 137 | , Point2d.fromTuple meters ( 0, sideLength ) 138 | , Point2d.fromTuple meters ( sideLength, sideLength ) 139 | , Point2d.fromTuple meters ( sideLength, 0 ) 140 | , Point2d.fromTuple meters ( 0, 0 ) 141 | ] 142 | 143 | expectedCentroid = 144 | Point2d.meters (sideLength / 2) (sideLength / 2) 145 | in 146 | Polyline2d.centroid squareline 147 | |> Expect.just (Expect.point2d expectedCentroid) 148 | 149 | 150 | centroidIsWithinBoundingBox : Test 151 | centroidIsWithinBoundingBox = 152 | Test.check3 "The centroid of a polyline is within the polyline's bounding box" 153 | Random.point2d 154 | Random.point2d 155 | (Random.smallList Random.point2d) 156 | (\first second rest -> 157 | let 158 | points = 159 | first :: second :: rest 160 | 161 | polyline = 162 | Polyline2d.fromVertices points 163 | 164 | maybeBoundingBox = 165 | Polyline2d.boundingBox polyline 166 | 167 | maybeCentroid = 168 | Polyline2d.centroid polyline 169 | in 170 | case ( maybeBoundingBox, maybeCentroid ) of 171 | ( Just boundingBox, Just centroid ) -> 172 | Expect.point2dContainedIn boundingBox centroid 173 | 174 | ( Nothing, _ ) -> 175 | Expect.fail "Error determining bounding box." 176 | 177 | ( _, Nothing ) -> 178 | Expect.fail "Error determining centroid." 179 | ) 180 | --------------------------------------------------------------------------------