-
Notifications
You must be signed in to change notification settings - Fork 303
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
Conversation
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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); | ||
}; | ||
|
||
/** ========================================================================================================================= |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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.
bff620c
to
80f4a40
Compare
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...)
80f4a40
to
91eafa1
Compare
There was a problem hiding this 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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
@peppsac : modified again following your suggestions. Also, my inputs no longer call update() but notifyChange() instead, much cleaner. |
…d of S (used for picking)
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 :-) )
There was a problem hiding this comment.
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 | ||
*/ |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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; | ||
} | ||
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
?
7edea28
to
851c223
Compare
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 268rotationXQuaternion
andvector
on line 279-280rotationZQuaternion
on line 290
else { | ||
pointUnderCursor.copy(this.getWorldPointFromMathPlaneAtScreenXY(posXY, this.groundLevel)); | ||
} | ||
return pointUnderCursor; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
Yay thanks \o/ |
Nice first contrib btw ! |
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
Thank you :) |
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