sebavan 6 năm trước cách đây
mục cha
commit
ab14a12897

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 3664 - 751
Playground/babylon.d.txt


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 770 - 751
dist/preview release/babylon.d.ts


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/babylon.js


+ 73 - 17
dist/preview release/babylon.max.js

@@ -27854,12 +27854,13 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask) {
+        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask, onAnimationLoop) {
             if (weight === void 0) { weight = 1.0; }
             if (speedRatio === void 0) { speedRatio = 1.0; }
-            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask);
+            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop);
             returnedAnimatable.weight = weight;
             return returnedAnimatable;
         };
@@ -27873,10 +27874,11 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
-         * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param targetMask defines if the target should be animate if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             if (from > to && speedRatio > 0) {
@@ -27886,7 +27888,7 @@ var BABYLON;
                 this.stopAnimation(target, undefined, targetMask);
             }
             if (!animatable) {
-                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd);
+                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop);
             }
             var shouldRunTargetAnimations = targetMask ? targetMask(target) : true;
             // Local animations
@@ -27897,7 +27899,7 @@ var BABYLON;
             if (target.getAnimatables) {
                 var animatables = target.getAnimatables();
                 for (var index = 0; index < animatables.length; index++) {
-                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask);
+                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop);
                 }
             }
             animatable.reset();
@@ -27915,9 +27917,10 @@ var BABYLON;
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             var children = target.getDescendants(directDescendantsOnly);
@@ -27938,13 +27941,14 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             if (speedRatio === undefined) {
                 speedRatio = 1.0;
             }
-            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations);
+            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop);
             return animatable;
         };
         /**
@@ -27957,15 +27961,16 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of animatables created for all nodes
          */
-        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             var children = target.getDescendants(directDescendantsOnly);
             var result = [];
-            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd));
+            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             for (var _i = 0, children_2 = children; _i < children_2.length; _i++) {
                 var child = children_2[_i];
-                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd));
+                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             }
             return result;
         };
@@ -55208,10 +55213,14 @@ var BABYLON;
             this._to = -Number.MAX_VALUE;
             this._speedRatio = 1;
             /**
-             * This observable will notify when one animation have ended.
+             * This observable will notify when one animation have ended
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
             /**
+             * Observer raised when one animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
+            /**
              * This observable will notify when all animations have ended.
              */
             this.onAnimationGroupEndObservable = new BABYLON.Observable();
@@ -55398,6 +55407,9 @@ var BABYLON;
                     _this.onAnimationEndObservable.notifyObservers(targetedAnimation);
                     _this._checkAnimationGroupEnded(animatable);
                 };
+                animatable.onAnimationLoop = function () {
+                    _this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
+                };
                 this_1._animatables.push(animatable);
             };
             var this_1 = this;
@@ -55549,6 +55561,11 @@ var BABYLON;
             if (index > -1) {
                 this._scene.animationGroups.splice(index, 1);
             }
+            this.onAnimationEndObservable.clear();
+            this.onAnimationGroupEndObservable.clear();
+            this.onAnimationGroupPauseObservable.clear();
+            this.onAnimationGroupPlayObservable.clear();
+            this.onAnimationLoopObservable.clear();
         };
         AnimationGroup.prototype._checkAnimationGroupEnded = function (animatable) {
             // animatable should be taken out of the array
@@ -55978,9 +55995,10 @@ var BABYLON;
          * @param loop defines if the current animation must loop
          * @param speedRatio defines the current speed ratio
          * @param weight defines the weight of the animation (default is -1 so no weight)
+         * @param onLoop optional callback called when animation loops
          * @returns a boolean indicating if the animation is running
          */
-        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight) {
+        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight, onLoop) {
             if (weight === void 0) { weight = -1.0; }
             var targetPropertyPath = this._animation.targetPropertyPath;
             if (!targetPropertyPath || targetPropertyPath.length < 1) {
@@ -56104,6 +56122,9 @@ var BABYLON;
             var events = this._events;
             if (range > 0 && this.currentFrame > currentFrame ||
                 range < 0 && this.currentFrame < currentFrame) {
+                if (onLoop) {
+                    onLoop();
+                }
                 // Need to reset animation events
                 for (var index = 0; index < events.length; index++) {
                     if (!events[index].onlyOnce) {
@@ -56161,6 +56182,7 @@ var BABYLON;
          * @param speedRatio defines the factor to apply to animation speed (default is 1)
          * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
          * @param animations defines a group of animation to add to the new Animatable
+         * @param onAnimationLoop defines a callback to call when animation loops
          */
         function Animatable(scene, 
         /** defines the target object */
@@ -56172,7 +56194,9 @@ var BABYLON;
         /** defines if the animation must loop (default is false)  */
         loopAnimation, speedRatio, 
         /** defines a callback to call when animation ends if it is not looping */
-        onAnimationEnd, animations) {
+        onAnimationEnd, animations, 
+        /** defines a callback to call when animation loops */
+        onAnimationLoop) {
             if (fromFrame === void 0) { fromFrame = 0; }
             if (toFrame === void 0) { toFrame = 100; }
             if (loopAnimation === void 0) { loopAnimation = false; }
@@ -56182,6 +56206,7 @@ var BABYLON;
             this.toFrame = toFrame;
             this.loopAnimation = loopAnimation;
             this.onAnimationEnd = onAnimationEnd;
+            this.onAnimationLoop = onAnimationLoop;
             this._localDelayOffset = null;
             this._pausedDelay = null;
             this._runtimeAnimations = new Array();
@@ -56201,6 +56226,10 @@ var BABYLON;
              * Observer raised when the animation ends
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
+            /**
+             * Observer raised when the animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
             this._scene = scene;
             if (animations) {
                 this.appendAnimations(target, animations);
@@ -56459,6 +56488,7 @@ var BABYLON;
         };
         /** @hidden */
         Animatable.prototype._animate = function (delay) {
+            var _this = this;
             if (this._paused) {
                 this.animationStarted = false;
                 if (this._pausedDelay === null) {
@@ -56483,7 +56513,12 @@ var BABYLON;
             var index;
             for (index = 0; index < runtimeAnimations.length; index++) {
                 var animation = runtimeAnimations[index];
-                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight);
+                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight, function () {
+                    _this.onAnimationLoopObservable.notifyObservers(_this);
+                    if (_this.onAnimationLoop) {
+                        _this.onAnimationLoop();
+                    }
+                });
                 running = running || isRunning;
             }
             this.animationStarted = running;
@@ -56500,6 +56535,8 @@ var BABYLON;
                 this._raiseOnAnimationEnd();
                 if (this.disposeOnEnd) {
                     this.onAnimationEnd = null;
+                    this.onAnimationLoop = null;
+                    this.onAnimationLoopObservable.clear();
                     this.onAnimationEndObservable.clear();
                 }
             }
@@ -100188,6 +100225,25 @@ var BABYLON;
             }
             var joint;
             switch (impostorJoint.joint.type) {
+                case BABYLON.PhysicsJoint.DistanceJoint:
+                    var distance = jointData.maxDistance;
+                    if (distance) {
+                        jointData.mainPivot = new BABYLON.Vector3(0, -distance / 2, 0);
+                        jointData.connectedPivot = new BABYLON.Vector3(0, distance / 2, 0);
+                    }
+                    joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
+                    break;
+                case BABYLON.PhysicsJoint.HingeJoint:
+                    if (!jointData.mainAxis) {
+                        jointData.mainAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    if (!jointData.connectedAxis) {
+                        jointData.connectedAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    var mainAxis = new Ammo.btVector3(jointData.mainAxis.x, jointData.mainAxis.y, jointData.mainAxis.z);
+                    var connectedAxis = new Ammo.btVector3(jointData.connectedAxis.x, jointData.connectedAxis.y, jointData.connectedAxis.z);
+                    joint = new Ammo.btHingeConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z), mainAxis, connectedAxis);
+                    break;
                 case BABYLON.PhysicsJoint.BallAndSocketJoint:
                     joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
                     break;
@@ -100510,7 +100566,7 @@ var BABYLON;
          * @param motorIndex index of the motor
          */
         AmmoJSPlugin.prototype.setMotor = function (joint, speed, maxForce, motorIndex) {
-            BABYLON.Tools.Warn("setMotor is not currently supported by the Ammo physics plugin");
+            joint.physicsJoint.enableAngularMotor(true, speed, maxForce);
         };
         /**
          * Sets the motors limit

+ 73 - 17
dist/preview release/babylon.no-module.max.js

@@ -27821,12 +27821,13 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask) {
+        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask, onAnimationLoop) {
             if (weight === void 0) { weight = 1.0; }
             if (speedRatio === void 0) { speedRatio = 1.0; }
-            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask);
+            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop);
             returnedAnimatable.weight = weight;
             return returnedAnimatable;
         };
@@ -27840,10 +27841,11 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
-         * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param targetMask defines if the target should be animate if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             if (from > to && speedRatio > 0) {
@@ -27853,7 +27855,7 @@ var BABYLON;
                 this.stopAnimation(target, undefined, targetMask);
             }
             if (!animatable) {
-                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd);
+                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop);
             }
             var shouldRunTargetAnimations = targetMask ? targetMask(target) : true;
             // Local animations
@@ -27864,7 +27866,7 @@ var BABYLON;
             if (target.getAnimatables) {
                 var animatables = target.getAnimatables();
                 for (var index = 0; index < animatables.length; index++) {
-                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask);
+                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop);
                 }
             }
             animatable.reset();
@@ -27882,9 +27884,10 @@ var BABYLON;
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             var children = target.getDescendants(directDescendantsOnly);
@@ -27905,13 +27908,14 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             if (speedRatio === undefined) {
                 speedRatio = 1.0;
             }
-            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations);
+            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop);
             return animatable;
         };
         /**
@@ -27924,15 +27928,16 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of animatables created for all nodes
          */
-        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             var children = target.getDescendants(directDescendantsOnly);
             var result = [];
-            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd));
+            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             for (var _i = 0, children_2 = children; _i < children_2.length; _i++) {
                 var child = children_2[_i];
-                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd));
+                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             }
             return result;
         };
@@ -55175,10 +55180,14 @@ var BABYLON;
             this._to = -Number.MAX_VALUE;
             this._speedRatio = 1;
             /**
-             * This observable will notify when one animation have ended.
+             * This observable will notify when one animation have ended
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
             /**
+             * Observer raised when one animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
+            /**
              * This observable will notify when all animations have ended.
              */
             this.onAnimationGroupEndObservable = new BABYLON.Observable();
@@ -55365,6 +55374,9 @@ var BABYLON;
                     _this.onAnimationEndObservable.notifyObservers(targetedAnimation);
                     _this._checkAnimationGroupEnded(animatable);
                 };
+                animatable.onAnimationLoop = function () {
+                    _this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
+                };
                 this_1._animatables.push(animatable);
             };
             var this_1 = this;
@@ -55516,6 +55528,11 @@ var BABYLON;
             if (index > -1) {
                 this._scene.animationGroups.splice(index, 1);
             }
+            this.onAnimationEndObservable.clear();
+            this.onAnimationGroupEndObservable.clear();
+            this.onAnimationGroupPauseObservable.clear();
+            this.onAnimationGroupPlayObservable.clear();
+            this.onAnimationLoopObservable.clear();
         };
         AnimationGroup.prototype._checkAnimationGroupEnded = function (animatable) {
             // animatable should be taken out of the array
@@ -55945,9 +55962,10 @@ var BABYLON;
          * @param loop defines if the current animation must loop
          * @param speedRatio defines the current speed ratio
          * @param weight defines the weight of the animation (default is -1 so no weight)
+         * @param onLoop optional callback called when animation loops
          * @returns a boolean indicating if the animation is running
          */
-        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight) {
+        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight, onLoop) {
             if (weight === void 0) { weight = -1.0; }
             var targetPropertyPath = this._animation.targetPropertyPath;
             if (!targetPropertyPath || targetPropertyPath.length < 1) {
@@ -56071,6 +56089,9 @@ var BABYLON;
             var events = this._events;
             if (range > 0 && this.currentFrame > currentFrame ||
                 range < 0 && this.currentFrame < currentFrame) {
+                if (onLoop) {
+                    onLoop();
+                }
                 // Need to reset animation events
                 for (var index = 0; index < events.length; index++) {
                     if (!events[index].onlyOnce) {
@@ -56128,6 +56149,7 @@ var BABYLON;
          * @param speedRatio defines the factor to apply to animation speed (default is 1)
          * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
          * @param animations defines a group of animation to add to the new Animatable
+         * @param onAnimationLoop defines a callback to call when animation loops
          */
         function Animatable(scene, 
         /** defines the target object */
@@ -56139,7 +56161,9 @@ var BABYLON;
         /** defines if the animation must loop (default is false)  */
         loopAnimation, speedRatio, 
         /** defines a callback to call when animation ends if it is not looping */
-        onAnimationEnd, animations) {
+        onAnimationEnd, animations, 
+        /** defines a callback to call when animation loops */
+        onAnimationLoop) {
             if (fromFrame === void 0) { fromFrame = 0; }
             if (toFrame === void 0) { toFrame = 100; }
             if (loopAnimation === void 0) { loopAnimation = false; }
@@ -56149,6 +56173,7 @@ var BABYLON;
             this.toFrame = toFrame;
             this.loopAnimation = loopAnimation;
             this.onAnimationEnd = onAnimationEnd;
+            this.onAnimationLoop = onAnimationLoop;
             this._localDelayOffset = null;
             this._pausedDelay = null;
             this._runtimeAnimations = new Array();
@@ -56168,6 +56193,10 @@ var BABYLON;
              * Observer raised when the animation ends
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
+            /**
+             * Observer raised when the animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
             this._scene = scene;
             if (animations) {
                 this.appendAnimations(target, animations);
@@ -56426,6 +56455,7 @@ var BABYLON;
         };
         /** @hidden */
         Animatable.prototype._animate = function (delay) {
+            var _this = this;
             if (this._paused) {
                 this.animationStarted = false;
                 if (this._pausedDelay === null) {
@@ -56450,7 +56480,12 @@ var BABYLON;
             var index;
             for (index = 0; index < runtimeAnimations.length; index++) {
                 var animation = runtimeAnimations[index];
-                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight);
+                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight, function () {
+                    _this.onAnimationLoopObservable.notifyObservers(_this);
+                    if (_this.onAnimationLoop) {
+                        _this.onAnimationLoop();
+                    }
+                });
                 running = running || isRunning;
             }
             this.animationStarted = running;
@@ -56467,6 +56502,8 @@ var BABYLON;
                 this._raiseOnAnimationEnd();
                 if (this.disposeOnEnd) {
                     this.onAnimationEnd = null;
+                    this.onAnimationLoop = null;
+                    this.onAnimationLoopObservable.clear();
                     this.onAnimationEndObservable.clear();
                 }
             }
@@ -100155,6 +100192,25 @@ var BABYLON;
             }
             var joint;
             switch (impostorJoint.joint.type) {
+                case BABYLON.PhysicsJoint.DistanceJoint:
+                    var distance = jointData.maxDistance;
+                    if (distance) {
+                        jointData.mainPivot = new BABYLON.Vector3(0, -distance / 2, 0);
+                        jointData.connectedPivot = new BABYLON.Vector3(0, distance / 2, 0);
+                    }
+                    joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
+                    break;
+                case BABYLON.PhysicsJoint.HingeJoint:
+                    if (!jointData.mainAxis) {
+                        jointData.mainAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    if (!jointData.connectedAxis) {
+                        jointData.connectedAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    var mainAxis = new Ammo.btVector3(jointData.mainAxis.x, jointData.mainAxis.y, jointData.mainAxis.z);
+                    var connectedAxis = new Ammo.btVector3(jointData.connectedAxis.x, jointData.connectedAxis.y, jointData.connectedAxis.z);
+                    joint = new Ammo.btHingeConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z), mainAxis, connectedAxis);
+                    break;
                 case BABYLON.PhysicsJoint.BallAndSocketJoint:
                     joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
                     break;
@@ -100477,7 +100533,7 @@ var BABYLON;
          * @param motorIndex index of the motor
          */
         AmmoJSPlugin.prototype.setMotor = function (joint, speed, maxForce, motorIndex) {
-            BABYLON.Tools.Warn("setMotor is not currently supported by the Ammo physics plugin");
+            joint.physicsJoint.enableAngularMotor(true, speed, maxForce);
         };
         /**
          * Sets the motors limit

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/babylon.worker.js


+ 73 - 17
dist/preview release/es6.js

@@ -27821,12 +27821,13 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask) {
+        Scene.prototype.beginWeightedAnimation = function (target, from, to, weight, loop, speedRatio, onAnimationEnd, animatable, targetMask, onAnimationLoop) {
             if (weight === void 0) { weight = 1.0; }
             if (speedRatio === void 0) { speedRatio = 1.0; }
-            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask);
+            var returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop);
             returnedAnimatable.weight = weight;
             return returnedAnimatable;
         };
@@ -27840,10 +27841,11 @@ var BABYLON;
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
-         * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param targetMask defines if the target should be animate if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginAnimation = function (target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             if (from > to && speedRatio > 0) {
@@ -27853,7 +27855,7 @@ var BABYLON;
                 this.stopAnimation(target, undefined, targetMask);
             }
             if (!animatable) {
-                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd);
+                animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop);
             }
             var shouldRunTargetAnimations = targetMask ? targetMask(target) : true;
             // Local animations
@@ -27864,7 +27866,7 @@ var BABYLON;
             if (target.getAnimatables) {
                 var animatables = target.getAnimatables();
                 for (var index = 0; index < animatables.length; index++) {
-                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask);
+                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop);
                 }
             }
             animatable.reset();
@@ -27882,9 +27884,10 @@ var BABYLON;
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask) {
+        Scene.prototype.beginHierarchyAnimation = function (target, directDescendantsOnly, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop) {
             if (speedRatio === void 0) { speedRatio = 1.0; }
             if (stopCurrent === void 0) { stopCurrent = true; }
             var children = target.getDescendants(directDescendantsOnly);
@@ -27905,13 +27908,14 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectAnimation = function (target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             if (speedRatio === undefined) {
                 speedRatio = 1.0;
             }
-            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations);
+            var animatable = new BABYLON.Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop);
             return animatable;
         };
         /**
@@ -27924,15 +27928,16 @@ var BABYLON;
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of animatables created for all nodes
          */
-        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd) {
+        Scene.prototype.beginDirectHierarchyAnimation = function (target, directDescendantsOnly, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop) {
             var children = target.getDescendants(directDescendantsOnly);
             var result = [];
-            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd));
+            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             for (var _i = 0, children_2 = children; _i < children_2.length; _i++) {
                 var child = children_2[_i];
-                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd));
+                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             }
             return result;
         };
@@ -55175,10 +55180,14 @@ var BABYLON;
             this._to = -Number.MAX_VALUE;
             this._speedRatio = 1;
             /**
-             * This observable will notify when one animation have ended.
+             * This observable will notify when one animation have ended
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
             /**
+             * Observer raised when one animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
+            /**
              * This observable will notify when all animations have ended.
              */
             this.onAnimationGroupEndObservable = new BABYLON.Observable();
@@ -55365,6 +55374,9 @@ var BABYLON;
                     _this.onAnimationEndObservable.notifyObservers(targetedAnimation);
                     _this._checkAnimationGroupEnded(animatable);
                 };
+                animatable.onAnimationLoop = function () {
+                    _this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
+                };
                 this_1._animatables.push(animatable);
             };
             var this_1 = this;
@@ -55516,6 +55528,11 @@ var BABYLON;
             if (index > -1) {
                 this._scene.animationGroups.splice(index, 1);
             }
+            this.onAnimationEndObservable.clear();
+            this.onAnimationGroupEndObservable.clear();
+            this.onAnimationGroupPauseObservable.clear();
+            this.onAnimationGroupPlayObservable.clear();
+            this.onAnimationLoopObservable.clear();
         };
         AnimationGroup.prototype._checkAnimationGroupEnded = function (animatable) {
             // animatable should be taken out of the array
@@ -55945,9 +55962,10 @@ var BABYLON;
          * @param loop defines if the current animation must loop
          * @param speedRatio defines the current speed ratio
          * @param weight defines the weight of the animation (default is -1 so no weight)
+         * @param onLoop optional callback called when animation loops
          * @returns a boolean indicating if the animation is running
          */
-        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight) {
+        RuntimeAnimation.prototype.animate = function (delay, from, to, loop, speedRatio, weight, onLoop) {
             if (weight === void 0) { weight = -1.0; }
             var targetPropertyPath = this._animation.targetPropertyPath;
             if (!targetPropertyPath || targetPropertyPath.length < 1) {
@@ -56071,6 +56089,9 @@ var BABYLON;
             var events = this._events;
             if (range > 0 && this.currentFrame > currentFrame ||
                 range < 0 && this.currentFrame < currentFrame) {
+                if (onLoop) {
+                    onLoop();
+                }
                 // Need to reset animation events
                 for (var index = 0; index < events.length; index++) {
                     if (!events[index].onlyOnce) {
@@ -56128,6 +56149,7 @@ var BABYLON;
          * @param speedRatio defines the factor to apply to animation speed (default is 1)
          * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
          * @param animations defines a group of animation to add to the new Animatable
+         * @param onAnimationLoop defines a callback to call when animation loops
          */
         function Animatable(scene, 
         /** defines the target object */
@@ -56139,7 +56161,9 @@ var BABYLON;
         /** defines if the animation must loop (default is false)  */
         loopAnimation, speedRatio, 
         /** defines a callback to call when animation ends if it is not looping */
-        onAnimationEnd, animations) {
+        onAnimationEnd, animations, 
+        /** defines a callback to call when animation loops */
+        onAnimationLoop) {
             if (fromFrame === void 0) { fromFrame = 0; }
             if (toFrame === void 0) { toFrame = 100; }
             if (loopAnimation === void 0) { loopAnimation = false; }
@@ -56149,6 +56173,7 @@ var BABYLON;
             this.toFrame = toFrame;
             this.loopAnimation = loopAnimation;
             this.onAnimationEnd = onAnimationEnd;
+            this.onAnimationLoop = onAnimationLoop;
             this._localDelayOffset = null;
             this._pausedDelay = null;
             this._runtimeAnimations = new Array();
@@ -56168,6 +56193,10 @@ var BABYLON;
              * Observer raised when the animation ends
              */
             this.onAnimationEndObservable = new BABYLON.Observable();
+            /**
+             * Observer raised when the animation loops
+             */
+            this.onAnimationLoopObservable = new BABYLON.Observable();
             this._scene = scene;
             if (animations) {
                 this.appendAnimations(target, animations);
@@ -56426,6 +56455,7 @@ var BABYLON;
         };
         /** @hidden */
         Animatable.prototype._animate = function (delay) {
+            var _this = this;
             if (this._paused) {
                 this.animationStarted = false;
                 if (this._pausedDelay === null) {
@@ -56450,7 +56480,12 @@ var BABYLON;
             var index;
             for (index = 0; index < runtimeAnimations.length; index++) {
                 var animation = runtimeAnimations[index];
-                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight);
+                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight, function () {
+                    _this.onAnimationLoopObservable.notifyObservers(_this);
+                    if (_this.onAnimationLoop) {
+                        _this.onAnimationLoop();
+                    }
+                });
                 running = running || isRunning;
             }
             this.animationStarted = running;
@@ -56467,6 +56502,8 @@ var BABYLON;
                 this._raiseOnAnimationEnd();
                 if (this.disposeOnEnd) {
                     this.onAnimationEnd = null;
+                    this.onAnimationLoop = null;
+                    this.onAnimationLoopObservable.clear();
                     this.onAnimationEndObservable.clear();
                 }
             }
@@ -100155,6 +100192,25 @@ var BABYLON;
             }
             var joint;
             switch (impostorJoint.joint.type) {
+                case BABYLON.PhysicsJoint.DistanceJoint:
+                    var distance = jointData.maxDistance;
+                    if (distance) {
+                        jointData.mainPivot = new BABYLON.Vector3(0, -distance / 2, 0);
+                        jointData.connectedPivot = new BABYLON.Vector3(0, distance / 2, 0);
+                    }
+                    joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
+                    break;
+                case BABYLON.PhysicsJoint.HingeJoint:
+                    if (!jointData.mainAxis) {
+                        jointData.mainAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    if (!jointData.connectedAxis) {
+                        jointData.connectedAxis = new BABYLON.Vector3(0, 0, 0);
+                    }
+                    var mainAxis = new Ammo.btVector3(jointData.mainAxis.x, jointData.mainAxis.y, jointData.mainAxis.z);
+                    var connectedAxis = new Ammo.btVector3(jointData.connectedAxis.x, jointData.connectedAxis.y, jointData.connectedAxis.z);
+                    joint = new Ammo.btHingeConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z), mainAxis, connectedAxis);
+                    break;
                 case BABYLON.PhysicsJoint.BallAndSocketJoint:
                     joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
                     break;
@@ -100477,7 +100533,7 @@ var BABYLON;
          * @param motorIndex index of the motor
          */
         AmmoJSPlugin.prototype.setMotor = function (joint, speed, maxForce, motorIndex) {
-            BABYLON.Tools.Warn("setMotor is not currently supported by the Ammo physics plugin");
+            joint.physicsJoint.enableAngularMotor(true, speed, maxForce);
         };
         /**
          * Sets the motors limit

+ 6 - 0
dist/preview release/gui/babylon.gui.d.ts

@@ -908,6 +908,8 @@ declare module BABYLON.GUI {
             /** @hidden */
             protected _isDirty: boolean;
             /** @hidden */
+            protected _wasDirty: boolean;
+            /** @hidden */
             _tempParentMeasure: Measure;
             /** @hidden */
             protected _cachedParentMeasure: Measure;
@@ -945,6 +947,10 @@ declare module BABYLON.GUI {
             isFocusInvisible: boolean;
             /** Gets or sets a boolean indicating if the children are clipped to the current control bounds */
             clipChildren: boolean;
+            /**
+                * Gets or sets a boolean indicating that the current control should cache its rendering (useful when the control does not change often)
+                */
+            useBitmapCache: boolean;
             /** Gets or sets a value indicating the offset to apply on X axis to render the shadow */
             shadowOffsetX: number;
             /** Gets or sets a value indicating the offset to apply on Y axis to render the shadow */

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/gui/babylon.gui.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/gui/babylon.gui.min.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/gui/babylon.gui.min.js.map


+ 13 - 1
dist/preview release/gui/babylon.gui.module.d.ts

@@ -836,7 +836,7 @@ declare module 'babylonjs-gui/2D/controls/checkbox' {
 declare module 'babylonjs-gui/2D/controls/colorpicker' {
     import { Control } from "babylonjs-gui/2D/controls/control";
     import { Color3, Observable, Vector2 } from "babylonjs";
-    import { Measure } from "2D";
+    import { Measure } from "babylonjs-gui/2D/measure";
     /** Class used to create color pickers */
     export class ColorPicker extends Control {
             name?: string | undefined;
@@ -1015,6 +1015,8 @@ declare module 'babylonjs-gui/2D/controls/control' {
             /** @hidden */
             protected _isDirty: boolean;
             /** @hidden */
+            protected _wasDirty: boolean;
+            /** @hidden */
             _tempParentMeasure: Measure;
             /** @hidden */
             protected _cachedParentMeasure: Measure;
@@ -1052,6 +1054,10 @@ declare module 'babylonjs-gui/2D/controls/control' {
             isFocusInvisible: boolean;
             /** Gets or sets a boolean indicating if the children are clipped to the current control bounds */
             clipChildren: boolean;
+            /**
+                * Gets or sets a boolean indicating that the current control should cache its rendering (useful when the control does not change often)
+                */
+            useBitmapCache: boolean;
             /** Gets or sets a value indicating the offset to apply on X axis to render the shadow */
             shadowOffsetX: number;
             /** Gets or sets a value indicating the offset to apply on Y axis to render the shadow */
@@ -4078,6 +4084,8 @@ declare module BABYLON.GUI {
             /** @hidden */
             protected _isDirty: boolean;
             /** @hidden */
+            protected _wasDirty: boolean;
+            /** @hidden */
             _tempParentMeasure: Measure;
             /** @hidden */
             protected _cachedParentMeasure: Measure;
@@ -4115,6 +4123,10 @@ declare module BABYLON.GUI {
             isFocusInvisible: boolean;
             /** Gets or sets a boolean indicating if the children are clipped to the current control bounds */
             clipChildren: boolean;
+            /**
+                * Gets or sets a boolean indicating that the current control should cache its rendering (useful when the control does not change often)
+                */
+            useBitmapCache: boolean;
             /** Gets or sets a value indicating the offset to apply on X axis to render the shadow */
             shadowOffsetX: number;
             /** Gets or sets a value indicating the offset to apply on Y axis to render the shadow */

+ 0 - 31
dist/preview release/viewer/babylon.viewer.d.ts

@@ -1728,37 +1728,6 @@ declare module BabylonViewer {
     }
 }
 declare module BabylonViewer {
-    export interface ICameraConfiguration {
-        position?: {
-            x: number;
-            y: number;
-            z: number;
-        };
-        rotation?: {
-            x: number;
-            y: number;
-            z: number;
-            w: number;
-        };
-        fov?: number;
-        fovMode?: number;
-        minZ?: number;
-        maxZ?: number;
-        inertia?: number;
-        exposure?: number;
-        pinchPrecision?: number;
-        behaviors?: {
-            [name: string]: boolean | number | ICameraBehaviorConfiguration;
-        };
-        disableCameraControl?: boolean;
-        disableCtrlForPanning?: boolean;
-        disableAutoFocus?: boolean;
-        [propName: string]: any;
-    }
-    export interface ICameraBehaviorConfiguration {
-        type: number;
-        [propName: string]: any;
-    }
 }
 declare module BabylonViewer {
     /**

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/viewer/babylon.viewer.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
dist/preview release/viewer/babylon.viewer.max.js


+ 1 - 31
dist/preview release/viewer/babylon.viewer.module.d.ts

@@ -1872,37 +1872,7 @@ declare module 'babylonjs-viewer/loader/plugins/extendedMaterialLoaderPlugin' {
 }
 
 declare module 'babylonjs-viewer/configuration/interfaces/cameraConfiguration' {
-    export interface ICameraConfiguration {
-        position?: {
-            x: number;
-            y: number;
-            z: number;
-        };
-        rotation?: {
-            x: number;
-            y: number;
-            z: number;
-            w: number;
-        };
-        fov?: number;
-        fovMode?: number;
-        minZ?: number;
-        maxZ?: number;
-        inertia?: number;
-        exposure?: number;
-        pinchPrecision?: number;
-        behaviors?: {
-            [name: string]: boolean | number | ICameraBehaviorConfiguration;
-        };
-        disableCameraControl?: boolean;
-        disableCtrlForPanning?: boolean;
-        disableAutoFocus?: boolean;
-        [propName: string]: any;
-    }
-    export interface ICameraBehaviorConfiguration {
-        type: number;
-        [propName: string]: any;
-    }
+    
 }
 
 declare module 'babylonjs-viewer/configuration/interfaces/colorGradingConfiguration' {

+ 8 - 4
dist/preview release/what's new.md

@@ -4,9 +4,9 @@
 
 - [Inspector v2.0](https://doc.babylonjs.com/features/playground_debuglayer). [Dev log](https://medium.com/@babylonjs/dev-log-creating-the-new-inspector-b15c50900205) ([Deltakosh](https://github.com/deltakosh))
 - Added support for [parallel shader compilation](https://www.khronos.org/registry/webgl/extensions/KHR_parallel_shader_compile/) ([Deltakosh](https://github.com/deltakosh))
-- Added FlyCamera for free navigation in 3D space, with a limited set of settings ([Phuein](https://github.com/phuein)) [@NEED DEMO]
 - Added [Object Based Motion Blur](http://doc.babylonjs.com/how_to/using_motionblurpostprocess) post-process ([julien-moreau](https://github.com/julien-moreau))
-- WebXR ([TrevorDev](https://github.com/TrevorDev)) [@NEED DEMO]
+- Added [support for AmmoJS](https://doc.babylonjs.com/how_to/using_the_physics_engine) as a physics plugin (Composite objects, joints, motors) ([TrevorDev](https://github.com/TrevorDev))
+- Added support for [WebXR](https://doc.babylonjs.com/how_to/webxr) ([TrevorDev](https://github.com/TrevorDev))
   - Add customAnimationFrameRequester to allow sessions to hook into engine's render loop ([TrevorDev](https://github.com/TrevorDev))
   - camera customDefaultRenderTarget to allow cameras to render to a custom render target (eg. xr framebuffer) instead of the canvas ([TrevorDev](https://github.com/TrevorDev))
   - webXR camera which can be updated by a webXRSession ([TrevorDev](https://github.com/TrevorDev))
@@ -16,9 +16,10 @@
   - WebXRInput manage controllers for the XR experience ([TrevorDev](https://github.com/TrevorDev))
   - WebXR camera rotation using parent container ([TrevorDev](https://github.com/TrevorDev))
 - GUI:
+  - Added `control.useBitmapCache` to optimize re-rendering of complex controls by keeping a cached version ([Deltakosh](https://github.com/deltakosh))
   - Added new [ImageBasedSlider](http://doc.babylonjs.com/how_to/gui#imagebasedslider) to let users customize sliders using images ([Deltakosh](https://github.com/deltakosh))
   - Added support for clipboard events to let users perform `cut`, `copy` and `paste` events ([Saket Saurabh](https://github.com/ssaket))
-  - Added new [ScrollViewer](https://doc.babylonjs.com/how_to/scrollviewer) with mouse wheel scrolling for larger containers to be viewed using Sliders ([JohnK](https://github.com/BabylonJSGuide/))
+  - Added new [ScrollViewer](https://doc.babylonjs.com/how_to/scrollviewer) with mouse wheel scrolling for larger containers to be viewed using Sliders ([JohnK](https://github.com/BabylonJSGuide/) / [Deltakosh](https://github.com/deltakosh))
   - Moved to a measure / draw mechanism ([Deltakosh](https://github.com/deltakosh))
 
 ## Updates
@@ -37,6 +38,9 @@
 
 ### Core Engine
 
+- Added `animatable.onAnimationLoopObservable` ([Deltakosh](https://github.com/deltakosh))
+- Added `animationGroup.onAnimationLoopObservable` ([Deltakosh](https://github.com/deltakosh))
+- Added FlyCamera for free navigation in 3D space, with a limited set of settings ([Phuein](https://github.com/phuein))
 - Added support for Scissor testing ([Deltakosh](https://github.com/deltakosh))
 - Added `Engine.onNewSceneAddedObservable` ([Deltakosh](https://github.com/deltakosh))
 - Added new `PassCubePostProcess` to render cube map content ([Deltakosh](https://github.com/deltakosh))
@@ -74,9 +78,9 @@
 - Added support for overriding the mesh used for the world matrix for a mesh with a skeleton ([bghgary](https://github.com/bghgary))
 - Added support for linking a bone to a transform node ([bghgary](https://github.com/bghgary))
 - Factored out `setDirection` function from `lookAt` for transform node ([bghgary](https://github.com/bghgary))
-- Added support for AmmoJS as a physics plugin ([TrevorDev](https://github.com/TrevorDev))
 - Add support for setting renderingGroupId and creating instances to `AxesViewer` ([bghgary](https://github.com/bghgary))
 - Invert vScale of compressed ktx textures as they are inverted in the file and UNPACK_FLIP_Y_WEBGL is not supported by ktx ([TrevorDev](https://github.com/TrevorDev))
+- Enable dragging in boundingBoxGizmo without needing a parent ([TrevorDev](https://github.com/TrevorDev))
 
 ### glTF Loader
 

+ 487 - 487
gui/src/2D/controls/colorpicker.ts

@@ -1,488 +1,488 @@
-import { Control } from "./control";
-import { Color3, Observable, Vector2 } from "babylonjs";
-import { Measure } from "2D";
-
-/** Class used to create color pickers */
-export class ColorPicker extends Control {
-    private static _Epsilon = 0.000001;
-    private _colorWheelCanvas: HTMLCanvasElement;
-
-    private _value: Color3 = Color3.Red();
-    private _tmpColor = new Color3();
-
-    private _pointerStartedOnSquare = false;
-    private _pointerStartedOnWheel = false;
-
-    private _squareLeft = 0;
-    private _squareTop = 0;
-    private _squareSize = 0;
-
-    private _h = 360;
-    private _s = 1;
-    private _v = 1;
-
-    /**
-     * Observable raised when the value changes
-     */
-    public onValueChangedObservable = new Observable<Color3>();
-
-    /** Gets or sets the color of the color picker */
-    public get value(): Color3 {
-        return this._value;
-    }
-
-    public set value(value: Color3) {
-        if (this._value.equals(value)) {
-            return;
-        }
-
-        this._value.copyFrom(value);
-
-        this._RGBtoHSV(this._value, this._tmpColor);
-
-        this._h = this._tmpColor.r;
-        this._s = Math.max(this._tmpColor.g, 0.00001);
-        this._v = Math.max(this._tmpColor.b, 0.00001);
-
-        this._markAsDirty();
-
-        if (this._value.r <= ColorPicker._Epsilon) {
-            this._value.r = 0;
-        }
-
-        if (this._value.g <= ColorPicker._Epsilon) {
-            this._value.g = 0;
-        }
-
-        if (this._value.b <= ColorPicker._Epsilon) {
-            this._value.b = 0;
-        }
-
-        if (this._value.r >= 1.0 - ColorPicker._Epsilon) {
-            this._value.r = 1.0;
-        }
-
-        if (this._value.g >= 1.0 - ColorPicker._Epsilon) {
-            this._value.g = 1.0;
-        }
-
-        if (this._value.b >= 1.0 - ColorPicker._Epsilon) {
-            this._value.b = 1.0;
-        }
-
-        this.onValueChangedObservable.notifyObservers(this._value);
-    }
-
-    /**
-     * Gets or sets control width
-     * @see http://doc.babylonjs.com/how_to/gui#position-and-size
-     */
-    public get width(): string | number {
-        return this._width.toString(this._host);
-    }
-
-    public set width(value: string | number) {
-        if (this._width.toString(this._host) === value) {
-            return;
-        }
-
-        if (this._width.fromString(value)) {
-            this._height.fromString(value);
-            this._markAsDirty();
-        }
-    }
-
-    /**
-     * Gets or sets control height
-     * @see http://doc.babylonjs.com/how_to/gui#position-and-size
-     */
-    public get height(): string | number {
-        return this._height.toString(this._host);
-    }
-
-    /** Gets or sets control height */
-    public set height(value: string | number) {
-        if (this._height.toString(this._host) === value) {
-            return;
-        }
-
-        if (this._height.fromString(value)) {
-            this._width.fromString(value);
-            this._markAsDirty();
-        }
-    }
-
-    /** Gets or sets control size */
-    public get size(): string | number {
-        return this.width;
-    }
-
-    public set size(value: string | number) {
-        this.width = value;
-    }
-
-    /**
-     * Creates a new ColorPicker
-     * @param name defines the control name
-     */
-    constructor(public name?: string) {
-        super(name);
-        this.value = new Color3(.88, .1, .1);
-        this.size = "200px";
-        this.isPointerBlocker = true;
-    }
-
-    protected _getTypeName(): string {
-        return "ColorPicker";
-    }
-
-    /** @hidden */
-    protected _preMeasure(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
-
-        if (parentMeasure.width < parentMeasure.height) {
-            this._currentMeasure.height = parentMeasure.width;
-        } else {
-            this._currentMeasure.width = parentMeasure.height;
-        }
-    }
-
-    private _updateSquareProps(): void {
-        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
-        var wheelThickness = radius * .2;
-        var innerDiameter = (radius - wheelThickness) * 2;
-        var squareSize = innerDiameter / (Math.sqrt(2));
-        var offset = radius - squareSize * .5;
-
-        this._squareLeft = this._currentMeasure.left + offset;
-        this._squareTop = this._currentMeasure.top + offset;
-        this._squareSize = squareSize;
-    }
-
-    private _drawGradientSquare(hueValue: number, left: number, top: number, width: number, height: number, context: CanvasRenderingContext2D) {
-        var lgh = context.createLinearGradient(left, top, width + left, top);
-        lgh.addColorStop(0, '#fff');
-        lgh.addColorStop(1, 'hsl(' + hueValue + ', 100%, 50%)');
-
-        context.fillStyle = lgh;
-        context.fillRect(left, top, width, height);
-
-        var lgv = context.createLinearGradient(left, top, left, height + top);
-        lgv.addColorStop(0, 'rgba(0,0,0,0)');
-        lgv.addColorStop(1, '#000');
-
-        context.fillStyle = lgv;
-        context.fillRect(left, top, width, height);
-    }
-
-    private _drawCircle(centerX: number, centerY: number, radius: number, context: CanvasRenderingContext2D) {
-        context.beginPath();
-        context.arc(centerX, centerY, radius + 1, 0, 2 * Math.PI, false);
-        context.lineWidth = 3;
-        context.strokeStyle = '#333333';
-        context.stroke();
-        context.beginPath();
-        context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
-        context.lineWidth = 3;
-        context.strokeStyle = '#ffffff';
-        context.stroke();
-    }
-
-    private _createColorWheelCanvas(radius: number, thickness: number): HTMLCanvasElement {
-        var canvas = document.createElement("canvas");
-        canvas.width = radius * 2;
-        canvas.height = radius * 2;
-        var context = <CanvasRenderingContext2D>canvas.getContext("2d");
-        var image = context.getImageData(0, 0, radius * 2, radius * 2);
-        var data = image.data;
-
-        var color = this._tmpColor;
-        var maxDistSq = radius * radius;
-        var innerRadius = radius - thickness;
-        var minDistSq = innerRadius * innerRadius;
-
-        for (var x = -radius; x < radius; x++) {
-            for (var y = -radius; y < radius; y++) {
-
-                var distSq = x * x + y * y;
-
-                if (distSq > maxDistSq || distSq < minDistSq) {
-                    continue;
-                }
-
-                var dist = Math.sqrt(distSq);
-                var ang = Math.atan2(y, x);
-
-                this._HSVtoRGB(ang * 180 / Math.PI + 180, dist / radius, 1, color);
-
-                var index = ((x + radius) + ((y + radius) * 2 * radius)) * 4;
-
-                data[index] = color.r * 255;
-                data[index + 1] = color.g * 255;
-                data[index + 2] = color.b * 255;
-                var alphaRatio = (dist - innerRadius) / (radius - innerRadius);
-
-                //apply less alpha to bigger color pickers
-                var alphaAmount = .2;
-                var maxAlpha = .2;
-                var minAlpha = .04;
-                var lowerRadius = 50;
-                var upperRadius = 150;
-
-                if (radius < lowerRadius) {
-                    alphaAmount = maxAlpha;
-                } else if (radius > upperRadius) {
-                    alphaAmount = minAlpha;
-                } else {
-                    alphaAmount = (minAlpha - maxAlpha) * (radius - lowerRadius) / (upperRadius - lowerRadius) + maxAlpha;
-                }
-
-                var alphaRatio = (dist - innerRadius) / (radius - innerRadius);
-
-                if (alphaRatio < alphaAmount) {
-                    data[index + 3] = 255 * (alphaRatio / alphaAmount);
-                } else if (alphaRatio > 1 - alphaAmount) {
-                    data[index + 3] = 255 * (1.0 - ((alphaRatio - (1 - alphaAmount)) / alphaAmount));
-                } else {
-                    data[index + 3] = 255;
-                }
-
-            }
-        }
-
-        context.putImageData(image, 0, 0);
-
-        return canvas;
-    }
-
-    private _RGBtoHSV(color: Color3, result: Color3) {
-        var r = color.r;
-        var g = color.g;
-        var b = color.b;
-
-        var max = Math.max(r, g, b);
-        var min = Math.min(r, g, b);
-        var h = 0;
-        var s = 0;
-        var v = max;
-
-        var dm = max - min;
-
-        if (max !== 0) {
-            s = dm / max;
-        }
-
-        if (max != min) {
-            if (max == r) {
-                h = (g - b) / dm;
-                if (g < b) {
-                    h += 6;
-                }
-            } else if (max == g) {
-                h = (b - r) / dm + 2;
-            } else if (max == b) {
-                h = (r - g) / dm + 4;
-            }
-            h *= 60;
-        }
-
-        result.r = h;
-        result.g = s;
-        result.b = v;
-    }
-
-    private _HSVtoRGB(hue: number, saturation: number, value: number, result: Color3) {
-        var chroma = value * saturation;
-        var h = hue / 60;
-        var x = chroma * (1 - Math.abs((h % 2) - 1));
-        var r = 0;
-        var g = 0;
-        var b = 0;
-
-        if (h >= 0 && h <= 1) {
-            r = chroma;
-            g = x;
-        } else if (h >= 1 && h <= 2) {
-            r = x;
-            g = chroma;
-        } else if (h >= 2 && h <= 3) {
-            g = chroma;
-            b = x;
-        } else if (h >= 3 && h <= 4) {
-            g = x;
-            b = chroma;
-        } else if (h >= 4 && h <= 5) {
-            r = x;
-            b = chroma;
-        } else if (h >= 5 && h <= 6) {
-            r = chroma;
-            b = x;
-        }
-
-        var m = value - chroma;
-        result.set((r + m), (g + m), (b + m));
-    }
-
-    /** @hidden */
-    public _draw(context: CanvasRenderingContext2D): void {
-        context.save();
-
-        this._applyStates(context);
-
-        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
-        var wheelThickness = radius * .2;
-        var left = this._currentMeasure.left;
-        var top = this._currentMeasure.top;
-
-        if (!this._colorWheelCanvas || this._colorWheelCanvas.width != radius * 2) {
-            this._colorWheelCanvas = this._createColorWheelCanvas(radius, wheelThickness);
-        }
-
-        this._updateSquareProps();
-
-        if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
-            context.shadowColor = this.shadowColor;
-            context.shadowBlur = this.shadowBlur;
-            context.shadowOffsetX = this.shadowOffsetX;
-            context.shadowOffsetY = this.shadowOffsetY;
-
-            context.fillRect(this._squareLeft, this._squareTop, this._squareSize, this._squareSize);
-        }
-
-        context.drawImage(this._colorWheelCanvas, left, top);
-
-        if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
-            context.shadowBlur = 0;
-            context.shadowOffsetX = 0;
-            context.shadowOffsetY = 0;
-        }
-
-        this._drawGradientSquare(this._h,
-            this._squareLeft,
-            this._squareTop,
-            this._squareSize,
-            this._squareSize,
-            context);
-
-        var cx = this._squareLeft + this._squareSize * this._s;
-        var cy = this._squareTop + this._squareSize * (1 - this._v);
-
-        this._drawCircle(cx, cy, radius * .04, context);
-
-        var dist = radius - wheelThickness * .5;
-        cx = left + radius + Math.cos((this._h - 180) * Math.PI / 180) * dist;
-        cy = top + radius + Math.sin((this._h - 180) * Math.PI / 180) * dist;
-        this._drawCircle(cx, cy, wheelThickness * .35, context);
-
-        context.restore();
-    }
-
-    // Events
-    private _pointerIsDown = false;
-
-    private _updateValueFromPointer(x: number, y: number): void {
-        if (this._pointerStartedOnWheel) {
-            var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
-            var centerX = radius + this._currentMeasure.left;
-            var centerY = radius + this._currentMeasure.top;
-            this._h = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI + 180;
-        }
-        else if (this._pointerStartedOnSquare) {
-            this._updateSquareProps();
-            this._s = (x - this._squareLeft) / this._squareSize;
-            this._v = 1 - (y - this._squareTop) / this._squareSize;
-            this._s = Math.min(this._s, 1);
-            this._s = Math.max(this._s, ColorPicker._Epsilon);
-            this._v = Math.min(this._v, 1);
-            this._v = Math.max(this._v, ColorPicker._Epsilon);
-        }
-
-        this._HSVtoRGB(this._h, this._s, this._v, this._tmpColor);
-
-        this.value = this._tmpColor;
-    }
-
-    private _isPointOnSquare(x: number, y: number): boolean {
-        this._updateSquareProps();
-
-        var left = this._squareLeft;
-        var top = this._squareTop;
-        var size = this._squareSize;
-
-        if (x >= left && x <= left + size &&
-            y >= top && y <= top + size) {
-            return true;
-        }
-
-        return false;
-    }
-
-    private _isPointOnWheel(x: number, y: number): boolean {
-        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
-        var centerX = radius + this._currentMeasure.left;
-        var centerY = radius + this._currentMeasure.top;
-        var wheelThickness = radius * .2;
-        var innerRadius = radius - wheelThickness;
-        var radiusSq = radius * radius;
-        var innerRadiusSq = innerRadius * innerRadius;
-
-        var dx = x - centerX;
-        var dy = y - centerY;
-
-        var distSq = dx * dx + dy * dy;
-
-        if (distSq <= radiusSq && distSq >= innerRadiusSq) {
-            return true;
-        }
-
-        return false;
-    }
-
-    public _onPointerDown(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number): boolean {
-        if (!super._onPointerDown(target, coordinates, pointerId, buttonIndex)) {
-            return false;
-        }
-
-        this._pointerIsDown = true;
-
-        this._pointerStartedOnSquare = false;
-        this._pointerStartedOnWheel = false;
-
-        // Invert transform
-        this._invertTransformMatrix.transformCoordinates(coordinates.x, coordinates.y, this._transformedPosition);
-
-        let x = this._transformedPosition.x;
-        let y = this._transformedPosition.y;
-
-        if (this._isPointOnSquare(x, y)) {
-            this._pointerStartedOnSquare = true;
-        } else if (this._isPointOnWheel(x, y)) {
-            this._pointerStartedOnWheel = true;
-        }
-
-        this._updateValueFromPointer(x, y);
-        this._host._capturingControl[pointerId] = this;
-
-        return true;
-    }
-
-    public _onPointerMove(target: Control, coordinates: Vector2): void {
-        // Invert transform
-        this._invertTransformMatrix.transformCoordinates(coordinates.x, coordinates.y, this._transformedPosition);
-
-        let x = this._transformedPosition.x;
-        let y = this._transformedPosition.y;
-
-        if (this._pointerIsDown) {
-            this._updateValueFromPointer(x, y);
-        }
-
-        super._onPointerMove(target, coordinates);
-    }
-
-    public _onPointerUp(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number, notifyClick: boolean): void {
-        this._pointerIsDown = false;
-
-        delete this._host._capturingControl[pointerId];
-        super._onPointerUp(target, coordinates, pointerId, buttonIndex, notifyClick);
-    }
+import { Control } from "./control";
+import { Color3, Observable, Vector2 } from "babylonjs";
+import { Measure } from "../measure";
+
+/** Class used to create color pickers */
+export class ColorPicker extends Control {
+    private static _Epsilon = 0.000001;
+    private _colorWheelCanvas: HTMLCanvasElement;
+
+    private _value: Color3 = Color3.Red();
+    private _tmpColor = new Color3();
+
+    private _pointerStartedOnSquare = false;
+    private _pointerStartedOnWheel = false;
+
+    private _squareLeft = 0;
+    private _squareTop = 0;
+    private _squareSize = 0;
+
+    private _h = 360;
+    private _s = 1;
+    private _v = 1;
+
+    /**
+     * Observable raised when the value changes
+     */
+    public onValueChangedObservable = new Observable<Color3>();
+
+    /** Gets or sets the color of the color picker */
+    public get value(): Color3 {
+        return this._value;
+    }
+
+    public set value(value: Color3) {
+        if (this._value.equals(value)) {
+            return;
+        }
+
+        this._value.copyFrom(value);
+
+        this._RGBtoHSV(this._value, this._tmpColor);
+
+        this._h = this._tmpColor.r;
+        this._s = Math.max(this._tmpColor.g, 0.00001);
+        this._v = Math.max(this._tmpColor.b, 0.00001);
+
+        this._markAsDirty();
+
+        if (this._value.r <= ColorPicker._Epsilon) {
+            this._value.r = 0;
+        }
+
+        if (this._value.g <= ColorPicker._Epsilon) {
+            this._value.g = 0;
+        }
+
+        if (this._value.b <= ColorPicker._Epsilon) {
+            this._value.b = 0;
+        }
+
+        if (this._value.r >= 1.0 - ColorPicker._Epsilon) {
+            this._value.r = 1.0;
+        }
+
+        if (this._value.g >= 1.0 - ColorPicker._Epsilon) {
+            this._value.g = 1.0;
+        }
+
+        if (this._value.b >= 1.0 - ColorPicker._Epsilon) {
+            this._value.b = 1.0;
+        }
+
+        this.onValueChangedObservable.notifyObservers(this._value);
+    }
+
+    /**
+     * Gets or sets control width
+     * @see http://doc.babylonjs.com/how_to/gui#position-and-size
+     */
+    public get width(): string | number {
+        return this._width.toString(this._host);
+    }
+
+    public set width(value: string | number) {
+        if (this._width.toString(this._host) === value) {
+            return;
+        }
+
+        if (this._width.fromString(value)) {
+            this._height.fromString(value);
+            this._markAsDirty();
+        }
+    }
+
+    /**
+     * Gets or sets control height
+     * @see http://doc.babylonjs.com/how_to/gui#position-and-size
+     */
+    public get height(): string | number {
+        return this._height.toString(this._host);
+    }
+
+    /** Gets or sets control height */
+    public set height(value: string | number) {
+        if (this._height.toString(this._host) === value) {
+            return;
+        }
+
+        if (this._height.fromString(value)) {
+            this._width.fromString(value);
+            this._markAsDirty();
+        }
+    }
+
+    /** Gets or sets control size */
+    public get size(): string | number {
+        return this.width;
+    }
+
+    public set size(value: string | number) {
+        this.width = value;
+    }
+
+    /**
+     * Creates a new ColorPicker
+     * @param name defines the control name
+     */
+    constructor(public name?: string) {
+        super(name);
+        this.value = new Color3(.88, .1, .1);
+        this.size = "200px";
+        this.isPointerBlocker = true;
+    }
+
+    protected _getTypeName(): string {
+        return "ColorPicker";
+    }
+
+    /** @hidden */
+    protected _preMeasure(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
+
+        if (parentMeasure.width < parentMeasure.height) {
+            this._currentMeasure.height = parentMeasure.width;
+        } else {
+            this._currentMeasure.width = parentMeasure.height;
+        }
+    }
+
+    private _updateSquareProps(): void {
+        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
+        var wheelThickness = radius * .2;
+        var innerDiameter = (radius - wheelThickness) * 2;
+        var squareSize = innerDiameter / (Math.sqrt(2));
+        var offset = radius - squareSize * .5;
+
+        this._squareLeft = this._currentMeasure.left + offset;
+        this._squareTop = this._currentMeasure.top + offset;
+        this._squareSize = squareSize;
+    }
+
+    private _drawGradientSquare(hueValue: number, left: number, top: number, width: number, height: number, context: CanvasRenderingContext2D) {
+        var lgh = context.createLinearGradient(left, top, width + left, top);
+        lgh.addColorStop(0, '#fff');
+        lgh.addColorStop(1, 'hsl(' + hueValue + ', 100%, 50%)');
+
+        context.fillStyle = lgh;
+        context.fillRect(left, top, width, height);
+
+        var lgv = context.createLinearGradient(left, top, left, height + top);
+        lgv.addColorStop(0, 'rgba(0,0,0,0)');
+        lgv.addColorStop(1, '#000');
+
+        context.fillStyle = lgv;
+        context.fillRect(left, top, width, height);
+    }
+
+    private _drawCircle(centerX: number, centerY: number, radius: number, context: CanvasRenderingContext2D) {
+        context.beginPath();
+        context.arc(centerX, centerY, radius + 1, 0, 2 * Math.PI, false);
+        context.lineWidth = 3;
+        context.strokeStyle = '#333333';
+        context.stroke();
+        context.beginPath();
+        context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
+        context.lineWidth = 3;
+        context.strokeStyle = '#ffffff';
+        context.stroke();
+    }
+
+    private _createColorWheelCanvas(radius: number, thickness: number): HTMLCanvasElement {
+        var canvas = document.createElement("canvas");
+        canvas.width = radius * 2;
+        canvas.height = radius * 2;
+        var context = <CanvasRenderingContext2D>canvas.getContext("2d");
+        var image = context.getImageData(0, 0, radius * 2, radius * 2);
+        var data = image.data;
+
+        var color = this._tmpColor;
+        var maxDistSq = radius * radius;
+        var innerRadius = radius - thickness;
+        var minDistSq = innerRadius * innerRadius;
+
+        for (var x = -radius; x < radius; x++) {
+            for (var y = -radius; y < radius; y++) {
+
+                var distSq = x * x + y * y;
+
+                if (distSq > maxDistSq || distSq < minDistSq) {
+                    continue;
+                }
+
+                var dist = Math.sqrt(distSq);
+                var ang = Math.atan2(y, x);
+
+                this._HSVtoRGB(ang * 180 / Math.PI + 180, dist / radius, 1, color);
+
+                var index = ((x + radius) + ((y + radius) * 2 * radius)) * 4;
+
+                data[index] = color.r * 255;
+                data[index + 1] = color.g * 255;
+                data[index + 2] = color.b * 255;
+                var alphaRatio = (dist - innerRadius) / (radius - innerRadius);
+
+                //apply less alpha to bigger color pickers
+                var alphaAmount = .2;
+                var maxAlpha = .2;
+                var minAlpha = .04;
+                var lowerRadius = 50;
+                var upperRadius = 150;
+
+                if (radius < lowerRadius) {
+                    alphaAmount = maxAlpha;
+                } else if (radius > upperRadius) {
+                    alphaAmount = minAlpha;
+                } else {
+                    alphaAmount = (minAlpha - maxAlpha) * (radius - lowerRadius) / (upperRadius - lowerRadius) + maxAlpha;
+                }
+
+                var alphaRatio = (dist - innerRadius) / (radius - innerRadius);
+
+                if (alphaRatio < alphaAmount) {
+                    data[index + 3] = 255 * (alphaRatio / alphaAmount);
+                } else if (alphaRatio > 1 - alphaAmount) {
+                    data[index + 3] = 255 * (1.0 - ((alphaRatio - (1 - alphaAmount)) / alphaAmount));
+                } else {
+                    data[index + 3] = 255;
+                }
+
+            }
+        }
+
+        context.putImageData(image, 0, 0);
+
+        return canvas;
+    }
+
+    private _RGBtoHSV(color: Color3, result: Color3) {
+        var r = color.r;
+        var g = color.g;
+        var b = color.b;
+
+        var max = Math.max(r, g, b);
+        var min = Math.min(r, g, b);
+        var h = 0;
+        var s = 0;
+        var v = max;
+
+        var dm = max - min;
+
+        if (max !== 0) {
+            s = dm / max;
+        }
+
+        if (max != min) {
+            if (max == r) {
+                h = (g - b) / dm;
+                if (g < b) {
+                    h += 6;
+                }
+            } else if (max == g) {
+                h = (b - r) / dm + 2;
+            } else if (max == b) {
+                h = (r - g) / dm + 4;
+            }
+            h *= 60;
+        }
+
+        result.r = h;
+        result.g = s;
+        result.b = v;
+    }
+
+    private _HSVtoRGB(hue: number, saturation: number, value: number, result: Color3) {
+        var chroma = value * saturation;
+        var h = hue / 60;
+        var x = chroma * (1 - Math.abs((h % 2) - 1));
+        var r = 0;
+        var g = 0;
+        var b = 0;
+
+        if (h >= 0 && h <= 1) {
+            r = chroma;
+            g = x;
+        } else if (h >= 1 && h <= 2) {
+            r = x;
+            g = chroma;
+        } else if (h >= 2 && h <= 3) {
+            g = chroma;
+            b = x;
+        } else if (h >= 3 && h <= 4) {
+            g = x;
+            b = chroma;
+        } else if (h >= 4 && h <= 5) {
+            r = x;
+            b = chroma;
+        } else if (h >= 5 && h <= 6) {
+            r = chroma;
+            b = x;
+        }
+
+        var m = value - chroma;
+        result.set((r + m), (g + m), (b + m));
+    }
+
+    /** @hidden */
+    public _draw(context: CanvasRenderingContext2D): void {
+        context.save();
+
+        this._applyStates(context);
+
+        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
+        var wheelThickness = radius * .2;
+        var left = this._currentMeasure.left;
+        var top = this._currentMeasure.top;
+
+        if (!this._colorWheelCanvas || this._colorWheelCanvas.width != radius * 2) {
+            this._colorWheelCanvas = this._createColorWheelCanvas(radius, wheelThickness);
+        }
+
+        this._updateSquareProps();
+
+        if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
+            context.shadowColor = this.shadowColor;
+            context.shadowBlur = this.shadowBlur;
+            context.shadowOffsetX = this.shadowOffsetX;
+            context.shadowOffsetY = this.shadowOffsetY;
+
+            context.fillRect(this._squareLeft, this._squareTop, this._squareSize, this._squareSize);
+        }
+
+        context.drawImage(this._colorWheelCanvas, left, top);
+
+        if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
+            context.shadowBlur = 0;
+            context.shadowOffsetX = 0;
+            context.shadowOffsetY = 0;
+        }
+
+        this._drawGradientSquare(this._h,
+            this._squareLeft,
+            this._squareTop,
+            this._squareSize,
+            this._squareSize,
+            context);
+
+        var cx = this._squareLeft + this._squareSize * this._s;
+        var cy = this._squareTop + this._squareSize * (1 - this._v);
+
+        this._drawCircle(cx, cy, radius * .04, context);
+
+        var dist = radius - wheelThickness * .5;
+        cx = left + radius + Math.cos((this._h - 180) * Math.PI / 180) * dist;
+        cy = top + radius + Math.sin((this._h - 180) * Math.PI / 180) * dist;
+        this._drawCircle(cx, cy, wheelThickness * .35, context);
+
+        context.restore();
+    }
+
+    // Events
+    private _pointerIsDown = false;
+
+    private _updateValueFromPointer(x: number, y: number): void {
+        if (this._pointerStartedOnWheel) {
+            var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
+            var centerX = radius + this._currentMeasure.left;
+            var centerY = radius + this._currentMeasure.top;
+            this._h = Math.atan2(y - centerY, x - centerX) * 180 / Math.PI + 180;
+        }
+        else if (this._pointerStartedOnSquare) {
+            this._updateSquareProps();
+            this._s = (x - this._squareLeft) / this._squareSize;
+            this._v = 1 - (y - this._squareTop) / this._squareSize;
+            this._s = Math.min(this._s, 1);
+            this._s = Math.max(this._s, ColorPicker._Epsilon);
+            this._v = Math.min(this._v, 1);
+            this._v = Math.max(this._v, ColorPicker._Epsilon);
+        }
+
+        this._HSVtoRGB(this._h, this._s, this._v, this._tmpColor);
+
+        this.value = this._tmpColor;
+    }
+
+    private _isPointOnSquare(x: number, y: number): boolean {
+        this._updateSquareProps();
+
+        var left = this._squareLeft;
+        var top = this._squareTop;
+        var size = this._squareSize;
+
+        if (x >= left && x <= left + size &&
+            y >= top && y <= top + size) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private _isPointOnWheel(x: number, y: number): boolean {
+        var radius = Math.min(this._currentMeasure.width, this._currentMeasure.height) * .5;
+        var centerX = radius + this._currentMeasure.left;
+        var centerY = radius + this._currentMeasure.top;
+        var wheelThickness = radius * .2;
+        var innerRadius = radius - wheelThickness;
+        var radiusSq = radius * radius;
+        var innerRadiusSq = innerRadius * innerRadius;
+
+        var dx = x - centerX;
+        var dy = y - centerY;
+
+        var distSq = dx * dx + dy * dy;
+
+        if (distSq <= radiusSq && distSq >= innerRadiusSq) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public _onPointerDown(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number): boolean {
+        if (!super._onPointerDown(target, coordinates, pointerId, buttonIndex)) {
+            return false;
+        }
+
+        this._pointerIsDown = true;
+
+        this._pointerStartedOnSquare = false;
+        this._pointerStartedOnWheel = false;
+
+        // Invert transform
+        this._invertTransformMatrix.transformCoordinates(coordinates.x, coordinates.y, this._transformedPosition);
+
+        let x = this._transformedPosition.x;
+        let y = this._transformedPosition.y;
+
+        if (this._isPointOnSquare(x, y)) {
+            this._pointerStartedOnSquare = true;
+        } else if (this._isPointOnWheel(x, y)) {
+            this._pointerStartedOnWheel = true;
+        }
+
+        this._updateValueFromPointer(x, y);
+        this._host._capturingControl[pointerId] = this;
+
+        return true;
+    }
+
+    public _onPointerMove(target: Control, coordinates: Vector2): void {
+        // Invert transform
+        this._invertTransformMatrix.transformCoordinates(coordinates.x, coordinates.y, this._transformedPosition);
+
+        let x = this._transformedPosition.x;
+        let y = this._transformedPosition.y;
+
+        if (this._pointerIsDown) {
+            this._updateValueFromPointer(x, y);
+        }
+
+        super._onPointerMove(target, coordinates);
+    }
+
+    public _onPointerUp(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number, notifyClick: boolean): void {
+        this._pointerIsDown = false;
+
+        delete this._host._capturingControl[pointerId];
+        super._onPointerUp(target, coordinates, pointerId, buttonIndex, notifyClick);
+    }
 }

+ 20 - 1
gui/src/2D/controls/control.ts

@@ -46,6 +46,8 @@ export class Control {
     /** @hidden */
     protected _isDirty = true;
     /** @hidden */
+    protected _wasDirty = false;
+    /** @hidden */
     public _tempParentMeasure = Measure.Empty();
     /** @hidden */
     protected _cachedParentMeasure = Measure.Empty();
@@ -111,6 +113,13 @@ export class Control {
     /** Gets or sets a boolean indicating if the children are clipped to the current control bounds */
     public clipChildren = true;
 
+    /**
+     * Gets or sets a boolean indicating that the current control should cache its rendering (useful when the control does not change often)
+     */
+    public useBitmapCache = false;
+
+    private _cacheData: Nullable<ImageData>;
+
     private _shadowOffsetX = 0;
     /** Gets or sets a value indicating the offset to apply on X axis to render the shadow */
     public get shadowOffsetX() {
@@ -1193,6 +1202,7 @@ export class Control {
 
         context.restore();
 
+        this._wasDirty = this._isDirty;
         this._isDirty = false;
 
         return true;
@@ -1408,7 +1418,16 @@ export class Control {
             this.onBeforeDrawObservable.notifyObservers(this);
         }
 
-        this._draw(context);
+        if (this.useBitmapCache && !this._wasDirty && this._cacheData) {
+            context.putImageData(this._cacheData, this._currentMeasure.left, this._currentMeasure.top);
+        } else {
+            this._draw(context);
+        }
+
+        if (this.useBitmapCache && this._wasDirty) {
+            this._cacheData = context.getImageData(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
+        }
+
         this._renderHighlight(context);
 
         if (this.onAfterDrawObservable.hasObservers()) {

+ 10 - 8
gui/src/2D/controls/stackPanel.ts

@@ -118,21 +118,23 @@ export class StackPanel extends Container {
             }
 
             if (this._isVertical) {
-                child.top = stackHeight + "px";
-                if (!child._top.ignoreAdaptiveScaling) {
-                    child._markAsDirty();
+                if (child.top !== stackHeight + "px") {
+                    child.top = stackHeight + "px";
+                    this._rebuildLayout = true;
+                    child._top.ignoreAdaptiveScaling = true;
                 }
-                child._top.ignoreAdaptiveScaling = true;
+
                 stackHeight += child._currentMeasure.height + child.paddingTopInPixels;
                 if (child._currentMeasure.width > stackWidth) {
                     stackWidth = child._currentMeasure.width;
                 }
             } else {
-                child.left = stackWidth + "px";
-                if (!child._left.ignoreAdaptiveScaling) {
-                    child._markAsDirty();
+                if (child.left !== stackWidth + "px") {
+                    child.left = stackWidth + "px";
+                    this._rebuildLayout = true;
+                    child._left.ignoreAdaptiveScaling = true;
                 }
-                child._left.ignoreAdaptiveScaling = true;
+
                 stackWidth += child._currentMeasure.width + child.paddingLeftInPixels;
                 if (child._currentMeasure.height > stackHeight) {
                     stackHeight = child._currentMeasure.height;

+ 4 - 18
inspector/src/components/sceneExplorer/entities/meshTreeItemComponent.tsx

@@ -1,4 +1,4 @@
-import { AbstractMesh, Mesh, IExplorerExtensibilityGroup, BoundingBoxGizmo, Color3, PointerDragBehavior } from "babylonjs";
+import { AbstractMesh, IExplorerExtensibilityGroup } from "babylonjs";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faCube } from '@fortawesome/free-solid-svg-icons';
 import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
@@ -44,36 +44,22 @@ export class MeshTreeItemComponent extends React.Component<IMeshTreeItemComponen
                 mesh.reservedDataStore.previousParent.reservedDataStore.detachedChildren.push(mesh);
             }
 
-            // Connect to gizmo
-            const dummy = BoundingBoxGizmo.MakeNotPickableAndWrapInBoundingBox(mesh as Mesh);
-            dummy.reservedDataStore = { hidden: true };
-            const gizmo = new BoundingBoxGizmo(Color3.FromHexString("#0984e3"));
-            gizmo.attachedMesh = dummy;
+            const gizmo = new BoundingBoxGizmo(BABYLON.Color3.FromHexString("#0984e3"));
+            gizmo.attachedMesh = mesh;
+            gizmo.enableDragBehavior();
 
             gizmo.updateBoundingBox();
 
             gizmo.fixedDragMeshScreenSize = true;
             mesh.reservedDataStore.gizmo = gizmo;
-
-            var pointerDragBehavior = new PointerDragBehavior();
-            pointerDragBehavior.useObjectOrienationForDragging = false;
-
-            dummy.addBehavior(pointerDragBehavior);
-
-            mesh.reservedDataStore.pointerDragBehavior = pointerDragBehavior;
-            mesh.reservedDataStore.dummy = dummy;
-
             this.setState({ isGizmoEnabled: true });
             return;
         }
 
         const previousParent = mesh.reservedDataStore.previousParent;
-        mesh.removeBehavior(mesh.reservedDataStore.pointerDragBehavior);
         mesh.reservedDataStore.gizmo.dispose();
         mesh.reservedDataStore.gizmo = null;
         mesh.setParent(previousParent);
-        mesh.reservedDataStore.dummy.dispose();
-        mesh.reservedDataStore.dummy = null;
 
         if (previousParent && previousParent.reservedDataStore) {
             previousParent.reservedDataStore.detachedChildren = null;

+ 9 - 0
localDev/index.html

@@ -42,6 +42,13 @@
             width: 60px;
             height: 20px;
         }
+
+        @font-face {
+            font-family: BabylonJSglyphs;
+           /* src: url("http://www.killer-squid.com/fonts/BabylonJSglyphs.otf"); */
+            src: local("BabylonJSglyphs");
+        }
+        
     </style>
 </head>
 
@@ -67,6 +74,8 @@
         }
         indexjs += '.js';
 
+        // var indexjs = "http://localhost:1234/index.js"
+
         // Load the scripts + map file to allow vscode debug.
         BABYLONDEVTOOLS.Loader
             .require(indexjs)

+ 20 - 2
src/Animations/animatable.ts

@@ -35,6 +35,11 @@ import { Scene } from "scene";
         public onAnimationEndObservable = new Observable<Animatable>();
 
         /**
+         * Observer raised when the animation loops
+         */
+        public onAnimationLoopObservable = new Observable<Animatable>();
+
+        /**
          * Gets the root Animatable used to synchronize and normalize animations
          */
         public get syncRoot(): Animatable {
@@ -96,6 +101,7 @@ import { Scene } from "scene";
          * @param speedRatio defines the factor to apply to animation speed (default is 1)
          * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
          * @param animations defines a group of animation to add to the new Animatable
+         * @param onAnimationLoop defines a callback to call when animation loops
          */
         constructor(scene: Scene,
             /** defines the target object */
@@ -109,7 +115,9 @@ import { Scene } from "scene";
             speedRatio: number = 1.0,
             /** defines a callback to call when animation ends if it is not looping */
             public onAnimationEnd?: Nullable<() => void>,
-            animations?: Animation[]) {
+            animations?: Animation[],
+            /** defines a callback to call when animation loops */
+            public onAnimationLoop?: Nullable<() => void>) {
             this._scene = scene;
             if (animations) {
                 this.appendAnimations(target, animations);
@@ -374,7 +382,15 @@ import { Scene } from "scene";
 
             for (index = 0; index < runtimeAnimations.length; index++) {
                 var animation = runtimeAnimations[index];
-                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame, this.toFrame, this.loopAnimation, this._speedRatio, this._weight);
+                var isRunning = animation.animate(delay - this._localDelayOffset, this.fromFrame,
+                    this.toFrame, this.loopAnimation, this._speedRatio, this._weight,
+                    () => {
+                        this.onAnimationLoopObservable.notifyObservers(this);
+                        if (this.onAnimationLoop) {
+                            this.onAnimationLoop();
+                        }
+                    }
+                );
                 running = running || isRunning;
             }
 
@@ -396,6 +412,8 @@ import { Scene } from "scene";
 
                 if (this.disposeOnEnd) {
                     this.onAnimationEnd = null;
+                    this.onAnimationLoop = null;
+                    this.onAnimationLoopObservable.clear();
                     this.onAnimationEndObservable.clear();
                 }
             }

+ 15 - 1
src/Animations/animationGroup.ts

@@ -39,11 +39,16 @@ import { Engine } from "Engines/engine";
         public uniqueId: number;
 
         /**
-         * This observable will notify when one animation have ended.
+         * This observable will notify when one animation have ended
          */
         public onAnimationEndObservable = new Observable<TargetedAnimation>();
 
         /**
+         * Observer raised when one animation loops
+         */
+        public onAnimationLoopObservable = new Observable<TargetedAnimation>();
+
+        /**
          * This observable will notify when all animations have ended.
          */
         public onAnimationGroupEndObservable = new Observable<AnimationGroup>();
@@ -231,6 +236,9 @@ import { Engine } from "Engines/engine";
                     this.onAnimationEndObservable.notifyObservers(targetedAnimation);
                     this._checkAnimationGroupEnded(animatable);
                 };
+                animatable.onAnimationLoop = () => {
+                    this.onAnimationLoopObservable.notifyObservers(targetedAnimation);
+                };
                 this._animatables.push(animatable);
             }
 
@@ -410,6 +418,12 @@ import { Engine } from "Engines/engine";
             if (index > -1) {
                 this._scene.animationGroups.splice(index, 1);
             }
+
+            this.onAnimationEndObservable.clear();
+            this.onAnimationGroupEndObservable.clear();
+            this.onAnimationGroupPauseObservable.clear();
+            this.onAnimationGroupPlayObservable.clear();
+            this.onAnimationLoopObservable.clear();
         }
 
         private _checkAnimationGroupEnded(animatable: Animatable) {

+ 6 - 1
src/Animations/runtimeAnimation.ts

@@ -413,9 +413,10 @@ import { Scene } from "scene";
          * @param loop defines if the current animation must loop
          * @param speedRatio defines the current speed ratio
          * @param weight defines the weight of the animation (default is -1 so no weight)
+         * @param onLoop optional callback called when animation loops
          * @returns a boolean indicating if the animation is running
          */
-        public animate(delay: number, from: number, to: number, loop: boolean, speedRatio: number, weight = -1.0): boolean {
+        public animate(delay: number, from: number, to: number, loop: boolean, speedRatio: number, weight = -1.0, onLoop?: () => void): boolean {
             let targetPropertyPath = this._animation.targetPropertyPath;
             if (!targetPropertyPath || targetPropertyPath.length < 1) {
                 this._stopped = true;
@@ -552,6 +553,10 @@ import { Scene } from "scene";
             let events = this._events;
             if (range > 0 && this.currentFrame > currentFrame ||
                 range < 0 && this.currentFrame < currentFrame) {
+                if (onLoop) {
+                    onLoop();
+                }
+
                 // Need to reset animation events
                 for (var index = 0; index < events.length; index++) {
                     if (!events[index].onlyOnce) {

+ 40 - 0
src/Gizmos/boundingBoxGizmo.ts

@@ -81,6 +81,10 @@ import { StandardMaterial } from "Materials/standardMaterial";
         private _anchorMesh: AbstractMesh;
         private _existingMeshScale = new Vector3();
 
+        // Dragging
+        private _dragMesh: Nullable<Mesh> = null;
+        private pointerDragBehavior = new BABYLON.PointerDragBehavior();
+
         // Stores the state of the pivot cache (_oldPivotPoint, _pivotTranslation)
         // store/remove pivot point should only be applied during their outermost calls
         private static _PivotCached = 0;
@@ -223,6 +227,7 @@ import { StandardMaterial } from "Materials/standardMaterial";
 
                         BoundingBoxGizmo._RestorePivotPoint(this.attachedMesh);
                     }
+                    this._updateDummy();
                 });
 
                 // Selection/deselection
@@ -233,6 +238,7 @@ import { StandardMaterial } from "Materials/standardMaterial";
                 _dragBehavior.onDragEndObservable.add(() => {
                     this.onRotationSphereDragEndObservable.notifyObservers({});
                     this._selectNode(null);
+                    this._updateDummy();
                 });
 
                 this._rotateSpheresParent.addChild(sphere);
@@ -285,6 +291,7 @@ import { StandardMaterial } from "Materials/standardMaterial";
 
                                 BoundingBoxGizmo._RestorePivotPoint(this.attachedMesh);
                             }
+                            this._updateDummy();
                         });
 
                         // Selection/deselection
@@ -295,6 +302,7 @@ import { StandardMaterial } from "Materials/standardMaterial";
                         _dragBehavior.onDragEndObservable.add(() => {
                             this.onScaleBoxDragEndObservable.notifyObservers({});
                             this._selectNode(null);
+                            this._updateDummy();
                         });
 
                         this._scaleBoxesParent.addChild(box);
@@ -330,6 +338,12 @@ import { StandardMaterial } from "Materials/standardMaterial";
                     this._updateRotationSpheres();
                     this._updateScaleBoxes();
                 }
+
+                // If dragg mesh is enabled and dragging, update the attached mesh pose to match the drag mesh
+                if (this._dragMesh && this.attachedMesh && this.pointerDragBehavior.dragging) {
+                    this._lineBoundingBox.position.rotateByQuaternionToRef(this._rootMesh.rotationQuaternion!, this._tmpVector);
+                    this.attachedMesh.setAbsolutePosition(this._dragMesh.position.add(this._tmpVector.scale(-1)));
+                }
             });
             this.updateBoundingBox();
         }
@@ -343,6 +357,10 @@ import { StandardMaterial } from "Materials/standardMaterial";
                 this._anchorMesh.removeChild(value);
                 BoundingBoxGizmo._RestorePivotPoint(value);
                 this.updateBoundingBox();
+
+                this.gizmoLayer.utilityLayerScene.onAfterRenderObservable.addOnce(() => {
+                    this._updateDummy();
+                });
             }
         }
 
@@ -472,6 +490,25 @@ import { StandardMaterial } from "Materials/standardMaterial";
             });
         }
 
+        private _updateDummy() {
+            if (this._dragMesh) {
+                this._dragMesh.position.copyFrom(this._lineBoundingBox.getAbsolutePosition());
+                this._dragMesh.scaling.copyFrom(this._lineBoundingBox.scaling);
+                this._dragMesh.rotationQuaternion!.copyFrom(this._rootMesh.rotationQuaternion!);
+            }
+        }
+
+        /**
+         * Enables a pointer drag behavior on the bounding box of the gizmo
+         */
+        public enableDragBehavior() {
+            this._dragMesh = BABYLON.Mesh.CreateBox("dummy", 1, this.gizmoLayer.utilityLayerScene);
+            this._dragMesh.visibility = 0;
+            this._dragMesh.rotationQuaternion = new BABYLON.Quaternion();
+            this.pointerDragBehavior.useObjectOrienationForDragging = false;
+            this._dragMesh.addBehavior(this.pointerDragBehavior);
+        }
+
         /**
          * Disposes of the gizmo
          */
@@ -481,6 +518,9 @@ import { StandardMaterial } from "Materials/standardMaterial";
             this._lineBoundingBox.dispose();
             this._rotateSpheresParent.dispose();
             this._scaleBoxesParent.dispose();
+            if (this._dragMesh) {
+                this._dragMesh.dispose();
+            }
             super.dispose();
         }
 

+ 20 - 1
src/Physics/Plugins/ammoJSPlugin.ts

@@ -314,6 +314,25 @@ import { AbstractMesh } from "Meshes/abstractMesh";
 
             var joint: any;
             switch (impostorJoint.joint.type) {
+                case PhysicsJoint.DistanceJoint:
+                    var distance = (<DistanceJointData>jointData).maxDistance;
+                    if (distance) {
+                        jointData.mainPivot = new Vector3(0, -distance / 2, 0);
+                        jointData.connectedPivot = new Vector3(0, distance / 2, 0);
+                    }
+                    joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
+                    break;
+                case PhysicsJoint.HingeJoint:
+                    if (!jointData.mainAxis) {
+                        jointData.mainAxis = new Vector3(0, 0, 0);
+                    }
+                    if (!jointData.connectedAxis) {
+                        jointData.connectedAxis = new Vector3(0, 0, 0);
+                    }
+                    var mainAxis = new Ammo.btVector3(jointData.mainAxis.x, jointData.mainAxis.y, jointData.mainAxis.z);
+                    var connectedAxis = new Ammo.btVector3(jointData.connectedAxis.x, jointData.connectedAxis.y, jointData.connectedAxis.z);
+                    joint = new Ammo.btHingeConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z), mainAxis, connectedAxis);
+                    break;
                 case PhysicsJoint.BallAndSocketJoint:
                     joint = new Ammo.btPoint2PointConstraint(mainBody, connectedBody, new Ammo.btVector3(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z), new Ammo.btVector3(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z));
                     break;
@@ -664,7 +683,7 @@ import { AbstractMesh } from "Meshes/abstractMesh";
          * @param motorIndex index of the motor
          */
         public setMotor(joint: IMotorEnabledJoint, speed?: number, maxForce?: number, motorIndex?: number) {
-            Logger.Warn("setMotor is not currently supported by the Ammo physics plugin");
+            joint.physicsJoint.enableAngularMotor(true, speed, maxForce);
         }
 
         /**

+ 24 - 12
src/scene.ts

@@ -2481,10 +2481,13 @@ import { Logger } from "Misc/logger";
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        public beginWeightedAnimation(target: any, from: number, to: number, weight = 1.0, loop?: boolean, speedRatio: number = 1.0, onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean): Animatable {
-            let returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask);
+        public beginWeightedAnimation(target: any, from: number, to: number, weight = 1.0, loop?: boolean, speedRatio: number = 1.0,
+            onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable {
+
+            let returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop);
             returnedAnimatable.weight = weight;
 
             return returnedAnimatable;
@@ -2500,10 +2503,13 @@ import { Logger } from "Misc/logger";
          * @param onAnimationEnd defines the function to be executed when the animation ends
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
-         * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param targetMask defines if the target should be animate if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the animatable object created for this animation
          */
-        public beginAnimation(target: any, from: number, to: number, loop?: boolean, speedRatio: number = 1.0, onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true, targetMask?: (target: any) => boolean): Animatable {
+        public beginAnimation(target: any, from: number, to: number, loop?: boolean, speedRatio: number = 1.0,
+            onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true,
+            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable {
 
             if (from > to && speedRatio > 0) {
                 speedRatio *= -1;
@@ -2514,7 +2520,7 @@ import { Logger } from "Misc/logger";
             }
 
             if (!animatable) {
-                animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd);
+                animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop);
             }
 
             const shouldRunTargetAnimations = targetMask ? targetMask(target) : true;
@@ -2527,7 +2533,7 @@ import { Logger } from "Misc/logger";
             if (target.getAnimatables) {
                 var animatables = target.getAnimatables();
                 for (var index = 0; index < animatables.length; index++) {
-                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask);
+                    this.beginAnimation(animatables[index], from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, onAnimationLoop);
                 }
             }
 
@@ -2548,9 +2554,13 @@ import { Logger } from "Misc/logger";
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        public beginHierarchyAnimation(target: any, directDescendantsOnly: boolean, from: number, to: number, loop?: boolean, speedRatio: number = 1.0, onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true, targetMask?: (target: any) => boolean): Animatable[] {
+        public beginHierarchyAnimation(target: any, directDescendantsOnly: boolean, from: number, to: number, loop?: boolean, speedRatio: number = 1.0,
+            onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true,
+            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable[] {
+
             let children = target.getDescendants(directDescendantsOnly);
 
             let result = [];
@@ -2571,14 +2581,15 @@ import { Logger } from "Misc/logger";
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of created animatables
          */
-        public beginDirectAnimation(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void): Animatable {
+        public beginDirectAnimation(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable {
             if (speedRatio === undefined) {
                 speedRatio = 1.0;
             }
 
-            var animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations);
+            var animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop);
 
             return animatable;
         }
@@ -2593,15 +2604,16 @@ import { Logger } from "Misc/logger";
          * @param loop defines if you want animation to loop (off by default)
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
+         * @param onAnimationLoop defines the callback to call when an animation loops
          * @returns the list of animatables created for all nodes
          */
-        public beginDirectHierarchyAnimation(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void): Animatable[] {
+        public beginDirectHierarchyAnimation(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable[] {
             let children = target.getDescendants(directDescendantsOnly);
 
             let result = [];
-            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd));
+            result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             for (var child of children) {
-                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd));
+                result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
             }
 
             return result;