New option to make the north pole remain up to the view

* lockNorthUp new Aladin init option. False by default.
* setRotation is still allowed if lockNorthUp=true but when panning, the
view resets its orientation so that the north pole is up
This commit is contained in:
bmatthieu3
2025-03-27 09:44:06 +01:00
committed by Matthieu Baumann
parent addbd555c1
commit 62f9e61978
10 changed files with 141 additions and 46 deletions

View File

@@ -4,7 +4,8 @@
### What's Changed
* [perf] perform CPU computations with Vec3 and Matrix3 and not 4 dimensions matrices/vectors.
* [perf] perform CPU computations with Vec3 and Matrix3 and not 4 dimensions matrices/vectors
* [feat] lockNorthUp Aladin object new option locking the north pole up to the view center
## Released

View File

@@ -30,6 +30,7 @@
fullScreen: true,
samp: true,
realFullscreen: true,
lockNorthUp: true,
}
);
});

View File

@@ -74,6 +74,7 @@ pub struct App {
// Task executor
//exec: Rc<RefCell<TaskExecutor>>,
inertia: Option<Inertia>,
north_up: bool,
disable_inertia: Rc<RefCell<bool>>,
dist_dragging: f32,
time_start_dragging: Time,
@@ -243,7 +244,7 @@ impl App {
catalog_loaded,
tile_fetcher,
north_up: false,
colormaps,
projection,
@@ -1520,11 +1521,11 @@ impl App {
return;
}
let now = Time::now();
let dragging_duration = (now - self.time_start_dragging).as_secs();
let dragging_vel = self.dist_dragging / dragging_duration;
let _dist_dragging = self.dist_dragging;
// Detect if there has been a recent acceleration
// It is also possible that the dragging time is too short and if it is the case, trigger the inertia
let recent_acceleration = (Time::now() - self.time_mouse_high_vel).as_secs() < 0.1
@@ -1545,20 +1546,20 @@ impl App {
let ampl = delta_angle * (dragging_vel as f64) * 5e-3;
//let ampl = (dragging_vel * 0.01) as f64;
self.inertia = Some(Inertia::new(ampl.to_radians(), axis))
self.inertia = Some(Inertia::new(ampl.to_radians(), axis, self.north_up))
}
pub(crate) fn set_view_center_pos_angle(&mut self, theta: ArcDeg<f64>) {
pub(crate) fn set_position_angle(&mut self, theta: ArcDeg<f64>) {
self.camera
.set_center_pos_angle(theta.into(), &self.projection);
.set_position_angle(theta.into(), &self.projection);
// New tiles can be needed and some tiles can be removed
self.request_for_new_tiles = true;
self.request_redraw = true;
}
pub(crate) fn get_north_shift_angle(&self) -> Angle<f64> {
self.camera.get_center_pos_angle()
pub(crate) fn get_position_angle(&self) -> Angle<f64> {
self.camera.get_position_angle()
}
pub(crate) fn set_fov(&mut self, fov: f64) {
@@ -1592,21 +1593,64 @@ impl App {
.screen_to_model_space(&Vector2::new(s2x, s2y), &self.camera),
) {
let prev_pos = w1;
//let cur_pos = w1.truncate();
let cur_pos = w2;
//let next_pos = w2.truncate();
if prev_pos != cur_pos {
/* 1. Rotate by computing the angle between the last and current position */
let prev_cam_position = self.camera.get_center().clone();
// Apply the rotation to the camera to
// go from the current pos to the next position
let axis = prev_pos.cross(cur_pos).normalize();
if self.north_up {
let lonlat1 = prev_pos.lonlat();
let lonlat2 = cur_pos.lonlat();
let d = math::vector::angle3(&prev_pos, &cur_pos);
let dlon = lonlat2.lon() - lonlat1.lon();
let dlat = lonlat2.lat() - lonlat1.lat();
self.camera.apply_lonlat_rotation(dlon, dlat, &self.projection);
self.prev_cam_position = *self.camera.get_center();
self.camera.apply_rotation(&(-axis), d, &self.projection);
// Detect if a pole has been crossed
let north_pole = Vector3::new(0.0, 1.0, 0.0);
let south_pole = Vector3::new(0.0, -1.0, 0.0);
let cross_north_pole = crate::math::lonlat::is_in(&prev_cam_position, &self.camera.get_center(), &north_pole);
let cross_south_pole = crate::math::lonlat::is_in(&prev_cam_position, &self.camera.get_center(), &south_pole);
let cross_pole = cross_north_pole | cross_south_pole;
// Detect if a pole has been crossed
let center = if cross_pole {
&prev_cam_position
} else {
self.camera.get_center()
};
let fov = self.camera.get_aperture();
let pole = if center.y >= 0.0 {
north_pole
} else {
south_pole
};
let c2p = crate::math::vector::angle3(center, &pole).to_radians();
let near_pole = c2p.abs() < 5e-3 * fov;
if near_pole || cross_pole {
// too near to the pole
let axis = center.cross(pole).normalize();
use crate::math::rotation::Rotation;
let new_center = Rotation::from_axis_angle(&axis, (-5e-3 * fov).to_angle()).rotate(&pole);
self.camera.set_center_xyz(&new_center, &self.projection);
self.camera.set_position_angle(0.0.to_angle(), &self.projection);
}
} else {
/* 1. Rotate by computing the angle between the last and current position */
let d = math::vector::angle3(&prev_pos, &cur_pos);
let axis = prev_pos.cross(cur_pos).normalize();
self.camera.apply_axis_rotation(&(-axis), d, &self.projection);
}
self.prev_cam_position = prev_cam_position;
self.request_for_new_tiles = true;
}
} else {
@@ -1614,6 +1658,10 @@ impl App {
}
}
pub(crate) fn lock_north_up(&mut self) {
self.north_up = true;
}
pub(crate) fn add_cmap(&mut self, label: String, cmap: Colormap) -> Result<(), JsValue> {
self.colormaps.add_cmap(label, cmap)
}

View File

@@ -14,6 +14,7 @@ const ID_R: &Matrix3<f64> = &Matrix3::new(
0.0, 0.0, 1.0,
);
use cgmath::{Vector3, InnerSpace};
use super::{fov::FieldOfView, view_hpx_cells::ViewHpxCells};
use crate::healpix::cell::HEALPixCell;
use crate::healpix::coverage::HEALPixCoverage;
@@ -91,7 +92,7 @@ use crate::{
};
use crate::LonLatT;
use cgmath::{SquareMatrix, Vector3};
use cgmath::SquareMatrix;
use wasm_bindgen::JsCast;
const MAX_DPI_LIMIT: f32 = 2.0;
@@ -621,7 +622,7 @@ impl CameraViewPort {
self.texture_depth
}
pub fn apply_rotation(
pub fn apply_axis_rotation(
&mut self,
axis: &cgmath::Vector3<f64>,
angle: Angle<f64>,
@@ -634,14 +635,29 @@ impl CameraViewPort {
self.update_rot_matrices(proj);
}
pub fn apply_lonlat_rotation(
&mut self,
dlon: Angle<f64>,
dlat: Angle<f64>,
proj: &ProjectionType
) {
let center = self.get_center();
let rot = Rotation::from_axis_angle(&Vector3::new(center.z, 0.0, -center.x).normalize(), dlat) * Rotation::from_axis_angle(&Vector3::unit_y(), -dlon) * Rotation::from_sky_position(&center);
self.set_rotation(&rot, proj);
}
/// center lonlat must be given in icrs frame
pub fn set_center(&mut self, lonlat: &LonLatT<f64>, proj: &ProjectionType) {
let icrs_pos: Vector3<_> = lonlat.vector();
let icrs_pos = lonlat.vector();
self.set_center_xyz(&icrs_pos, proj);
}
let center = CooSystem::ICRS.to(self.get_coo_system()) * icrs_pos;
pub fn set_center_xyz(&mut self, xyz: &Vector3<f64>, proj: &ProjectionType) {
let center = CooSystem::ICRS.to(self.get_coo_system()) * xyz;
let rot_to_center = Rotation::from_sky_position(&center);
let phi = self.get_center_pos_angle();
let phi = self.get_position_angle();
let third_euler_rot = Rotation::from_axis_angle(&center, phi);
let rot = third_euler_rot * rot_to_center;
@@ -651,7 +667,7 @@ impl CameraViewPort {
self.set_rotation(&rot, proj);
}
pub fn set_center_pos_angle(&mut self, phi: Angle<f64>, proj: &ProjectionType) {
pub fn set_position_angle(&mut self, phi: Angle<f64>, proj: &ProjectionType) {
let c = self.center;
let rot_to_center = Rotation::from_sky_position(&c);
let third_euler_rot = Rotation::from_axis_angle(&c, phi);
@@ -660,7 +676,7 @@ impl CameraViewPort {
self.set_rotation(&total_rot, proj);
}
fn set_rotation(&mut self, rot: &Rotation<f64>, proj: &ProjectionType) {
pub fn set_rotation(&mut self, rot: &Rotation<f64>, proj: &ProjectionType) {
self.w2m_rot = *rot;
self.update_rot_matrices(proj);
@@ -797,7 +813,7 @@ impl CameraViewPort {
self.coo_sys
}
pub fn get_center_pos_angle(&self) -> Angle<f64> {
pub fn get_position_angle(&self) -> Angle<f64> {
(self.w2m.x.y).atan2(self.w2m.y.y).to_angle()
}
}

View File

@@ -4,7 +4,6 @@ use crate::camera::CameraViewPort;
use crate::math::angle::ToAngle;
use crate::math::projection::ProjectionType;
use crate::time::{DeltaTime, Time};
/// State for inertia
pub struct Inertia {
// Initial angular distance
@@ -14,15 +13,17 @@ pub struct Inertia {
axis: Vector3<f64>,
// The time when the inertia begins
time_start: Time,
north_up: bool,
}
impl Inertia {
pub fn new(ampl: f64, axis: Vector3<f64>) -> Self {
pub fn new(ampl: f64, axis: Vector3<f64>, north_up: bool) -> Self {
Inertia {
time_start: Time::now(),
ampl: ampl,
ampl,
speed: ampl,
axis: axis,
axis,
north_up
}
}
@@ -35,7 +36,7 @@ impl Inertia {
// where:
// * k is the stiffness of the ressort
// * m is its mass
let w0 = 10.0;
let w0 = 7.0;
// The angular distance goes from d0 to 0.0
//self.speed = self.ampl * ((-w0 * t).exp());
// The angular distance goes from d0 to 0.0
@@ -43,7 +44,11 @@ impl Inertia {
/*let alpha = 1_f32 + (0_f32 - 1_f32) * (10_f32 * t + 1_f32) * (-10_f32 * t).exp();
let alpha = alpha * alpha;
let fov = start_fov * (1_f32 - alpha) + goal_fov * alpha;*/
camera.apply_rotation(&self.axis, self.speed.to_angle(), proj)
camera.apply_axis_rotation(&self.axis, self.speed.to_angle(), proj);
if self.north_up {
camera.set_position_angle(0.0.to_angle(), proj);
}
}
pub fn get_start_ampl(&self) -> f64 {

View File

@@ -534,18 +534,18 @@ impl WebClient {
/// # Arguments
///
/// * `theta` - The rotation angle in degrees
#[wasm_bindgen(js_name = setViewCenter2NorthPoleAngle)]
pub fn set_view_center_pos_angle(&mut self, theta: f64) -> Result<(), JsValue> {
#[wasm_bindgen(js_name = setRotation)]
pub fn set_rotation(&mut self, theta: f64) -> Result<(), JsValue> {
let theta = ArcDeg(theta);
self.app.set_view_center_pos_angle(theta);
self.app.set_position_angle(theta);
Ok(())
}
/// Get the absolute orientation angle of the view
#[wasm_bindgen(js_name = getViewCenter2NorthPoleAngle)]
pub fn get_north_shift_angle(&mut self) -> Result<f64, JsValue> {
let phi = self.app.get_north_shift_angle();
#[wasm_bindgen(js_name = getRotation)]
pub fn get_rotation(&mut self) -> Result<f64, JsValue> {
let phi = self.app.get_position_angle();
Ok(phi.to_degrees())
}
@@ -610,6 +610,11 @@ impl WebClient {
Ok(())
}
#[wasm_bindgen(js_name = lockNorthUp)]
pub fn lock_north_up(&mut self) {
self.app.lock_north_up();
}
/// Get the center of the view
///
/// This returns a javascript array of size 2.

View File

@@ -400,7 +400,7 @@ where
S::max_value().to_angle()
}
pub fn to_radians(&self) -> S {
pub const fn to_radians(&self) -> S {
self.rad
}

View File

@@ -204,3 +204,10 @@ pub fn unproj_from_screen(
.screen_to_model_space(&xy, camera)
.map(|model_pos| model_pos.lonlat())
}
#[inline]
pub fn is_in(v1: &Vector3<f64>, v2: &Vector3<f64>, v: &Vector3<f64>) -> bool {
let theta = crate::math::vector::angle3(&v1, &v2).abs();
crate::math::vector::angle3(&v1, &v).abs() < theta && crate::math::vector::angle3(&v, &v2).abs() < theta
}

View File

@@ -123,7 +123,8 @@ import { Polyline } from "./shapes/Polyline";
* @property {boolean} [showReticle=true] - Whether to show the reticle.
* @property {boolean} [showCatalog=true] - Whether to show the catalog.
* @property {boolean} [showCooGrid=true] - Whether the coordinates grid should be shown at startup.
*
* @property {boolean} [inertia=true] - Whether mouse release triggers an inertia effect.
* @property {boolean} [lockNorthUp=false] - If true, the north pole will always be up.
* @property {boolean} [fullScreen=false] - Whether to start in full-screen mode.
* @property {string} [reticleColor="rgb(178, 50, 178)"] - Color of the reticle in RGB format.
* @property {number} [reticleSize=22] - Size of the reticle.
@@ -504,12 +505,18 @@ export let Aladin = (function () {
this.samp = new SAMPConnector(this);
}
// lockNorthUp option
this.lockNorthUp = options.lockNorthUp || false;
if (this.lockNorthUp) {
this.wasm.lockNorthUp();
}
if (options.inertia !== undefined) {
this.wasm.setInertia(options.inertia);
}
if (options.northPoleOrientation) {
this.setViewCenter2NorthPoleAngle(options.northPoleOrientation);
this.setRotation(options.northPoleOrientation);
}
};
@@ -1996,7 +2003,7 @@ export let Aladin = (function () {
* view in the counter clockwise order (or towards the east)
*/
Aladin.prototype.setRotation = function (rotation) {
this.view.setViewCenter2NorthPoleAngle(rotation);
this.view.setRotation(rotation);
};
@@ -2009,7 +2016,7 @@ export let Aladin = (function () {
* @returns {number} - Angle between the position center and the north pole
*/
Aladin.prototype.getRotation = function () {
return this.view.wasm.getViewCenter2NorthPoleAngle();
return this.view.wasm.getRotation();
};
/**

View File

@@ -592,7 +592,7 @@ export let View = (function () {
const [lon, lat] = view.aladin.pix2world(xymouse.x, xymouse.y, 'icrs');
view.pointTo(lon, lat);
// reset the rotation around center view
view.setViewCenter2NorthPoleAngle(0.0);
view.setRotation(0.0);
}
catch (err) {
return;
@@ -752,6 +752,11 @@ export let View = (function () {
if (e.type === 'touchstart' && e.targetTouches && e.targetTouches.length >= 2) {
view.dragging = false;
// Do not start the pinched rotation if the north up is locked
if (view.aladin.lockNorthUp === true) {
return;
}
view.pinchZoomParameters.isPinching = true;
view.pinchZoomParameters.initialZoomFactor = view.zoomFactor;
view.pinchZoomParameters.initialDistance = Math.sqrt(Math.pow(e.targetTouches[0].clientX - e.targetTouches[1].clientX, 2) + Math.pow(e.targetTouches[0].clientY - e.targetTouches[1].clientY, 2));
@@ -1026,7 +1031,7 @@ export let View = (function () {
// planetary survey case
rotation -= fingerAngleDiff;
}
view.setViewCenter2NorthPoleAngle(rotation);
view.setRotation(rotation);
}
// zoom
@@ -1540,8 +1545,8 @@ export let View = (function () {
});
}
View.prototype.setViewCenter2NorthPoleAngle = function(rotation) {
this.wasm.setViewCenter2NorthPoleAngle(rotation);
View.prototype.setRotation = function(rotation) {
this.wasm.setRotation(rotation);
}
View.prototype.setGridOptions = function (options) {