From 53bf7c73cc7b8945095c64f1b52d4b3f2c2f0c84 Mon Sep 17 00:00:00 2001 From: Andrey Kuzmin Date: Sat, 9 Jan 2021 18:30:50 +0100 Subject: [PATCH] Add physics example --- examples/Lack.elm | 331 ++++++++++++++++++++++++++++++++++++++++++++++ examples/elm.json | 4 +- 2 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 examples/Lack.elm diff --git a/examples/Lack.elm b/examples/Lack.elm new file mode 100644 index 0000000..6af0c47 --- /dev/null +++ b/examples/Lack.elm @@ -0,0 +1,331 @@ +module Lack exposing (main) + +{-| This demo allows dragging objects with mouse, try flipping the table! + +1. Uses `World.raycast` on mouse down to pick a body +2. Creates a temporary body at the mouse position +3. Connects the temporary body with the selected body using a point to point constraint +4. Moves the temporary body on mouse move +5. Removes the temporary body on mouse up + +-} + +import Acceleration +import Angle +import Axis3d exposing (Axis3d) +import Block3d exposing (Block3d) +import Browser +import Browser.Dom +import Browser.Events +import Camera3d exposing (Camera3d) +import Color +import Direction3d +import Duration exposing (seconds) +import Html exposing (Html) +import Html.Attributes +import Html.Events +import Json.Decode exposing (Decoder) +import Length exposing (Meters, meters, millimeters) +import Mass exposing (kilograms) +import Physics.Body as Body exposing (Body) +import Physics.Constraint as Constraint +import Physics.Coordinates exposing (BodyCoordinates, WorldCoordinates) +import Physics.Shape +import Physics.World as World exposing (RaycastResult, World) +import Pixels exposing (Pixels, pixels) +import Plane3d +import Point2d +import Point3d +import Quantity exposing (Quantity) +import Rectangle2d +import Scene3d exposing (Entity) +import Scene3d.Material as Material +import Sphere3d +import Task +import Viewpoint3d + + +type Id + = Mouse + | Floor + | Table + + +type alias Model = + { world : World Id + , width : Quantity Float Pixels + , height : Quantity Float Pixels + , maybeRaycastResult : Maybe (RaycastResult Id) + } + + +type Msg + = AnimationFrame + | Resize Int Int + | MouseDown (Axis3d Meters WorldCoordinates) + | MouseMove (Axis3d Meters WorldCoordinates) + | MouseUp + + +main : Program () Model Msg +main = + Browser.element + { init = init + , update = \msg model -> ( update msg model, Cmd.none ) + , subscriptions = subscriptions + , view = view + } + + +init : () -> ( Model, Cmd Msg ) +init _ = + ( { world = initialWorld + , width = pixels 0 + , height = pixels 0 + , maybeRaycastResult = Nothing + } + , Task.perform + (\{ viewport } -> + Resize (round viewport.width) (round viewport.height) + ) + Browser.Dom.getViewport + ) + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.batch + [ Browser.Events.onResize Resize + , Browser.Events.onAnimationFrame (\_ -> AnimationFrame) + ] + + +initialWorld : World Id +initialWorld = + World.empty + |> World.withGravity (Acceleration.gees 1) Direction3d.negativeZ + |> World.add table + |> World.add (Body.plane Floor) + + +tableBlocks : List (Block3d Meters BodyCoordinates) +tableBlocks = + [ Block3d.from + (Point3d.millimeters 222 222 0) + (Point3d.millimeters 272 272 400) + , Block3d.from + (Point3d.millimeters -272 222 0) + (Point3d.millimeters -222 272 400) + , Block3d.from + (Point3d.millimeters -272 -272 0) + (Point3d.millimeters -222 -222 400) + , Block3d.from + (Point3d.millimeters 222 -272 0) + (Point3d.millimeters 272 -222 400) + , Block3d.from + (Point3d.millimeters -275 -275 400) + (Point3d.millimeters 275 275 450) + ] + + +table : Body Id +table = + Body.compound (List.map Physics.Shape.block tableBlocks) Table + |> Body.withBehavior (Body.dynamic (kilograms 3.58)) + + +camera : Camera3d Meters WorldCoordinates +camera = + Camera3d.perspective + { viewpoint = + Viewpoint3d.lookAt + { eyePoint = Point3d.meters 3 4 2 + , focalPoint = Point3d.meters -0.5 -0.5 0 + , upDirection = Direction3d.positiveZ + } + , verticalFieldOfView = Angle.degrees 24 + } + + +view : Model -> Html Msg +view { world, width, height } = + Html.div + [ Html.Attributes.style "position" "absolute" + , Html.Attributes.style "left" "0" + , Html.Attributes.style "top" "0" + , Html.Events.on "mousedown" (decodeMouseRay camera width height MouseDown) + , Html.Events.on "mousemove" (decodeMouseRay camera width height MouseMove) + , Html.Events.onMouseUp MouseUp + ] + [ Scene3d.sunny + { upDirection = Direction3d.z + , sunlightDirection = Direction3d.xyZ (Angle.degrees 135) (Angle.degrees -60) + , shadows = True + , camera = camera + , dimensions = + ( Pixels.int (round (Pixels.toFloat width)) + , Pixels.int (round (Pixels.toFloat height)) + ) + , background = Scene3d.transparentBackground + , clipDepth = Length.meters 0.1 + , entities = List.map bodyToEntity (World.bodies world) + } + ] + + +bodyToEntity : Body Id -> Entity WorldCoordinates +bodyToEntity body = + let + frame = + Body.frame body + + id = + Body.data body + in + Scene3d.placeIn frame <| + case id of + Mouse -> + Scene3d.sphere (Material.matte Color.white) + (Sphere3d.atOrigin (millimeters 20)) + + Table -> + tableBlocks + |> List.map + (Scene3d.blockWithShadow + (Material.nonmetal + { baseColor = Color.white + , roughness = 0.25 + } + ) + ) + |> Scene3d.group + + Floor -> + Scene3d.quad (Material.matte Color.darkCharcoal) + (Point3d.meters -100 -100 0) + (Point3d.meters -100 100 0) + (Point3d.meters 100 100 0) + (Point3d.meters 100 -100 0) + + +update : Msg -> Model -> Model +update msg model = + case msg of + AnimationFrame -> + { model | world = World.simulate (seconds (1 / 60)) model.world } + + Resize width height -> + { model + | width = Pixels.float (toFloat width) + , height = Pixels.float (toFloat height) + } + + MouseDown mouseRay -> + case World.raycast mouseRay model.world of + Just raycastResult -> + case Body.data raycastResult.body of + Table -> + let + worldPoint = + Point3d.placeIn + (Body.frame raycastResult.body) + raycastResult.point + + mouse = + Body.compound [] Mouse + |> Body.moveTo worldPoint + in + { model + | maybeRaycastResult = Just raycastResult + , world = + model.world + |> World.add mouse + |> World.constrain + (\b1 b2 -> + case ( Body.data b1, Body.data b2 ) of + ( Mouse, Table ) -> + [ Constraint.pointToPoint + Point3d.origin + raycastResult.point + ] + + _ -> + [] + ) + } + + _ -> + model + + Nothing -> + model + + MouseMove mouseRay -> + case model.maybeRaycastResult of + Just raycastResult -> + let + worldPoint = + Point3d.placeIn + (Body.frame raycastResult.body) + raycastResult.point + + plane = + Plane3d.through + worldPoint + (Viewpoint3d.viewDirection (Camera3d.viewpoint camera)) + in + { model + | world = + World.update + (\body -> + if Body.data body == Mouse then + case Axis3d.intersectionWithPlane plane mouseRay of + Just intersection -> + Body.moveTo intersection body + + Nothing -> + body + + else + body + ) + model.world + } + + Nothing -> + model + + MouseUp -> + { model + | maybeRaycastResult = Nothing + , world = + World.keepIf + (\body -> Body.data body /= Mouse) + model.world + } + + +decodeMouseRay : + Camera3d Meters WorldCoordinates + -> Quantity Float Pixels + -> Quantity Float Pixels + -> (Axis3d Meters WorldCoordinates -> msg) + -> Decoder msg +decodeMouseRay camera3d width height rayToMsg = + Json.Decode.map2 + (\x y -> + rayToMsg + (Camera3d.ray + camera3d + (Rectangle2d.with + { x1 = pixels 0 + , y1 = height + , x2 = width + , y2 = pixels 0 + } + ) + (Point2d.pixels x y) + ) + ) + (Json.Decode.field "pageX" Json.Decode.float) + (Json.Decode.field "pageY" Json.Decode.float) diff --git a/examples/elm.json b/examples/elm.json index cb37c7a..5e559ea 100644 --- a/examples/elm.json +++ b/examples/elm.json @@ -28,7 +28,7 @@ "mdgriffith/elm-ui": "1.1.8", "ohanhi/keyboard": "2.0.1", "w0rm/elm-obj-file": "1.0.0", - "w0rm/elm-physics": "5.1.1" + "w0rm/elm-physics": "5.1.3" }, "indirect": { "elm/bytes": "1.0.8", @@ -43,4 +43,4 @@ "direct": {}, "indirect": {} } -} +} \ No newline at end of file