From a89ea6a0d6484d5abfbb0e15473bdd61b9b280cd Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Mon, 16 Oct 2023 20:25:39 -0400 Subject: [PATCH] Super rough hit-testing logic for rotator rings --- Cargo.lock | 241 ++++++++++++++++++++ studio/Cargo.toml | 14 +- studio/src/interaction/gizmos/controller.rs | 193 ++++++++++++++++ studio/src/interaction/gizmos/mod.rs | 17 +- studio/src/interaction/gizmos/model.rs | 3 + studio/src/interaction/gizmos/view.rs | 27 ++- 6 files changed, 481 insertions(+), 14 deletions(-) create mode 100644 studio/src/interaction/gizmos/controller.rs diff --git a/Cargo.lock b/Cargo.lock index 5c0728a..84c0515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,6 +698,19 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c7586401a46f7d8e436028225c1df5288f2e0082d066b247a82466fea155c6" +[[package]] +name = "bevy_rapier3d" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12402872b857ba490f1040ab6212915bd9bf25f8584b31f2c43cef41b33f3be4" +dependencies = [ + "bevy", + "bitflags 1.3.2", + "log", + "nalgebra", + "rapier3d", +] + [[package]] name = "bevy_reflect" version = "0.11.3" @@ -1337,6 +1350,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1347,6 +1374,40 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1391,6 +1452,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encase" version = "0.6.1" @@ -2033,6 +2100,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libudev-sys" version = "0.1.4" @@ -2086,12 +2159,31 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.24.0" @@ -2175,6 +2267,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "nalgebra" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" +dependencies = [ + "approx", + "glam", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ndk" version = "0.7.0" @@ -2274,6 +2394,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -2313,6 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2449,6 +2579,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "optional" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978aa494585d3ca4ad74929863093e87cac9790d81fe7aba2b3dc2890643a0fc" + [[package]] name = "orbclient" version = "0.3.46" @@ -2502,6 +2638,27 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parry3d" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55dc0e6db79bddbc5fd583569f7356cdcc63e1e9b2b93a9ab70dd8e717160e0" +dependencies = [ + "approx", + "arrayvec", + "bitflags 1.3.2", + "downcast-rs", + "either", + "nalgebra", + "num-derive", + "num-traits", + "rustc-hash", + "simba", + "slab", + "smallvec", + "spade", +] + [[package]] name = "paste" version = "1.0.14" @@ -2610,12 +2767,39 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +[[package]] +name = "rapier3d" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a8a0bd9d3135f7b4eb45d0796540e7bab47b6b7c974f90567ccc5a0454f42b" +dependencies = [ + "approx", + "arrayvec", + "bit-vec", + "bitflags 1.3.2", + "crossbeam", + "downcast-rs", + "instant", + "nalgebra", + "num-derive", + "num-traits", + "parry3d", + "rustc-hash", + "simba", +] + [[package]] name = "raw-window-handle" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rectangle-pack" version = "0.4.2" @@ -2681,6 +2865,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +[[package]] +name = "robust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea" + [[package]] name = "rodio" version = "0.17.1" @@ -2732,6 +2922,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safe_arch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2793,6 +2992,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2835,6 +3047,18 @@ dependencies = [ "serde", ] +[[package]] +name = "spade" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e65803986868d2372c582007c39ba89936a36ea5f236bf7a7728dc258f04f9" +dependencies = [ + "num-traits", + "optional", + "robust", + "smallvec", +] + [[package]] name = "spirv" version = "0.2.0+1.5.4" @@ -2856,6 +3080,7 @@ name = "studio" version = "0.1.0" dependencies = [ "bevy", + "bevy_rapier3d", "bitflags 2.4.0", "console_error_panic_hook", "parking_lot", @@ -3096,6 +3321,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3351,6 +3582,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wide" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebecebefc38ff1860b4bc47550bbfa63af5746061cf0d29fcd7fa63171602598" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.0.2" diff --git a/studio/Cargo.toml b/studio/Cargo.toml index e41b8cc..8229cd4 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -13,13 +13,15 @@ name = "studio-standalone" path = "src/main.rs" [dependencies] -wasm-bindgen = { version = "0.2.84" } +bevy = { workspace = true } +bevy_rapier3d = { version = "0.22.0" } console_error_panic_hook = { version = "0.1.7", optional = true } -parking_lot = "0.12.1" bitflags = "2.4.0" +parking_lot = "0.12.1" +wasm-bindgen = { version = "0.2.84" } [features] -wasm = ["dep:console_error_panic_hook"] - -[dependencies.bevy] -workspace = true +wasm = [ + "dep:console_error_panic_hook", + "bevy_rapier3d/wasm-bindgen", +] diff --git a/studio/src/interaction/gizmos/controller.rs b/studio/src/interaction/gizmos/controller.rs new file mode 100644 index 0000000..864080b --- /dev/null +++ b/studio/src/interaction/gizmos/controller.rs @@ -0,0 +1,193 @@ +use core::fmt; +use std::f32::consts; + +use bevy::{prelude::*, window::PrimaryWindow}; +use bevy_rapier3d::prelude::*; + +use crate::{math::NearlyEq, orbit_cam::OrbitCam}; + +use super::{ + model::{self, Axes, Control, Gizmo, Hovered}, + InteractionMode, TransformSpace, TransformType, +}; + +#[derive(Component, Deref, DerefMut, Clone, Copy, Debug)] +pub(super) struct GizmoController(pub Control); + +pub(super) fn update_spawns( + mut cmd: Commands, + q_needs_spawn: Query<(Entity, &Gizmo), Without>, + q_needs_despawn: Query, Without)>, + mode: Res, +) { + for (ent, gizmo) in q_needs_spawn.iter() { + let mut ent_cmd = cmd.entity(ent); + + ent_cmd.insert((RigidBody::KinematicPositionBased, Sensor)); + + if mode.transform.contains(TransformType::ROTATE) { + ent_cmd.insert(GizmoController(Control::Rotation(Axes::XYZ))); + + let Some(radius) = gizmo.iter().find_map(|el| match el.control { + Control::Rotation(axes) if !axes.contains(Axes::SCREEN) => match el.shape { + model::Shape::Ring(ring) => Some(ring.radius), + _ => None, + }, + _ => None, + }) else { + return; + }; + + ent_cmd.insert(Collider::ball(radius)); + } + } + + for ent in q_needs_despawn.iter() { + cmd.entity(ent) + .remove::<(GizmoController, RigidBody, Sensor)>(); + } +} + +pub(super) fn update(mut q_collider: Query<(&mut Collider, &Gizmo, &GizmoController)>) { + for (mut collider, gizmo, ctrl) in q_collider.iter_mut() { + match **ctrl { + Control::Rotation(_) => { + let Some(radius) = gizmo.iter().find_map(|el| match el.control { + Control::Rotation(axes) if !axes.contains(Axes::SCREEN) => match el.shape { + model::Shape::Ring(ring) => Some(ring.radius), + _ => None, + }, + _ => None, + }) else { + continue; + }; + + let Some(mut ball) = collider.as_ball_mut() else { + continue; + }; + + ball.set_radius(radius); + } + _ => { + // TODO + } + } + } +} + +pub(super) fn hit_test( + mut cmd: Commands, + q_gizmo: Query<(Entity, &GlobalTransform, &GizmoController)>, + q_window: Query<&Window, With>, + q_camera: Query<(&Camera, &GlobalTransform), With>, + rapier_cx: Res, + mode: Res, +) { + let win = q_window.single(); + let (cam, cam_xform) = q_camera.single(); + + if let Some(ray) = win + .cursor_position() + .and_then(|pos| cam.viewport_to_world(cam_xform, pos)) + { + if let Some((ent, t)) = rapier_cx.cast_ray( + ray.origin, + ray.direction, + f32::MAX, + false, + QueryFilterFlags::EXCLUDE_SOLIDS.into(), + ) { + let hit_pos = ray.get_point(t); + + if let Ok((_, world_xform, _gizmo_ctrl)) = q_gizmo.get(ent) { + let cam_xform = cam_xform.compute_transform(); + let world_xform_mat = world_xform.compute_matrix(); + let world_xform = Transform::from_matrix(world_xform_mat); + let basis = match mode.space { + TransformSpace::Local => world_xform_mat, + TransformSpace::World => Mat4::from_translation(world_xform.translation), + }; + + let normal = (hit_pos - world_xform.translation).normalize(); + let to_camera = (cam_xform.translation - world_xform.translation).normalize(); + + let perpendicularity = 1. - Vec3::dot(to_camera, normal).abs(); + let threshold = (perpendicularity * (consts::PI / 5.)).max(2_f32.to_radians()); + + let local_pos = basis.inverse().transform_point3(hit_pos); + let coords = SphericalCoords::from(local_pos); + + let mut proximity_results = [ + (coords.heading.abs(), Axes::X), + (coords.pitch.abs(), Axes::Y), + ((90_f32.to_radians() - coords.heading.abs()).abs(), Axes::Z), + ]; + proximity_results + .sort_by(|(prox_a, _), (prox_b, _)| prox_a.partial_cmp(prox_b).unwrap()); + + let (prox, axis) = proximity_results[0]; + let hit_axes = if prox < threshold { axis } else { Axes::XYZ }; + + cmd.entity(ent).insert(Hovered(Control::Rotation(hit_axes))); + } + } else { + for (ent, _, _) in q_gizmo.iter() { + cmd.entity(ent).remove::(); + } + } + } +} + +struct SphericalCoords { + radius: f32, + heading: f32, + pitch: f32, +} + +impl Default for SphericalCoords { + fn default() -> Self { + Self { + radius: 0., + heading: 0., + pitch: 0., + } + } +} + +impl From for SphericalCoords { + fn from(value: Vec3) -> Self { + let Vec3 { x, y, z } = value; + + let radius = value.length(); + if radius.nearly_eq(0.) { + return Self::default(); + } + + let pitch = f32::asin(-y / radius); + let heading = if (pitch - 90_f32.to_radians()).abs() < 1e-3_f32 { + 0. + } else { + f32::atan2(x, z) + }; + + Self { + radius, + heading, + pitch, + } + } +} + +impl fmt::Display for SphericalCoords { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let precision = f.precision().unwrap_or(3); + write!( + f, + "(r {:.3$}, h {:.3$}°, p {:.3$}°)", + self.radius, + self.heading.to_degrees(), + self.pitch.to_degrees(), + precision, + ) + } +} diff --git a/studio/src/interaction/gizmos/mod.rs b/studio/src/interaction/gizmos/mod.rs index 10ee28b..ffb5f3a 100644 --- a/studio/src/interaction/gizmos/mod.rs +++ b/studio/src/interaction/gizmos/mod.rs @@ -1,8 +1,10 @@ mod composition; +mod controller; mod model; mod view; use bevy::prelude::*; +use bevy_rapier3d::prelude::{NoUserData, RapierPhysicsPlugin}; use super::{InteractionMode, Selected, TransformSpace, TransformType}; @@ -11,8 +13,19 @@ pub(super) struct StudioGizmosPlugin; impl Plugin for StudioGizmosPlugin { fn build(&self, app: &mut App) { app.insert_resource(StudioGizmoConfig::default()) - .add_systems(PreUpdate, (model::update_spawns, view::configure)) - .add_systems(Update, model::update) + .add_plugins(RapierPhysicsPlugin::::default()) + .add_systems( + PreUpdate, + ( + model::update_spawns, + controller::update_spawns, + view::configure, + ), + ) + .add_systems( + Update, + (model::update, controller::update, controller::hit_test), + ) .add_systems(PostUpdate, view::draw); } } diff --git a/studio/src/interaction/gizmos/model.rs b/studio/src/interaction/gizmos/model.rs index 1b61b88..ac74198 100644 --- a/studio/src/interaction/gizmos/model.rs +++ b/studio/src/interaction/gizmos/model.rs @@ -57,6 +57,9 @@ pub(super) fn update( #[derive(Component, Deref, DerefMut, Clone, Default, Debug)] pub(super) struct Gizmo(pub Vec); +#[derive(Component, Deref, DerefMut, Clone, Debug)] +pub(super) struct Hovered(pub Control); + #[derive(Clone, Copy, Debug)] pub(super) struct GizmoElement { pub shape: Shape, diff --git a/studio/src/interaction/gizmos/view.rs b/studio/src/interaction/gizmos/view.rs index eda7ff9..f5e59aa 100644 --- a/studio/src/interaction/gizmos/view.rs +++ b/studio/src/interaction/gizmos/view.rs @@ -2,10 +2,10 @@ use std::f32::consts; use bevy::{prelude::*, window::PrimaryWindow}; -use crate::math::Lerp; +use crate::math::{Lerp, NearlyEq}; use super::{ - model::{Arc, Axes, Control, Gizmo, GizmoColor, Ring, Shape}, + model::{Arc, Axes, Control, Gizmo, GizmoColor, Hovered, Ring, Shape}, InteractionMode, StudioGizmoConfig, TransformSpace, TransformType, }; @@ -22,14 +22,14 @@ pub(super) fn configure( } pub(super) fn draw( - q_gizmo: Query<(&Gizmo, &GlobalTransform)>, + q_gizmo: Query<(&Gizmo, &GlobalTransform, Option<&Hovered>)>, q_camera_xform: Query<&Transform, With>, mut gizmos: Gizmos, mode: Res, ) { let cam_xform = q_camera_xform.single(); - let Ok((gizmo, world_xform)) = q_gizmo.get_single() else { + let Ok((gizmo, world_xform, hovered)) = q_gizmo.get_single() else { return; }; @@ -40,10 +40,25 @@ pub(super) fn draw( ( Shape::Ring(ring), GizmoColor::Single(color), - Control::Rotation(Axes::X | Axes::Y | Axes::Z), + Control::Rotation(axis @ (Axes::X | Axes::Y | Axes::Z)), ) => { + let front_color = match hovered { + Some(Hovered(Control::Rotation(hovered_axes))) + if hovered_axes.intersects(axis) => + { + color * 2. + } + _ => color, + }; + let (front, back) = bisect_rotator_ring(&ring, cam_xform); - gizmos.arc(front.center, front.axis, front.start, front.length, color); + gizmos.arc( + front.center, + front.axis, + front.start, + front.length, + front_color, + ); if !mode.transform.intersects(TransformType::TRANSLATE) { gizmos.arc(