//
//  dynamicsTests.js
//  scripts/developer/tests/dynamics/
//
//  Created by Seth Alves 2017-4-30
//  Copyright 2017 High Fidelity, Inc.
//
//  Distributed under the Apache License, Version 2.0.
//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//

"use strict";

/* global Entities, Script, Tablet, MyAvatar, Vec3 */

(function() { // BEGIN LOCAL_SCOPE

    var DYNAMICS_TESTS_URL = Script.resolvePath("dynamics-tests.html");
    var DEFAULT_LIFETIME = 120; // seconds

    var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");

    var button = tablet.addButton({
        icon: Script.resolvePath("dynamicsTests.svg"),
        text: "Dynamics"
    });



    function coneTwistAndTractorLeverTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: -0.5, z: -2}));
        var lifetime = params.lifetime;

        var baseID = Entities.addEntity({
            name: "cone-twist test -- base",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: 0.5, y: 0.2, z: 0.5 },
            position: Vec3.sum(pos, { x: 0, y: 0, z:0 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        var leverID = Entities.addEntity({
            name: "cone-twist test -- lever",
            type: "Box",
            color: { blue: 128, green: 100, red: 200 },
            dimensions: { x: 0.05, y: 1, z: 0.05 },
            position: Vec3.sum(pos, { x: 0, y: 0.7, z:0 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addEntity({
            name: "cone-twist test -- handle",
            type: "Box",
            color: { blue: 30, green: 100, red: 200 },
            dimensions: { x: 0.1, y: 0.08, z: 0.08 },
            position: Vec3.sum(pos, { x: 0, y: 0.7 + 0.5, z:0 }),
            dynamic: false,
            collisionless: true,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            parentID: leverID,
            userData: "{ \"grabbableKey\": { \"grabbable\": false } }"
        });

        Entities.addAction("cone-twist", baseID, {
            pivot: { x: 0, y: 0.2, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: leverID,
            otherPivot: { x: 0, y: -0.55, z: 0 },
            otherAxis: { x: 0, y: 1, z: 0 },
            swingSpan1: Math.PI / 4,
            swingSpan2: Math.PI / 4,
            twistSpan: Math.PI / 2,
            tag: "cone-twist test"
        });

        Entities.addAction("tractor", leverID, {
            targetRotation: { x: 0, y: 0, z: 0, w: 1 },
            angularTimeScale: 0.2,
            tag: "cone-twist test tractor"
        });


        Entities.editEntity(baseID, { gravity: { x: 0, y: -5, z: 0 } });
    }

    function doorVSWorldTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var doorID = Entities.addEntity({
            name: "door test",
            type: "Box",
            color: { blue: 128, green: 20, red: 20 },
            dimensions: { x: 1.0, y: 2, z: 0.1 },
            position: pos,
            dynamic: true,
            collisionless: false,
            lifetime: lifetime,
            gravity: { x: 0, y: 0, z: 0 },
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", doorID, {
            pivot: { x: -0.5, y: 0, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            low: 0,
            high: Math.PI,
            tag: "door hinge test"
        });
    }

    function hingeChainTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var offset = 0.28;
        var prevEntityID = null;
        for (var i = 0; i < 5; i++) {
            var newID = Entities.addEntity({
                name: "hinge test " + i,
                type: "Box",
                color: { blue: 128, green: 40 * i, red: 20 },
                dimensions: { x: 0.2, y: 0.2, z: 0.1 },
                position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}),
                dynamic: true,
                collisionless: false,
                lifetime: lifetime,
                gravity: { x: 0, y: 0, z: 0 },
                userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
            });
            if (prevEntityID) {
                Entities.addAction("hinge", prevEntityID, {
                    pivot: { x: 0, y: offset / 2, z: 0 },
                    axis: { x: 1, y: 0, z: 0 },
                    otherEntityID: newID,
                    otherPivot: { x: 0, y: -offset / 2, z: 0 },
                    otherAxis: { x: 1, y: 0, z: 0 },
                    tag: "A/B hinge test " + i
                });
            }
            prevEntityID = newID;
        }
    }

    function sliderVSWorldTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var sliderEntityID = Entities.addEntity({
            name: "slider test",
            type: "Box",
            color: { blue: 128, green: 20, red: 20 },
            dimensions: { x: 0.2, y: 0.2, z: 0.2 },
            position: pos,
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("slider", sliderEntityID, {
            point: { x: -0.5, y: 0, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            linearLow: 0,
            linearHigh: 0.6,
            tag: "slider test"
        });
    }

    function sliderChainTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var offset = 0.28;
        var prevEntityID = null;
        for (var i = 0; i < 7; i++) {
            var newID = Entities.addEntity({
                name: "slider test " + i,
                type: "Box",
                color: { blue: 128, green: 40 * i, red: 20 },
                dimensions: { x: 0.2, y: 0.1, z: 0.2 },
                position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}),
                dynamic: true,
                collisionless: false,
                gravity: { x: 0, y: 0, z: 0 },
                lifetime: lifetime,
                userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
            });
            if (prevEntityID) {
                Entities.addAction("slider", prevEntityID, {
                    point: { x: 0, y: 0, z: 0 },
                    axis: { x: 0, y: 1, z: 0 },
                    otherEntityID: newID,
                    otherPoint: { x: 0, y: -offset / 2, z: 0 },
                    otherAxis: { x: 0, y: 1, z: 0 },
                    linearLow: 0,
                    linearHigh: 0.6,
                    tag: "A/B slider test " + i
                });
            }
            prevEntityID = newID;
        }
    }

    function ballSocketBetweenTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var offset = 0.2;
        var diameter = offset - 0.01;
        var prevEntityID = null;
        for (var i = 0; i < 7; i++) {
            var newID = Entities.addEntity({
                name: "ball and socket test " + i,
                type: "Sphere",
                color: { blue: 128, green: 40 * i, red: 20 },
                dimensions: { x: diameter, y: diameter, z: diameter },
                position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}),
                dynamic: true,
                collisionless: false,
                lifetime: lifetime,
                gravity: { x: 0, y: 0, z: 0 },
                userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
            });
            if (prevEntityID) {
                Entities.addAction("ball-socket", prevEntityID, {
                    pivot: { x: 0, y: offset / 2, z: 0 },
                    otherEntityID: newID,
                    otherPivot: { x: 0, y: -offset / 2, z: 0 },
                    tag: "A/B ball-and-socket test " + i
                });
            }
            prevEntityID = newID;
        }
    }

    function ballSocketCoincidentTest(params) {
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.1, z: -2}));
        var lifetime = params.lifetime;

        var offset = 0.2;
        var diameter = offset - 0.01;
        var prevEntityID = null;
        for (var i = 0; i < 7; i++) {
            var newID = Entities.addEntity({
                name: "ball and socket test " + i,
                type: "Sphere",
                color: { blue: 128, green: 40 * i, red: 20 },
                dimensions: { x: diameter, y: diameter, z: diameter },
                position: Vec3.sum(pos, {x: 0, y: offset * i, z:0}),
                dynamic: true,
                collisionless: false,
                lifetime: lifetime,
                gravity: { x: 0, y: 0, z: 0 },
                userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
            });
            if (prevEntityID) {
                Entities.addAction("ball-socket", prevEntityID, {
                    pivot: { x: 0, y: 0, z: 0 },
                    otherEntityID: newID,
                    otherPivot: { x: 0, y: offset, z: 0 },
                    tag: "A/B ball-and-socket test " + i
                });
            }
            prevEntityID = newID;
        }
    }

    function ragdollTest(params) {
        var scale = 1.6;
        var lifetime = params.lifetime;
        var pos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 1.0, z: -2}));

        var neckLength = scale * 0.05;
        var shoulderGap = scale * 0.1;
        var elbowGap = scale * 0.06;
        var hipGap = scale * 0.07;
        var kneeGap = scale * 0.08;
        var ankleGap = scale * 0.06;
        var ankleMin = 0;
        var ankleMax = Math.PI / 4;

        var headSize = scale * 0.2;

        var bodyHeight = scale * 0.4;
        var bodyWidth = scale * 0.3;
        var bodyDepth = scale * 0.2;

        var upperArmThickness = scale * 0.05;
        var upperArmLength = scale * 0.2;

        var lowerArmThickness = scale * 0.05;
        var lowerArmLength = scale * 0.2;

        var legLength = scale * 0.3;
        var legThickness = scale * 0.08;

        var shinLength = scale * 0.2;
        var shinThickness = scale * 0.06;

        var footLength = scale * 0.2;
        var footThickness = scale * 0.03;
        var footWidth = scale * 0.08;


        //
        // body
        //

        var bodyID = Entities.addEntity({
            name: "ragdoll body",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: bodyDepth, y: bodyHeight, z: bodyWidth },
            position: Vec3.sum(pos, { x: 0, y: scale * 0.0, z:0 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        //
        // head
        //

        var headID = Entities.addEntity({
            name: "ragdoll head",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: headSize, y: headSize, z: headSize },
            position: Vec3.sum(pos, { x: 0, y: bodyHeight / 2 + headSize / 2 + neckLength, z:0 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0.5, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("tractor", headID, {
            targetRotation: { x: 0, y: 0, z: 0, w: 1 },
            angularTimeScale: 2.0,
            otherID: bodyID,
            tag: "cone-twist test tractor"
        });


        var noseID = Entities.addEntity({
            name: "ragdoll nose",
            type: "Box",
            color: { blue: 128, green: 100, red: 100 },
            dimensions: { x: headSize / 5, y: headSize / 5, z: headSize / 5 },
            localPosition: { x: headSize / 2 + headSize / 10, y: 0, z: 0 },
            dynamic: false,
            collisionless: true,
            lifetime: lifetime,
            parentID: headID,
            userData: "{ \"grabbableKey\": { \"grabbable\": false } }"
        });

        Entities.addAction("cone-twist", headID, {
            pivot: { x: 0, y: -headSize / 2 - neckLength / 2, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: bodyID,
            otherPivot: { x: 0, y: bodyHeight / 2 + neckLength / 2, z: 0 },
            otherAxis: { x: 0, y: 1, z: 0 },
            swingSpan1: Math.PI / 4,
            swingSpan2: Math.PI / 4,
            twistSpan: Math.PI / 2,
            tag: "ragdoll neck joint"
        });

        //
        // right upper arm
        //

        var rightUpperArmID = Entities.addEntity({
            name: "ragdoll right arm",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: upperArmThickness, y: upperArmThickness, z: upperArmLength },
            position: Vec3.sum(pos, { x: 0,
                                      y: bodyHeight / 2 + upperArmThickness / 2,
                                      z: bodyWidth / 2 + shoulderGap + upperArmLength / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("cone-twist", bodyID, {
            pivot: { x: 0, y: bodyHeight / 2 + upperArmThickness / 2, z: bodyWidth / 2 + shoulderGap / 2 },
            axis: { x: 0, y: 0, z: 1 },
            otherEntityID: rightUpperArmID,
            otherPivot: { x: 0, y: 0, z: -upperArmLength / 2 - shoulderGap / 2 },
            otherAxis: { x: 0, y: 0, z: 1 },
            swingSpan1: Math.PI / 2,
            swingSpan2: Math.PI / 2,
            twistSpan: 0,
            tag: "ragdoll right shoulder joint"
        });

        //
        // left upper arm
        //

        var leftUpperArmID = Entities.addEntity({
            name: "ragdoll left arm",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: upperArmThickness, y: upperArmThickness, z: upperArmLength },
            position: Vec3.sum(pos, { x: 0,
                                      y: bodyHeight / 2 + upperArmThickness / 2,
                                      z: -bodyWidth / 2 - shoulderGap - upperArmLength / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("cone-twist", bodyID, {
            pivot: { x: 0, y: bodyHeight / 2 + upperArmThickness / 2, z: -bodyWidth / 2 - shoulderGap / 2 },
            axis: { x: 0, y: 0, z: -1 },
            otherEntityID: leftUpperArmID,
            otherPivot: { x: 0, y: 0, z: upperArmLength / 2 + shoulderGap / 2 },
            otherAxis: { x: 0, y: 0, z: -1 },
            swingSpan1: Math.PI / 2,
            swingSpan2: Math.PI / 2,
            twistSpan: 0,
            tag: "ragdoll left shoulder joint"
        });

        //
        // right lower arm
        //

        var rightLowerArmID = Entities.addEntity({
            name: "ragdoll right lower arm",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: lowerArmThickness, y: lowerArmThickness, z: lowerArmLength },
            position: Vec3.sum(pos, { x: 0,
                                      y: bodyHeight / 2 - upperArmThickness / 2,
                                      z: bodyWidth / 2 + shoulderGap + upperArmLength + elbowGap + lowerArmLength / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -1, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", rightLowerArmID, {
            pivot: { x: 0, y: 0, z: -lowerArmLength / 2 - elbowGap / 2 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: rightUpperArmID,
            otherPivot: { x: 0, y: 0, z: upperArmLength / 2 + elbowGap / 2 },
            otherAxis: { x: 0, y: 1, z: 0 },
            low: Math.PI / -2,
            high: 0,
            tag: "ragdoll right elbow joint"
        });

        //
        // left lower arm
        //

        var leftLowerArmID = Entities.addEntity({
            name: "ragdoll left lower arm",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: lowerArmThickness, y: lowerArmThickness, z: lowerArmLength },
            position: Vec3.sum(pos, { x: 0,
                                      y: bodyHeight / 2 - upperArmThickness / 2,
                                      z: -bodyWidth / 2 - shoulderGap - upperArmLength - elbowGap - lowerArmLength / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -1, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", leftLowerArmID, {
            pivot: { x: 0, y: 0, z: lowerArmLength / 2 + elbowGap / 2 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: leftUpperArmID,
            otherPivot: { x: 0, y: 0, z: -upperArmLength / 2 - elbowGap / 2 },
            otherAxis: { x: 0, y: 1, z: 0 },
            low: 0,
            high: Math.PI / 2,
            tag: "ragdoll left elbow joint"
        });

        //
        // right leg
        //

        var rightLegID = Entities.addEntity({
            name: "ragdoll right arm",
            type: "Box",
            color: { blue: 20, green: 200, red: 20 },
            dimensions: { x: legThickness, y: legLength, z: legThickness },
            position: Vec3.sum(pos, { x: 0, y: -bodyHeight / 2 - hipGap - legLength / 2, z: bodyWidth / 2 - legThickness / 2 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("cone-twist", rightLegID, {
            pivot: { x: 0, y: legLength / 2 + hipGap / 2, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: bodyID,
            otherPivot: { x: 0, y: -bodyHeight / 2 - hipGap / 2, z: bodyWidth / 2 - legThickness / 2 },
            otherAxis: Vec3.normalize({ x: -1, y: 1, z: 0 }),
            swingSpan1: Math.PI / 4,
            swingSpan2: Math.PI / 4,
            twistSpan: 0,
            tag: "ragdoll right hip joint"
        });

        //
        // left leg
        //

        var leftLegID = Entities.addEntity({
            name: "ragdoll left arm",
            type: "Box",
            color: { blue: 20, green: 200, red: 20 },
            dimensions: { x: legThickness, y: legLength, z: legThickness },
            position: Vec3.sum(pos, { x: 0, y: -bodyHeight / 2 - hipGap - legLength / 2, z: -bodyWidth / 2 + legThickness / 2 }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: 0, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("cone-twist", leftLegID, {
            pivot: { x: 0, y: legLength / 2 + hipGap / 2, z: 0 },
            axis: { x: 0, y: 1, z: 0 },
            otherEntityID: bodyID,
            otherPivot: { x: 0, y: -bodyHeight / 2 - hipGap / 2, z: -bodyWidth / 2 + legThickness / 2 },
            otherAxis: Vec3.normalize({ x: -1, y: 1, z: 0 }),
            swingSpan1: Math.PI / 4,
            swingSpan2: Math.PI / 4,
            twistSpan: 0,
            tag: "ragdoll left hip joint"
        });

        //
        // right shin
        //

        var rightShinID = Entities.addEntity({
            name: "ragdoll right shin",
            type: "Box",
            color: { blue: 20, green: 200, red: 20 },
            dimensions: { x: shinThickness, y: shinLength, z: shinThickness },
            position: Vec3.sum(pos, { x: 0,
                                      y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength / 2,
                                      z: bodyWidth / 2 - legThickness / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -2, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", rightShinID, {
            pivot: { x: 0, y: shinLength / 2 + kneeGap / 2, z: 0 },
            axis: { x: 0, y: 0, z: 1 },
            otherEntityID: rightLegID,
            otherPivot: { x: 0, y: -legLength / 2 - kneeGap / 2, z: 0 },
            otherAxis: { x: 0, y: 0, z: 1 },
            low: 0,
            high: Math.PI / 2,
            tag: "ragdoll right knee joint"
        });


        //
        // left shin
        //

        var leftShinID = Entities.addEntity({
            name: "ragdoll left shin",
            type: "Box",
            color: { blue: 20, green: 200, red: 20 },
            dimensions: { x: shinThickness, y: shinLength, z: shinThickness },
            position: Vec3.sum(pos, { x: 0,
                                      y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength / 2,
                                      z: -bodyWidth / 2 + legThickness / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -2, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", leftShinID, {
            pivot: { x: 0, y: shinLength / 2 + kneeGap / 2, z: 0 },
            axis: { x: 0, y: 0, z: 1 },
            otherEntityID: leftLegID,
            otherPivot: { x: 0, y: -legLength / 2 - kneeGap / 2, z: 0 },
            otherAxis: { x: 0, y: 0, z: 1 },
            low: 0,
            high: Math.PI / 2,
            tag: "ragdoll left knee joint"
        });

        //
        // right foot
        //

        var rightFootID = Entities.addEntity({
            name: "ragdoll right foot",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: footLength, y: footThickness, z: footWidth },
            position: Vec3.sum(pos, { x: -shinThickness / 2 + footLength / 2,
                                      y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength - ankleGap - footThickness / 2,
                                      z: bodyWidth / 2 - legThickness / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -5, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", rightFootID, {
            pivot: { x: -footLength / 2 + shinThickness / 2, y: ankleGap / 2, z: 0 },
            axis: { x: 0, y: 0, z: 1 },
            otherEntityID: rightShinID,
            otherPivot: { x: 0, y: -shinLength / 2 - ankleGap / 2, z: 0 },
            otherAxis: { x: 0, y: 0, z: 1 },
            low: ankleMin,
            high: ankleMax,
            tag: "ragdoll right ankle joint"
        });

        //
        // left foot
        //

        var leftFootID = Entities.addEntity({
            name: "ragdoll left foot",
            type: "Box",
            color: { blue: 128, green: 100, red: 20 },
            dimensions: { x: footLength, y: footThickness, z: footWidth },
            position: Vec3.sum(pos, { x: -shinThickness / 2 + footLength / 2,
                                      y: -bodyHeight / 2 - hipGap - legLength - kneeGap - shinLength - ankleGap - footThickness / 2,
                                      z: bodyWidth / 2 - legThickness / 2
                                    }),
            dynamic: true,
            collisionless: false,
            gravity: { x: 0, y: -5, z: 0 },
            lifetime: lifetime,
            userData: "{ \"grabbableKey\": { \"grabbable\": true, \"kinematic\": false } }"
        });

        Entities.addAction("hinge", leftFootID, {
            pivot: { x: -footLength / 2 + shinThickness / 2, y: ankleGap / 2, z: 0 },
            axis: { x: 0, y: 0, z: 1 },
            otherEntityID: leftShinID,
            otherPivot: { x: 0, y: -shinLength / 2 - ankleGap / 2, z: 0 },
            otherAxis: { x: 0, y: 0, z: 1 },
            low: ankleMin,
            high: ankleMax,
            tag: "ragdoll left ankle joint"
        });

    }

    function onWebEventReceived(eventString) {
        if (typeof eventString === "string") {
            var event;
            try {
                event = JSON.parse(eventString);
            } catch(e) {
                return;
            }

            if (event["dynamics-tests-command"]) {
                var commandToFunctionMap = {
                    "cone-twist-and-tractor-lever-test": coneTwistAndTractorLeverTest,
                    "door-vs-world-test": doorVSWorldTest,
                    "hinge-chain-test": hingeChainTest,
                    "slider-vs-world-test": sliderVSWorldTest,
                    "slider-chain-test": sliderChainTest,
                    "ball-socket-between-test": ballSocketBetweenTest,
                    "ball-socket-coincident-test": ballSocketCoincidentTest,
                    "ragdoll-test": ragdollTest
                };

                var cmd = event["dynamics-tests-command"];
                if (commandToFunctionMap.hasOwnProperty(cmd)) {
                    var func = commandToFunctionMap[cmd];
                    func(event);
                }
            }
        }
    }


    var onDynamicsTestsScreen = false;
    var shouldActivateButton = false;

    function onClicked() {
        if (onDynamicsTestsScreen) {
            tablet.gotoHomeScreen();
        } else {
            shouldActivateButton = true;
            tablet.gotoWebScreen(DYNAMICS_TESTS_URL +
                                 "?lifetime=" + DEFAULT_LIFETIME.toString()
                                );
            onDynamicsTestsScreen = true;
        }
    }

    function onScreenChanged() {
        // for toolbar mode: change button to active when window is first openend, false otherwise.
        button.editProperties({isActive: shouldActivateButton});
        shouldActivateButton = false;
        onDynamicsTestsScreen = shouldActivateButton;
    }

    function cleanup() {
        button.clicked.disconnect(onClicked);
        tablet.removeButton(button);
    }

    button.clicked.connect(onClicked);
    tablet.webEventReceived.connect(onWebEventReceived);
    tablet.screenChanged.connect(onScreenChanged);
    Script.scriptEnding.connect(cleanup);
}()); // END LOCAL_SCOPE