Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : added new PlanarControls #454

Merged
merged 5 commits into from
Aug 30, 2017

Conversation

EmmanuelSchmuck
Copy link
Contributor

PlanarControls provides ergonomic camera controls for the planar mode : drag, pan, orbit,
zoom and animated expression to smoothly move and orient the camera.

Modified planar example to use these controls
Update contributors

Copy link
Contributor

@peppsac peppsac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR and welcome to iTowns project :)

I did a very quick first review.
I (or another reviewer) will have do a more in depth one before merging but the code is looking good mostly and is well documented, nice!

// time management
let deltaTime = 0;
let lastElapsedTime = 0;
const clock = new THREE.Clock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about storing all these inside the PlanarControls instance so a user can instanciate several controls?

dragDelta.set(0, 0, 0);
};

/** =========================================================================================================================
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the ========= separator but this is something that should be done everywhere or nowhere (as all other stylistic choices).
So I'd suggest removing them from this PR and open a specific PR to convert itowns to this style (but I'm not sure it would be merged...)

* @param {number} value : the value to be smoothed, between 0 and 1
* @returns {number}
*/
var clamp01 = function clamp01(value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should directly use THREE.Math.clamp(value, 0, 1) instead of adding a one liner function.


// event keycode
const keys = { CTRL: 17, SPACE: 32, S: 83, T: 84 };
const mouseButtons = { LEFTCLICK: THREE.MOUSE.LEFT, MIDDLECLICK: THREE.MOUSE.MIDDLE, RIGHTCLICK: THREE.MOUSE.RIGHT };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style comment:

 const mouseButtons = {
    LEFTCLICK: THREE.MOUSE.LEFT,
    MIDDLECLICK: THREE.MOUSE.MIDDLE,
    RIGHTCLICK: THREE.MOUSE.RIGHT,
};

is prefered.

@EmmanuelSchmuck EmmanuelSchmuck force-pushed the planar_control branch 2 times, most recently from bff620c to 80f4a40 Compare August 25, 2017 14:41
@EmmanuelSchmuck
Copy link
Contributor Author

Thank you for the review @peppsac , I made the modifications you suggested.

PlanarControls offers ergonomic camera controls for the planar mode : drag, pan, orbit,
zoom and animated expression to smoothly move and orient the camera.

Modified planar example to use these controls
Update contributors

Modifications following peppsac review :
-all external variables are now within the class, this allows to
instanciate more than one controller.
-clamp01 function removed, using THREE clamp instead.
-various style modifications (separator removed, const array format...)
Copy link
Contributor

@peppsac peppsac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this controls quickly and it seems to be working good.

I left a few comments and then it'll be ready for merging.

this.view = view;
this.camera = view.camera.camera3D;
this.domElement = view.mainLoop.gfxEngine.renderer.domElement;
this.position = this.camera.position;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.position isn't really useful: let's use this.camera.position directly instead.

* Updates the view and camera if needed, and handles the animated travel
*/
this.update = function update() {
this.deltaTime = this.clock.getElapsedTime() - this.lastElapsedTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you use the dt provided as an argument to your update function (see https://github.com/iTowns/itowns/blob/master/src/Core/MainLoop.js#L63)?

this.handleTravel(this.deltaTime);
}
if (this.state !== STATE.NONE) {
this.view.camera.update(window.innerWidth, window.innerHeight);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to call this: it's done for you in view's update process (https://github.com/iTowns/itowns/blob/master/src/Core/MainLoop.js#L96)

update() uses mainloop's deltatime, no longer call camera.update()
this.camera.position replaces this.position
@EmmanuelSchmuck
Copy link
Contributor Author

@peppsac : modified again following your suggestions. Also, my inputs no longer call update() but notifyChange() instead, much cleaner.

Copy link
Contributor

@autra autra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking over this, as @peppsac is off. Sorry for the inconvenience to have another reviewer with different expectations stepping in :-/

On overall I really like it! The ergonomy (and the code) is good :-) I made a lot of comments, but most are minor.

// time management
this.deltaTime = 0;
this.lastElapsedTime = 0;
this.clock = new THREE.Clock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.clock is not used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, all three "time management" vars are not used, if I'm not mistaken.

this._handlerOnMouseDown = onMouseDown.bind(this);
this._handlerOnMouseUp = onMouseUp.bind(this);
this._handlerOnMouseMove = onMouseMove.bind(this);
this._handlerOnMouseWheel = onMouseWheel.bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As everything is defined in the constructor, you don't really need to attach these handlers on this. They can be local variables. That's better because they are truly isolated then (and being able to really isolate things is a very good reason to do this way, and not to use prototype or ES6 classes :-) )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also true for every local state you don't want to expose. I'm thinking about:

  • this.view
  • this.camera
  • all travelFoo vars
  • all drag vars
  • this.mousePosition, this.lastMousePosition and this.deltaMousePosition
  • isCtrlDown

I'm pretty sure these should be hidden.

In contrast, I believe keeping all the option properties visible is perfect :-)

For this.state, I have no strong opinion about it.

* Updates the view and camera if needed, and handles the animated travel
* @param {number} dt : deltatime given by mainLoop
* @param {boolean} updateLoopRestarted : given by mainLoop
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want to document this. It's just the implementation of a FrameRequester. It's not meant to be called by the outside world. Better not have it in documentation.

dt = 16;
}
if (this.state === STATE.TRAVEL) {
this.handleTravel(dt / 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about reasoning in ms everywhere?

* @returns {number}
*/
var smooth = function smooth(value) {
// p between 1.0 and 1.5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p is 1.20?

// setup the end rotation :

// case where targetOrientation is a quaternion
if (typeof targetOrientation.w !== 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the test should be if (targetOrientation instanceof THREE.Quaternion)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it would also work if targetOrientation is undefined this way)

// update cursor
this.updateMouseCursorType();

this.travelUseRotation = !(targetOrientation === 'none');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would test targetOrientation instanceOf THREE.Quaternion || targetOrientation instanceOf THREE.Vector3, it would also cover undefined, null, whatever string and invalid objects the caller gave you. Here the caller needs to remember to pass 'none' to explicitly disable rotation.

Not using isVector3 because this would throw an error if undefined or null. (I don't know why Vector3 implementors felt the need to have this boolean, it's useless imho).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I assume you don't want a rotation to the default value of travelEndRot if targetOrientation is invalid)

if (this.instantTravel) {
this.travelDuration = 0;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: remove empty line (a reader could miss the else if below)

// case where travelTime is set to 'auto' : travelDuration will be a value between autoTravelTimeMin and autoTravelTimeMax
// depending on travel distance and travel angular difference
else if (travelTime === 'auto') {
// a value between 0 and 1 according to the travel distance. Adjusted by autoTravelTimeDist parameter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then autoTravelTimeDist is a factor, not a real distance? Maybe we could rename it then.

const screenCenter = new THREE.Vector2(0.5 * window.innerWidth, 0.5 * window.innerHeight);

topViewPos.copy(this.getWorldPointAtScreenXY(screenCenter));
topViewPos.z += Math.min(this.maxAltitude, this.camera.position.distanceTo(topViewPos));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this line to me? Why isn't it always maxAltitude?

Copy link
Contributor

@autra autra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nearly there :-)

return pointUnderCursor;
};
let dir = new THREE.Vector3();
let pointUnderCursor = new THREE.Vector3();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a big deal but, dir does not need to be declared here. You can declare it when you first use it. Anyway, dir and vector will store the same pointer, because unproject, sub, normalize and multiplyScalar all modify the vector3 inplace and returns it (see for instance how sub is implemented).

On the other hand, pointUnderCursor must not be declared here, otherwise we would always return the same pointer, which is dangerous. We need to create a new instance each time, which is done by the clone anyway. So in short, just

return this.camera.clone().add(dir.multiplyScalar(distance));

instead of line 538-540 is fine (that being said, you can keep pointUnderCursor if you find it clearer, just declare it with const on line 538).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it's totally my fault, because I was the one to ask you about pointUnderCursor. Sorry :-/

// the returned value
const pointUnderCursor = new THREE.Vector3();
return (posXY) => {
// check if there is valid geometry under cursor
if (typeof this.view.getPickingPositionFromDepth(posXY) !== 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed that in my previous review: let's put this.view.getPickingPositionFromDepth(posXY) in a variable to avoid calling it 2 times.

@@ -212,10 +223,10 @@ function PlanarControls(view, options = {}) {
const panSpeed = THREE.Math.lerp(this.minPanSpeed, this.maxPanSpeed, distToGround);

// lateral movement (local x axis)
this.camera.position.copy(this.camera.localToWorld(new THREE.Vector3(panSpeed * -1 * this.deltaMousePosition.x, 0, 0)));
this.camera.position.copy(this.camera.localToWorld(new THREE.Vector3(panSpeed * -1 * deltaMousePosition.x, 0, 0)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.handlePanMovement  = (() => {
    const tmp = new THREE.Vector3();
    
    return () => {
         // ....
         tmp.set(panSpeed * -1 * deltaMousePosition.x, 0, 0);
         this.camera.position.copy(this.camera.localToWorld(tmp));
         // ...
   }
})();

should prevent one instanciation.

const offset = this.camera.position.clone().sub(centerPoint);

quat.setFromUnitVectors(this.camera.up, new THREE.Vector3(0, 0, 1));
quatInverse = quat.clone().inverse();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about quat.setFromUnitVectors(this.camera.up, new THREE.Vector3(0, 0, 1)).inverse() and using quat instead of quatInverse below? It saves a clone().

offset.applyQuaternion(rotationZQuaternion);
this.handleRotation = (() => {
const quat = new THREE.Quaternion();
let quatInverse = new THREE.Quaternion();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please "cache" here:

  • new THREE.Vector3(0, 0, 1) on line 268
  • rotationXQuaternion and vector on line 279-280
  • rotationZQuaternion on line 290

else {
pointUnderCursor.copy(this.getWorldPointFromMathPlaneAtScreenXY(posXY, this.groundLevel));
}
return pointUnderCursor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing as above. Sorry, my fault :-/

};

PlanarControls.prototype = Object.create(THREE.EventDispatcher.prototype);
PlanarControls.prototype.constructor = PlanarControls;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not using dispatchEvent, so you don't need the 2 lines above.

@autra
Copy link
Contributor

autra commented Aug 30, 2017

Yay thanks \o/

@autra
Copy link
Contributor

autra commented Aug 30, 2017

Nice first contrib btw !

@autra autra merged commit 92f404d into iTowns:master Aug 30, 2017
EmmanuelSchmuck pushed a commit to EmmanuelSchmuck/itowns that referenced this pull request Aug 30, 2017
With the miniGlobe, getZoom used to always return 0.

Feature : added new PlanarControls (iTowns#454)

* Feature : added new PlanarControls

PlanarControls offers ergonomic camera controls for the planar mode : drag, pan, orbit,
zoom and animated expression to smoothly move and orient the camera.

Modified planar example to use these controls
Update contributors

Feature : added new PlanarControls

PlanarControls offers ergonomic camera controls for the planar mode : drag, pan, orbit,
zoom and animated expression to smoothly move and orient the camera.

Modified planar example to use these controls
Update contributors

Modifications following peppsac review :
-all external variables are now within the class, this allows to
instanciate more than one controller.
-clamp01 function removed, using THREE clamp instead.
-various style modifications (separator removed, const array format...)

planarcontrols modifications following peppsac review :
update() uses mainloop's deltatime, no longer call camera.update()
this.camera.position replaces this.position

removed options.startposition and startlook, start view with Y instead of S (used for picking)

many modifications following review by Autra, see PR
@EmmanuelSchmuck
Copy link
Contributor Author

Thank you :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants