From e60b896db308406e5ddb0d3bd313e82b3835450f Mon Sep 17 00:00:00 2001 From: Andrey Kuzmin Date: Fri, 8 Dec 2023 11:24:44 +0100 Subject: [PATCH] Document the code --- src/Ball.elm | 22 +++--- src/Bodies.elm | 8 +++ src/Camera.elm | 172 +++++++++++++++++++++++++++----------------- src/Cue.elm | 12 +++- src/Table.elm | 189 ++++++++++++++++++++++++++----------------------- 5 files changed, 235 insertions(+), 168 deletions(-) diff --git a/src/Ball.elm b/src/Ball.elm index a1a9d4f..ddb61a3 100644 --- a/src/Ball.elm +++ b/src/Ball.elm @@ -16,7 +16,7 @@ import Quantity import Scene3d exposing (Entity) import Scene3d.Material as Material import SketchPlane3d -import Sphere3d exposing (Sphere3d) +import Sphere3d import Vector3d @@ -38,27 +38,21 @@ damping = { linear = 0.4, angular = 0.4 } -ballMaterial : Material -ballMaterial = +material : Material +material = Material.custom { friction = 0.06 , bounciness = 0.6 } -sphere : Sphere3d Meters BodyCoordinates -sphere = - Sphere3d.atOrigin radius - - body : id -> Body id body id = - Body.sphere sphere - id - |> Body.withMaterial ballMaterial + Body.sphere (Sphere3d.atOrigin radius) id + |> Body.withMaterial material |> Body.withDamping damping |> Body.withBehavior (Body.dynamic weight) - -- rotate to see the numbers + -- rotate to see the numbers on the balls |> Body.rotateAround Axis3d.x (Angle.degrees 90) @@ -71,9 +65,11 @@ entity baseColor roughnessTexture = , metallic = Material.constant 0 } ) - sphere + (Sphere3d.atOrigin radius) +{-| Rack the balls at the foot spot on the table +-} rack : Point2d Meters WorldCoordinates -> List (Body Id) rack footSpot = let diff --git a/src/Bodies.elm b/src/Bodies.elm index 9dbb29e..53965e1 100644 --- a/src/Bodies.elm +++ b/src/Bodies.elm @@ -1,8 +1,16 @@ module Bodies exposing (Id(..)) +{-| + +@docs Id + +-} + import EightBall exposing (Ball) +{-| Identify the different bodies in the physical simulation +-} type Id = Floor | Numbered Ball diff --git a/src/Camera.elm b/src/Camera.elm index 7f7458d..81fe542 100644 --- a/src/Camera.elm +++ b/src/Camera.elm @@ -1,18 +1,31 @@ module Camera exposing - ( Camera - , ScreenCoordinates - , animate - , azimuth - , camera3d - , focusOn - , initial - , mouseOrbiting - , mouseWheelZoom - , orbitingPrecision - , ray - , zoomOut + ( ScreenCoordinates, Camera, initial + , camera3d, azimuth, orbitingPrecision + , ray, mouseOrbiting, mouseWheelZoom + , focusOn, zoomOut, animate ) +{-| Animated 3d camera controls + +@docs ScreenCoordinates, Camera, initial + + +# Current state + +@docs camera3d, azimuth, orbitingPrecision + + +# Interaction + +@docs ray, mouseOrbiting, mouseWheelZoom + + +# Animation + +@docs focusOn, zoomOut, animate + +-} + import Angle exposing (Angle) import Animator exposing (Timeline) import Axis3d exposing (Axis3d) @@ -30,10 +43,13 @@ import Vector2d import Viewpoint3d +{-| Screen space coordinate system +-} type ScreenCoordinates = ScreenCoordinates Never +{-| -} type Camera = Camera { zoom : Timeline Float -- also used for orbiting precision @@ -43,6 +59,8 @@ type Camera } +{-| Initial look at the table +-} initial : Camera initial = Camera @@ -53,26 +71,30 @@ initial = } -ray : - Camera - -> Rectangle2d Pixels ScreenCoordinates - -> Point2d Pixels ScreenCoordinates - -> Axis3d Meters WorldCoordinates -ray camera = - Camera3d.ray (camera3d camera) +-- CURRENT STATE + +{-| Get the currrent Camera3d for rendering with elm-3d-scene +-} camera3d : Camera -> Camera3d Meters WorldCoordinates camera3d (Camera camera) = let distance = Animator.move camera.zoom Animator.at |> Quantity.interpolateFrom (Length.meters 0.5) (Length.meters 6) + + focalPoint = + Point3d.fromRecord Length.meters <| + Animator.xyz camera.focalPoint + (Point3d.toMeters + >> (\p -> { x = Animator.at p.x, y = Animator.at p.y, z = Animator.at p.z }) + ) in Camera3d.perspective { viewpoint = Viewpoint3d.orbit - { focalPoint = pointFromTimeline camera.focalPoint + { focalPoint = focalPoint , groundPlane = SketchPlane3d.xy , azimuth = camera.azimuth , elevation = angleFromTimeline camera.elevation @@ -82,30 +104,41 @@ camera3d (Camera camera) = } +{-| Get the currrent azimuth used for aiming the cue +-} azimuth : Camera -> Angle azimuth (Camera camera) = camera.azimuth -animate : Posix -> Camera -> Camera -animate time (Camera camera) = - Camera - { camera - | elevation = Animator.updateTimeline time camera.elevation - , zoom = Animator.updateTimeline time camera.zoom - , focalPoint = Animator.updateTimeline time camera.focalPoint - } +{-| Make orbiting precision depend on zoom level. +Controls how much radians correspond to the change in mouse offset. +-} +orbitingPrecision : Camera -> Quantity Float (Quantity.Rate Angle.Radians Pixels) +orbitingPrecision (Camera camera) = + Quantity.rate + (Angle.radians (0.2 + Animator.move camera.zoom Animator.at / 0.8)) + (Pixels.pixels (180 / pi)) -mouseWheelZoom : Float -> Camera -> Camera -mouseWheelZoom deltaY (Camera camera) = - let - newZoom = - clamp 0 1 (Animator.move camera.zoom Animator.at - deltaY * 0.002) - in - Camera { camera | zoom = Animator.go Animator.immediately newZoom camera.zoom } + +-- INTERACTION + + +{-| Get the ray from the camera into the viewplane, +useful for mouse interactions with the 3d objects +-} +ray : + Camera + -> Rectangle2d Pixels ScreenCoordinates + -> Point2d Pixels ScreenCoordinates + -> Axis3d Meters WorldCoordinates +ray camera = + Camera3d.ray (camera3d camera) +{-| Orbit the camera with mouse +-} mouseOrbiting : Point2d Pixels ScreenCoordinates -> Point2d Pixels ScreenCoordinates -> Camera -> Camera mouseOrbiting originalPosition newPosition (Camera camera) = let @@ -134,15 +167,30 @@ mouseOrbiting originalPosition newPosition (Camera camera) = } -zoomOut : Camera -> Camera -zoomOut (Camera camera) = - Camera - { camera - | zoom = Animator.go Animator.verySlowly 1 camera.zoom - , elevation = Animator.go Animator.verySlowly (Angle.degrees 50) camera.elevation - } +{-| Zoom in/out by mouse wheel delta +-} +mouseWheelZoom : Float -> Camera -> Camera +mouseWheelZoom deltaY (Camera camera) = + let + newZoom = + clamp 0 1 (Animator.move camera.zoom Animator.at - deltaY * 0.002) + in + Camera { camera | zoom = Animator.go Animator.immediately newZoom camera.zoom } + + +{-| Read the angle value from the timeline +-} +angleFromTimeline : Timeline Angle -> Angle +angleFromTimeline angleTimeline = + Angle.radians (Animator.move angleTimeline (Angle.inRadians >> Animator.at)) + +-- ANIMATION + + +{-| Animate the focal point of the camera to the new position +-} focusOn : Point3d Meters WorldCoordinates -> Camera -> Camera focusOn focalPoint (Camera camera) = Camera @@ -151,29 +199,25 @@ focusOn focalPoint (Camera camera) = } -{-| Make orbiting precision depend on zoom level. -Controls how much radians correspond to the change in mouse offset. +{-| Zoom out the camera to look over the table from the top -} -orbitingPrecision : Camera -> Quantity Float (Quantity.Rate Angle.Radians Pixels) -orbitingPrecision (Camera camera) = - Quantity.rate - (Angle.radians (0.2 + Animator.move camera.zoom Animator.at / 0.8)) - (Pixels.pixels (180 / pi)) - - -{-| Read the angle value from the timeline --} -angleFromTimeline : Timeline Angle -> Angle -angleFromTimeline angleTimeline = - Angle.radians (Animator.move angleTimeline (Angle.inRadians >> Animator.at)) +zoomOut : Camera -> Camera +zoomOut (Camera camera) = + Camera + { camera + | zoom = Animator.go Animator.verySlowly 1 camera.zoom + , elevation = Animator.go Animator.verySlowly (Angle.degrees 50) camera.elevation + } -{-| Read the point value from the timeline +{-| Update the camera animation state, this needs to be called +from the animation frame subscription -} -pointFromTimeline : Timeline (Point3d Meters WorldCoordinates) -> Point3d Meters WorldCoordinates -pointFromTimeline pointTimeline = - Point3d.fromRecord Length.meters <| - Animator.xyz pointTimeline - (Point3d.toMeters - >> (\p -> { x = Animator.at p.x, y = Animator.at p.y, z = Animator.at p.z }) - ) +animate : Posix -> Camera -> Camera +animate time (Camera camera) = + Camera + { camera + | elevation = Animator.updateTimeline time camera.elevation + , zoom = Animator.updateTimeline time camera.zoom + , focalPoint = Animator.updateTimeline time camera.focalPoint + } diff --git a/src/Cue.elm b/src/Cue.elm index 6021f58..2b2c044 100644 --- a/src/Cue.elm +++ b/src/Cue.elm @@ -1,4 +1,11 @@ -module Cue exposing (canShoot, entity) +module Cue exposing (entity, canShoot) + +{-| The cue is reperensented as an `Axis3d Meters WorldCoordinates` +that points away from the hit point on the cue ball. + +@docs entity, canShoot + +-} import Angle import Axis3d exposing (Axis3d) @@ -36,6 +43,9 @@ offset = Length.centimeters 2 +{-| Render the cue as a cylinder, make sure it is trimmed with the end cap +when interecting with the view plane. +-} entity : Camera3d Meters WorldCoordinates -> Length -> Color -> Axis3d Meters WorldCoordinates -> Entity WorldCoordinates entity camera3d clipDepth color axis = case cylinder camera3d clipDepth axis of diff --git a/src/Table.elm b/src/Table.elm index a6b4020..c1a14fb 100644 --- a/src/Table.elm +++ b/src/Table.elm @@ -1,10 +1,6 @@ module Table exposing - ( Table - , areaBallInHand - , areaBehindTheHeadString - , areaBehindTheHeadStringEntity - , footSpot - , load + ( Table, load + , footSpot, areaBallInHand, areaBehindTheHeadString, areaBehindTheHeadStringEntity ) {-| elm-obj-file is used to decode various objects from the obj file. @@ -16,6 +12,13 @@ module Table exposing The Billiard Table model is designed by Kolja Wilcke +@docs Table, load + + +# Dimensions + +@docs footSpot, areaBallInHand, areaBehindTheHeadString, areaBehindTheHeadStringEntity + -} import Ball @@ -42,96 +45,15 @@ import SketchPlane3d import Task exposing (Task) +{-| The visual entity and the collider bodies used in simulation +-} type alias Table = { bodies : List (Body Id) , entity : Entity BodyCoordinates } -length : Length -length = - Length.meters 2.26 - - -width : Length -width = - Length.meters 1.24 - - -{-| Anywhere on the table. This where the cue ball should be placed after it goes in a pocket -or after the failure to hit the object ball. --} -areaBallInHand : Rectangle3d Meters WorldCoordinates -areaBallInHand = - let - xOffset = - Quantity.half length |> Quantity.minus Ball.radius - - yOffset = - Quantity.half width |> Quantity.minus Ball.radius - in - Rectangle3d.on SketchPlane3d.xy - (Rectangle2d.from - (Point2d.xy (Quantity.negate xOffset) (Quantity.negate yOffset)) - (Point2d.xy xOffset yOffset) - ) - |> Rectangle3d.translateIn Direction3d.z (Length.millimeters 1) - - -{-| The foot spot is the place where you “spot the ball”, it is also -the place where the top object ball is placed when racking a game --} -footSpot : Point2d Meters WorldCoordinates -footSpot = - Point2d.xy - (Quantity.half (Quantity.half length)) - Quantity.zero - - -{-| The area where you break from, and where you must place the cue ball after a scratch. --} -areaBehindTheHeadString : Rectangle3d Meters WorldCoordinates -areaBehindTheHeadString = - let - yOffset = - Quantity.half width |> Quantity.minus Ball.radius - - xMin = - Quantity.half length |> Quantity.minus Ball.radius |> Quantity.negate - - xMax = - Quantity.half (Quantity.half length) |> Quantity.negate - in - Rectangle3d.on SketchPlane3d.xy - (Rectangle2d.from - (Point2d.xy xMin (Quantity.negate yOffset)) - (Point2d.xy xMax yOffset) - ) - |> Rectangle3d.translateIn Direction3d.z (Length.millimeters 1) - - -{-| Highlight the area behind the head string when the ball should be placed there --} -areaBehindTheHeadStringEntity : Entity WorldCoordinates -areaBehindTheHeadStringEntity = - case Rectangle3d.vertices areaBehindTheHeadString of - [ v1, v2, v3, v4 ] -> - Scene3d.quad - (Material.nonmetal - { baseColor = Color.rgb255 131 146 34 - , roughness = 1 - } - ) - v1 - v2 - v3 - v4 - - _ -> - Scene3d.nothing - - -{-| Load the visual entity and the collider bodies for the table from the obj file and texture files +{-| Load the table from the obj file and texture files -} load : { colorTexture : String, roughnessTexture : String, metallicTexture : String, mesh : String } -> Task String Table load urls = @@ -231,3 +153,90 @@ startsWith prefix decoder = |> List.map (\name -> Obj.Decode.object name decoder) |> Obj.Decode.combine ) + + + +-- DIMENSIONS + + +length : Length +length = + Length.meters 2.26 + + +width : Length +width = + Length.meters 1.24 + + +{-| The foot spot is the place where you “spot the ball”, it is also +the place where the top object ball is placed when racking a game +-} +footSpot : Point2d Meters WorldCoordinates +footSpot = + Point2d.xy + (Quantity.half (Quantity.half length)) + Quantity.zero + + +{-| Anywhere on the table. This where the cue ball should be placed after it goes in a pocket +or after the failure to hit the object ball. +-} +areaBallInHand : Rectangle3d Meters WorldCoordinates +areaBallInHand = + let + xOffset = + Quantity.half length |> Quantity.minus Ball.radius + + yOffset = + Quantity.half width |> Quantity.minus Ball.radius + in + Rectangle3d.on SketchPlane3d.xy + (Rectangle2d.from + (Point2d.xy (Quantity.negate xOffset) (Quantity.negate yOffset)) + (Point2d.xy xOffset yOffset) + ) + |> Rectangle3d.translateIn Direction3d.z (Length.millimeters 1) + + +{-| The area where you break from, and where you must place the cue ball after a scratch. +-} +areaBehindTheHeadString : Rectangle3d Meters WorldCoordinates +areaBehindTheHeadString = + let + yOffset = + Quantity.half width |> Quantity.minus Ball.radius + + xMin = + Quantity.half length |> Quantity.minus Ball.radius |> Quantity.negate + + xMax = + Quantity.half (Quantity.half length) |> Quantity.negate + in + Rectangle3d.on SketchPlane3d.xy + (Rectangle2d.from + (Point2d.xy xMin (Quantity.negate yOffset)) + (Point2d.xy xMax yOffset) + ) + |> Rectangle3d.translateIn Direction3d.z (Length.millimeters 1) + + +{-| Highlight the area behind the head string when the ball should be placed there +-} +areaBehindTheHeadStringEntity : Entity WorldCoordinates +areaBehindTheHeadStringEntity = + case Rectangle3d.vertices areaBehindTheHeadString of + [ v1, v2, v3, v4 ] -> + Scene3d.quad + (Material.nonmetal + { baseColor = Color.rgb255 131 146 34 + , roughness = 1 + } + ) + v1 + v2 + v3 + v4 + + _ -> + Scene3d.nothing