diff --git a/vizmat-app/mobile-gesture.js b/vizmat-app/mobile-gesture.js index a222ea2..a2642cd 100644 --- a/vizmat-app/mobile-gesture.js +++ b/vizmat-app/mobile-gesture.js @@ -40,229 +40,142 @@ }; }; - const emitIf = (condition, payload) => { - if (!condition) return false; - emitTouchGesture(payload); - return true; - }; - if (canvas) { canvas.style.touchAction = "none"; - if (window.PointerEvent) { - const pointerState = new Map(); - - const removePointer = (event) => { - if (event.pointerType !== "touch") return; - if (!event.currentTarget || !pointerState.has(event.pointerId)) return; - pointerState.delete(event.pointerId); + const pointerState = new Map(); - if (pointerState.size === 0) { - clearTouchState(); - return; - } + const removePointer = (event) => { + if (event.pointerType !== "touch") return; + if (!event.currentTarget || !pointerState.has(event.pointerId)) return; + pointerState.delete(event.pointerId); - if (pointerState.size === 1) { - touchPanState.one = [...pointerState.values()][0]; - touchPanState.two = { - active: false, - midpoint: null, - distance: null, - }; - return; - } + if (pointerState.size === 0) { + clearTouchState(); + return; + } - const points = [...pointerState.values()]; + if (pointerState.size === 1) { + touchPanState.one = [...pointerState.values()][0]; touchPanState.two = { - active: true, - midpoint: getMidpoint(points[0], points[1]), - distance: getDistance(points[0], points[1]), + active: false, + midpoint: null, + distance: null, }; + return; + } + + const points = [...pointerState.values()]; + touchPanState.two = { + active: true, + midpoint: getMidpoint(points[0], points[1]), + distance: getDistance(points[0], points[1]), }; + }; - const updateSinglePointer = (point) => { - if (!touchPanState.one) { - touchPanState.one = point; - return; - } - - const dx = point.x - touchPanState.one.x; - const dy = point.y - touchPanState.one.y; - const emitted = emitIf(dx !== 0 || dy !== 0, { - gesture: "Rotate", - dx, - dy, - scale_delta: 0, - }); - if (emitted) { - touchPanState.one = point; - } - }; - - const updateTwoPointer = (a, b) => { - if (!touchPanState.two.active) { - touchPanState.two = { - active: true, - midpoint: getMidpoint(a, b), - distance: getDistance(a, b), - }; - return; - } - - const midpoint = getMidpoint(a, b); - const distance = getDistance(a, b); - const panDx = midpoint.x - touchPanState.two.midpoint.x; - const panDy = midpoint.y - touchPanState.two.midpoint.y; - const scaleDelta = touchPanState.two.distance > 0 - ? (distance - touchPanState.two.distance) / touchPanState.two.distance - : 0; - - const emitted = emitIf(panDx !== 0 || panDy !== 0 || scaleDelta !== 0, { + const updateTwoPointer = (a, b) => { + if (!touchPanState.two.active) { + touchPanState.two = { + active: true, + midpoint: getMidpoint(a, b), + distance: getDistance(a, b), + }; + return; + } + + const midpoint = getMidpoint(a, b); + const distance = getDistance(a, b); + const panDx = midpoint.x - touchPanState.two.midpoint.x; + const panDy = midpoint.y - touchPanState.two.midpoint.y; + const scaleDelta = touchPanState.two.distance > 0 + ? (distance - touchPanState.two.distance) / touchPanState.two.distance + : 0; + + if (panDx !== 0 || panDy !== 0 || scaleDelta !== 0) { + emitTouchGesture({ gesture: "TwoFinger", - dx: panDx, - dy: panDy, + x: panDx, + y: panDy, scale_delta: scaleDelta, }); + touchPanState.two.midpoint = midpoint; + touchPanState.two.distance = distance; + } + }; - if (emitted) { - touchPanState.two.midpoint = midpoint; - touchPanState.two.distance = distance; - } - }; - - canvas.addEventListener( - "pointerdown", - (event) => { - if (event.pointerType !== "touch") return; - if (!event.target) return; - const point = toVec(event); - pointerState.set(event.pointerId, point); - event.currentTarget.setPointerCapture(event.pointerId); - touchPanState.one = point; - touchPanState.two = { - active: false, - midpoint: null, - distance: null, - }; - event.preventDefault(); - }, - { passive: false }, - ); - - canvas.addEventListener( - "pointermove", - (event) => { - if (event.pointerType !== "touch") return; - if (!pointerState.has(event.pointerId)) return; - - const point = toVec(event); - pointerState.set(event.pointerId, point); - - if (pointerState.size === 1) { - updateSinglePointer(point); - } else if (pointerState.size >= 2) { - const points = [...pointerState.values()]; - const a = points[0]; - const b = points[1]; - updateTwoPointer(a, b); - } - - event.preventDefault(); - }, - { passive: false }, - ); + canvas.addEventListener( + "pointerdown", + (event) => { + if (event.pointerType !== "touch") return; + if (!event.target) return; + const point = toVec(event); + pointerState.set(event.pointerId, point); + event.currentTarget.setPointerCapture(event.pointerId); + const x = point.x; + const y = point.y; + emitTouchGesture({ + gesture: "OneFingerDown", + x, + y, + scale_delta: 0, + }); + touchPanState.two = { + active: false, + midpoint: null, + distance: null, + }; + event.preventDefault(); + }, + { passive: false }, + ); - canvas.addEventListener( - "pointerup", - removePointer, - { passive: false }, - ); - canvas.addEventListener("pointercancel", removePointer, { - passive: false, - }); - canvas.addEventListener("pointerleave", removePointer, { - passive: false, - }); - canvas.addEventListener("pointerout", removePointer, { passive: false }); - canvas.addEventListener("pointerlostcapture", removePointer); - } else { - canvas.addEventListener( - "touchstart", - (event) => { - if (!event.target) return; - if (event.touches.length === 1) { - touchPanState.one = toVec(event.touches[0]); - touchPanState.two.active = false; - } else if (event.touches.length === 2) { - const a = toVec(event.touches[0]); - const b = toVec(event.touches[1]); - touchPanState.two = { - active: true, - midpoint: getMidpoint(a, b), - distance: getDistance(a, b), - }; - touchPanState.one = null; - } - event.preventDefault(); - }, - { passive: false }, - ); + canvas.addEventListener( + "pointermove", + (event) => { + if (event.pointerType !== "touch") return; + if (!pointerState.has(event.pointerId)) return; - canvas.addEventListener( - "touchmove", - (event) => { - if (event.touches.length === 1 && touchPanState.one) { - const current = toVec(event.touches[0]); - const dx = current.x - touchPanState.one.x; - const dy = current.y - touchPanState.one.y; + const point = toVec(event); + pointerState.set(event.pointerId, point); - if (dx !== 0 || dy !== 0) { - emitTouchGesture({ - gesture: "Rotate", - dx, - dy, - scale_delta: 0, - }); - touchPanState.one = current; - } - event.preventDefault(); + if (pointerState.size === 1) { + if (!touchPanState.one) { + touchPanState.one = point; return; } - if (event.touches.length === 2 && touchPanState.two.active) { - const a = toVec(event.touches[0]); - const b = toVec(event.touches[1]); - const midpoint = getMidpoint(a, b); - const distance = getDistance(a, b); - - const panDx = midpoint.x - touchPanState.two.midpoint.x; - const panDy = midpoint.y - touchPanState.two.midpoint.y; - const scaleDelta = touchPanState.two.distance > 0 - ? (distance - touchPanState.two.distance) / - touchPanState.two.distance - : 0; - - if (panDx !== 0 || panDy !== 0 || scaleDelta !== 0) { - emitTouchGesture({ - gesture: "TwoFinger", - dx: panDx, - dy: panDy, - scale_delta: scaleDelta, - }); - touchPanState.two.midpoint = midpoint; - touchPanState.two.distance = distance; - } + const x = point.x; + const y = point.y; + emitTouchGesture({ + gesture: "OneFingerMove", + x, + y, + scale_delta: 0, + }); + } else if (pointerState.size >= 2) { + const points = [...pointerState.values()]; + const a = points[0]; + const b = points[1]; + updateTwoPointer(a, b); + } - event.preventDefault(); - } - }, - { passive: false }, - ); - } + event.preventDefault(); + }, + { passive: false }, + ); - const clearTouch = () => clearTouchState(); - canvas.addEventListener("touchend", clearTouch, { passive: false }); - canvas.addEventListener("touchcancel", clearTouch, { passive: false }); - canvas.addEventListener("touchleave", clearTouch, { passive: false }); + canvas.addEventListener( + "pointerup", + removePointer, + { passive: false }, + ); + canvas.addEventListener("pointercancel", removePointer, { + passive: false, + }); + canvas.addEventListener("pointerleave", removePointer, { + passive: false, + }); + canvas.addEventListener("pointerout", removePointer, { passive: false }); + canvas.addEventListener("pointerlostcapture", removePointer); } })(); diff --git a/vizmat-core/src/lib.rs b/vizmat-core/src/lib.rs index 2890a06..cf2338a 100644 --- a/vizmat-core/src/lib.rs +++ b/vizmat-core/src/lib.rs @@ -45,7 +45,8 @@ use crate::ui::{ transition_to_running_on_structure_loaded, update_atom_hover_cache, update_atom_hover_label, update_bond_order_legend, update_color_mode_availability, update_file_ui, update_gizmo_viewport, update_scene, update_selected_atom_from_click, - update_structure_loading_overlay, AppUiState, CatalogLoadChannel, TouchGestureState, + update_structure_loading_overlay, AppUiState, ArcballState, CatalogLoadChannel, + OneFingerTouchGestureKind, OneFingerTouchGestureState, TwoFingersTouchGestureState, }; use crate::ui::{setup_buttons, spawn_axis}; @@ -87,15 +88,16 @@ pub enum WebEvent { }, TouchGesture { kind: TouchGestureKind, - dx: f32, - dy: f32, + x: f32, + y: f32, scale_delta: f32, }, } #[derive(Clone, Copy, Debug)] pub enum TouchGestureKind { - Rotate, + OneFingerDown, + OneFingerMove, TwoFinger, } @@ -108,13 +110,16 @@ impl<'de> serde::Deserialize<'de> for TouchGestureKind { let normalized = raw.trim().to_ascii_lowercase().replace(['_', '-'], ""); match normalized.as_str() { - "rotate" => Ok(Self::Rotate), + "onefingerdown" => Ok(Self::OneFingerDown), + "onefingermove" => Ok(Self::OneFingerMove), "twofinger" => Ok(Self::TwoFinger), _ => Err(de::Error::unknown_variant( &raw, &[ - "Rotate", - "rotate", + "OneFingerMove", + "OnefingerDown", + "Onefingermove", + "Onefingerdown", "TwoFinger", "two_finger", "Two_Finger", @@ -130,8 +135,8 @@ impl<'de> serde::Deserialize<'de> for TouchGestureKind { #[serde(rename_all = "snake_case")] struct TouchGesturePayload { gesture: TouchGestureKind, - dx: f32, - dy: f32, + x: f32, + y: f32, scale_delta: f32, } @@ -369,8 +374,8 @@ fn register_touch_gesture_listener() -> Option<()> { send_event(WebEvent::TouchGesture { kind: payload.gesture, - dx: payload.dx, - dy: payload.dy, + x: payload.x, + y: payload.y, scale_delta: payload.scale_delta, }); }, @@ -400,10 +405,12 @@ pub fn run_app() { }) .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() + .init_resource::() .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .init_state::() .add_event::() .add_event::() @@ -536,7 +543,8 @@ fn web_event_observer( mut file_drag_drop: ResMut, mut next_ui_state: ResMut>, mut commands: Commands, - mut touch_gesture_state: ResMut, + mut two_fingers_touch_gesture_state: ResMut, + mut one_finger_touch_gesture_state: ResMut, ) { match trigger.event() { WebEvent::Drop { @@ -581,16 +589,21 @@ fn web_event_observer( } WebEvent::TouchGesture { kind, - dx, - dy, + x, + y, scale_delta, } => match kind { - TouchGestureKind::Rotate => { - touch_gesture_state.rotate += Vec2::new(*dx, *dy); + TouchGestureKind::OneFingerMove => { + one_finger_touch_gesture_state.position = Vec2::new(*x, *y); + one_finger_touch_gesture_state.kind = OneFingerTouchGestureKind::Move; + } + TouchGestureKind::OneFingerDown => { + one_finger_touch_gesture_state.position = Vec2::new(*x, *y); + one_finger_touch_gesture_state.kind = OneFingerTouchGestureKind::Down; } TouchGestureKind::TwoFinger => { - touch_gesture_state.pan += Vec2::new(*dx, *dy); - touch_gesture_state.zoom += *scale_delta; + two_fingers_touch_gesture_state.pan += Vec2::new(*x, *y); + two_fingers_touch_gesture_state.zoom += *scale_delta; } }, } diff --git a/vizmat-core/src/ui.rs b/vizmat-core/src/ui.rs index d959faf..fcba4ce 100644 --- a/vizmat-core/src/ui.rs +++ b/vizmat-core/src/ui.rs @@ -136,6 +136,42 @@ pub(crate) struct StructurePickerState { pub(crate) visible: bool, } +// ---- for arcball control --- +#[derive(Resource, Default)] +pub struct ArcballState { + pub prev_cursor: Option, +} + +fn normalize_mouse(pos: Vec2, window: Vec2) -> Vec2 { + Vec2::new( + (2.0 * pos.x - window.x) / window.x, + (window.y - 2.0 * pos.y) / window.y, + ) +} + +fn arcball_vector(p: Vec2) -> Vec3 { + let d2 = p.length_squared(); + + if d2 <= 1.0 { + Vec3::new(p.x, p.y, (1.0 - d2).sqrt()) + } else { + Vec3::new(p.x, p.y, 0.0).normalize() + } +} + +fn arcball_quat(v0: Vec3, v1: Vec3) -> Quat { + let cross = v1.cross(v0); + let dot = v0.dot(v1); + + if cross.length_squared() < 1e-10 { + Quat::IDENTITY + } else { + Quat::from_xyzw(cross.x, cross.y, cross.z, 1.0 + dot).normalize() + } +} + +// end of helpers for arcball ------ + #[derive(Component)] pub(crate) struct HudTopBar; @@ -2119,13 +2155,41 @@ pub(crate) struct CameraRig { initial_scale: Vec3, } +impl Default for CameraRig { + fn default() -> Self { + let target = Vec3::ZERO; + let transform = Transform::from_xyz(5.0, 5.0, 5.0).looking_at(target, Vec3::Y); + + Self { + target, + distance: transform.translation.distance(target), + initial_target: target, + initial_translation: transform.translation, + initial_rotation: transform.rotation, + initial_scale: transform.scale, + } + } +} + #[derive(Resource, Default)] -pub(crate) struct TouchGestureState { - pub(crate) rotate: Vec2, +pub(crate) struct TwoFingersTouchGestureState { pub(crate) pan: Vec2, pub(crate) zoom: f32, } +#[derive(Default)] +pub(crate) enum OneFingerTouchGestureKind { + #[default] + Move, + Down, +} + +#[derive(Resource, Default)] +pub(crate) struct OneFingerTouchGestureState { + pub(crate) kind: OneFingerTouchGestureKind, + pub(crate) position: Vec2, +} + fn structure_center_and_extents(sv: &StructureView) -> Option<(Vec3, Vec3)> { let sites = sv.sites(); let first = sites.first()?; @@ -2977,12 +3041,6 @@ pub fn setup_cameras(mut commands: Commands, windows: Query<&Window>) { .saturating_sub(viewport_size.y + GIZMO_VIEWPORT_MARGIN_PX); let viewport_position = UVec2::new(GIZMO_VIEWPORT_MARGIN_PX, bottom_left_y); - let camera_transform = Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y); - let initial_translation = camera_transform.translation; - let initial_rotation = camera_transform.rotation; - let initial_scale = camera_transform.scale; - let initial_target = Vec3::ZERO; - commands.spawn(( Transform::default(), GlobalTransform::default(), @@ -3000,6 +3058,7 @@ pub fn setup_cameras(mut commands: Commands, windows: Query<&Window>) { order: 0, ..default() }, + Projection::default(), IsDefaultUiCamera, Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), LAYER_CANVAS, @@ -3027,14 +3086,7 @@ pub fn setup_cameras(mut commands: Commands, windows: Query<&Window>) { .id(); commands.insert_resource(MainCameraEntity(camera_entity)); - commands.insert_resource(CameraRig { - target: initial_target, - distance: initial_translation.distance(initial_target), - initial_target, - initial_translation, - initial_rotation, - initial_scale, - }); + commands.insert_resource(CameraRig::default()); } pub(crate) fn update_gizmo_viewport( @@ -3318,153 +3370,271 @@ pub(crate) fn camera_controls( time: Res