From 24a14b9f4580e74bcc5441d6ffd3742807ce85cd Mon Sep 17 00:00:00 2001 From: Urho Laukkarinen Date: Thu, 1 Feb 2024 08:12:31 +0200 Subject: [PATCH] Initial ArbBall rotation --- src/lib.rs | 38 +++++++++++++----- src/painter.rs | 62 ++++++++++++++++------------- src/subgizmo.rs | 6 ++- src/subgizmo/arcball.rs | 79 +++++++++++++++++++++++++++++++++++++ src/subgizmo/common.rs | 68 ++++++++++++++++++++----------- src/subgizmo/rotation.rs | 2 +- src/subgizmo/scale.rs | 10 ++--- src/subgizmo/translation.rs | 8 ++-- 8 files changed, 199 insertions(+), 74 deletions(-) create mode 100644 src/subgizmo/arcball.rs diff --git a/src/lib.rs b/src/lib.rs index fc31bb1..ca3b95c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,11 +28,11 @@ use std::hash::Hash; use std::ops::Sub; use crate::math::{screen_to_world, world_to_screen}; -use egui::{Color32, Context, Id, PointerButton, Rect, Sense, Ui}; +use egui::{Color32, Context, Id, PointerButton, Pos2, Rect, Sense, Ui}; use glam::{DMat4, DQuat, DVec3, Mat4, Quat, Vec3, Vec4Swizzles}; use crate::subgizmo::{ - RotationSubGizmo, ScaleSubGizmo, SubGizmo, TransformKind, TranslationSubGizmo, + ArcballSubGizmo, RotationSubGizmo, ScaleSubGizmo, SubGizmo, TransformKind, TranslationSubGizmo, }; mod math; @@ -136,7 +136,10 @@ impl Gizmo { // Choose subgizmos based on the gizmo mode match self.config.mode { - GizmoMode::Rotate => self.add_subgizmos(self.new_rotation()), + GizmoMode::Rotate => { + self.add_subgizmos(self.new_rotation()); + self.add_subgizmos(self.new_arcball()); + } GizmoMode::Translate => self.add_subgizmos(self.new_translation()), GizmoMode::Scale => self.add_subgizmos(self.new_scale()), }; @@ -193,8 +196,8 @@ impl Gizmo { result } - fn draw_subgizmos(&self, ui: &mut Ui, state: &mut GizmoState) { - for subgizmo in &self.subgizmos { + fn draw_subgizmos(&mut self, ui: &mut Ui, state: &mut GizmoState) { + for subgizmo in &mut self.subgizmos { if state.active_subgizmo_id.is_none() || subgizmo.is_active() { subgizmo.draw(ui); } @@ -210,6 +213,16 @@ impl Gizmo { .map(|(_, subgizmo)| subgizmo) } + /// Create arcball subgizmo + fn new_arcball(&self) -> [ArcballSubGizmo; 1] { + [ArcballSubGizmo::new( + self.id.with("arc"), + self.config, + GizmoDirection::Screen, + TransformKind::Axis, + )] + } + /// Create subgizmos for rotation fn new_rotation(&self) -> [RotationSubGizmo; 4] { [ @@ -345,15 +358,19 @@ impl Gizmo { /// Calculate a world space ray from current mouse position fn pointer_ray(&self, ui: &Ui) -> Option { - let hover = ui.input(|i| i.pointer.hover_pos())?; + let screen_pos = ui.input(|i| i.pointer.hover_pos())?; let mat = self.config.view_projection.inverse(); - let origin = screen_to_world(self.config.viewport, mat, hover, -1.0); - let target = screen_to_world(self.config.viewport, mat, hover, 1.0); + let origin = screen_to_world(self.config.viewport, mat, screen_pos, -1.0); + let target = screen_to_world(self.config.viewport, mat, screen_pos, 1.0); let direction = target.sub(origin).normalize(); - Some(Ray { origin, direction }) + Some(Ray { + screen_pos, + origin, + direction, + }) } } @@ -568,12 +585,13 @@ impl GizmoConfig { /// Whether local orientation is used pub(crate) fn local_space(&self) -> bool { - self.orientation == GizmoOrientation::Local || self.mode == GizmoMode::Scale + self.orientation == GizmoOrientation::Local } } #[derive(Debug, Copy, Clone)] pub(crate) struct Ray { + screen_pos: Pos2, origin: DVec3, direction: DVec3, } diff --git a/src/painter.rs b/src/painter.rs index d40f2ab..019c057 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -23,33 +23,14 @@ impl Painter3d { } } - pub fn arc( - &self, - radius: f64, - start_angle: f64, - end_angle: f64, - stroke: impl Into, - ) -> ShapeIdx { - let mut closed = false; - let mut angle = end_angle - start_angle; - - if angle <= -TAU { - angle = -TAU; - closed = true; - } else if angle >= TAU { - angle = TAU; - closed = true; - } + fn arc_points(&self, radius: f64, start_angle: f64, end_angle: f64) -> Vec { + let angle = f64::clamp(end_angle - start_angle, -TAU, TAU); - let mut step_count = steps(angle); + let step_count = steps(angle); let mut points = Vec::with_capacity(step_count); let step_size = angle / (step_count - 1) as f64; - if closed { - step_count -= 1; - } - for step in (0..step_count).map(|i| step_size * i as f64) { let x = f64::cos(start_angle + step) * radius; let z = f64::sin(start_angle + step) * radius; @@ -57,22 +38,47 @@ impl Painter3d { points.push(DVec3::new(x, 0.0, z)); } - let points = points + points .into_iter() .filter_map(|point| self.vec3_to_pos2(point)) - .collect::>(); + .collect::>() + } - self.painter.add(if closed { - Shape::closed_line(points, stroke) + pub fn arc( + &self, + radius: f64, + start_angle: f64, + end_angle: f64, + stroke: impl Into, + ) -> ShapeIdx { + let mut points = self.arc_points(radius, start_angle, end_angle); + + let closed = points + .first() + .zip(points.last()) + .filter(|(first, last)| first.distance(**last) < 1e-2) + .is_some(); + + if closed { + points.pop(); + self.painter.add(Shape::closed_line(points, stroke)) } else { - Shape::line(points, stroke) - }) + self.painter.add(Shape::line(points, stroke)) + } } pub fn circle(&self, radius: f64, stroke: impl Into) -> ShapeIdx { self.arc(radius, 0.0, TAU, stroke) } + pub fn filled_circle(&self, radius: f64, color: Color32) -> ShapeIdx { + let mut points = self.arc_points(radius, 0.0, TAU); + points.pop(); + + self.painter + .add(Shape::convex_polygon(points, color, Stroke::NONE)) + } + pub fn line_segment(&self, from: DVec3, to: DVec3, stroke: impl Into) { let mut points: [Pos2; 2] = Default::default(); diff --git a/src/subgizmo.rs b/src/subgizmo.rs index d6d6237..0148afe 100644 --- a/src/subgizmo.rs +++ b/src/subgizmo.rs @@ -6,10 +6,12 @@ use glam::DVec3; use crate::{GizmoConfig, GizmoDirection, GizmoResult, Ray, WidgetData}; +pub(crate) use arcball::ArcballSubGizmo; pub(crate) use rotation::RotationSubGizmo; pub(crate) use scale::ScaleSubGizmo; pub(crate) use translation::TranslationSubGizmo; +mod arcball; mod common; mod rotation; mod scale; @@ -53,7 +55,7 @@ pub(crate) trait SubGizmoBase: 'static { fn is_active(&self) -> bool; } -impl SubGizmoBase for SubGizmoConfig { +impl SubGizmoBase for SubGizmoConfig { fn id(&self) -> Id { self.id } @@ -82,7 +84,7 @@ pub(crate) trait SubGizmo: SubGizmoBase { /// Update the subgizmo based on pointer ray and interaction. fn update(&mut self, ui: &Ui, ray: Ray) -> Option; /// Draw the subgizmo - fn draw(&self, ui: &Ui); + fn draw(&mut self, ui: &Ui); } impl SubGizmoConfig diff --git a/src/subgizmo/arcball.rs b/src/subgizmo/arcball.rs new file mode 100644 index 0000000..482812b --- /dev/null +++ b/src/subgizmo/arcball.rs @@ -0,0 +1,79 @@ +use egui::{Pos2, Ui}; +use glam::DQuat; + +use crate::math::screen_to_world; +use crate::subgizmo::common::{draw_circle, pick_circle}; +use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState}; +use crate::{GizmoMode, GizmoResult, Ray}; + +pub(crate) type ArcballSubGizmo = SubGizmoConfig; + +impl SubGizmo for ArcballSubGizmo { + fn pick(&mut self, ui: &Ui, ray: Ray) -> Option { + let pick_result = pick_circle(self, ray, arcball_radius(self), true); + if !pick_result.picked { + return None; + } + + self.update_state_with(ui, |state: &mut ArcballState| { + state.last = ray.screen_pos; + }); + + Some(pick_result.t) + } + + fn update(&mut self, ui: &Ui, ray: Ray) -> Option { + let state = self.state(ui); + + let dir = ray.screen_pos - state.last; + + // Not a typical ArcBall rotation, but instead always rotates the object in the direction of mouse movement + + let quat = if dir.length_sq() > f32::EPSILON { + let mat = self.config.view_projection.inverse(); + let a = screen_to_world(self.config.viewport, mat, ray.screen_pos, 0.0); + let b = screen_to_world(self.config.viewport, mat, state.last, 0.0); + let origin = self.config.view_forward(); + let a = (a - origin).normalize(); + let b = (b - origin).normalize(); + + DQuat::from_axis_angle(a.cross(b).normalize(), a.dot(b).acos() * 10.0) + } else { + DQuat::IDENTITY + }; + + self.update_state_with(ui, |state: &mut ArcballState| { + state.last = ray.screen_pos; + }); + + let new_rotation = quat * self.config.rotation; + + Some(GizmoResult { + scale: self.config.scale.as_vec3().into(), + rotation: new_rotation.as_f32().into(), + translation: self.config.translation.as_vec3().into(), + mode: GizmoMode::Rotate, + value: self.normal().as_vec3().to_array(), + }) + } + + fn draw(&mut self, ui: &Ui) { + self.opacity = if self.focused { 0.10 } else { 0.0 }; + + draw_circle(self, ui, arcball_radius(self), true); + } +} + +/// Radius to use for outer circle subgizmos +pub(crate) fn arcball_radius(subgizmo: &SubGizmoConfig) -> f64 { + (subgizmo.config.scale_factor + * (subgizmo.config.visuals.gizmo_size + subgizmo.config.visuals.stroke_width - 5.0)) + as f64 +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct ArcballState { + last: Pos2, +} + +impl SubGizmoState for ArcballState {} diff --git a/src/subgizmo/common.rs b/src/subgizmo/common.rs index 93f64e1..3253f29 100644 --- a/src/subgizmo/common.rs +++ b/src/subgizmo/common.rs @@ -4,7 +4,7 @@ use std::ops::RangeInclusive; use crate::painter::Painter3d; use crate::subgizmo::{SubGizmoConfig, SubGizmoState}; -use crate::{GizmoDirection, GizmoMode, Ray}; +use crate::{GizmoDirection, Ray}; use glam::{DMat3, DMat4, DQuat, DVec3}; const ARROW_FADE: RangeInclusive = 0.95..=0.99; @@ -18,6 +18,12 @@ pub(crate) struct PickResult { pub t: f64, } +#[derive(Copy, Clone, PartialEq)] +pub(crate) enum ArrowheadStyle { + Cone, + Square, +} + pub(crate) fn pick_arrow(subgizmo: &SubGizmoConfig, ray: Ray) -> PickResult { let width = (subgizmo.config.scale_factor * subgizmo.config.visuals.stroke_width) as f64; @@ -111,7 +117,11 @@ pub(crate) fn pick_circle( } } -pub(crate) fn draw_arrow(subgizmo: &SubGizmoConfig, ui: &Ui) { +pub(crate) fn draw_arrow( + subgizmo: &SubGizmoConfig, + ui: &Ui, + arrowhead_style: ArrowheadStyle, +) { if subgizmo.opacity <= 1e-4 { return; } @@ -138,23 +148,26 @@ pub(crate) fn draw_arrow(subgizmo: &SubGizmoConfig, ui: &Ui let end = direction * length; painter.line_segment(start, end, (subgizmo.config.visuals.stroke_width, color)); - if subgizmo.config.mode == GizmoMode::Scale { - let end_stroke_width = subgizmo.config.visuals.stroke_width * 2.5; - let end_length = subgizmo.config.scale_factor * end_stroke_width; - - painter.line_segment( - end, - end + direction * end_length as f64, - (end_stroke_width, color), - ); - } else { - let arrow_length = width * 2.4; - - painter.arrow( - end, - end + direction * arrow_length, - (subgizmo.config.visuals.stroke_width * 1.2, color), - ); + match arrowhead_style { + ArrowheadStyle::Square => { + let end_stroke_width = subgizmo.config.visuals.stroke_width * 2.5; + let end_length = subgizmo.config.scale_factor * end_stroke_width; + + painter.line_segment( + end, + end + direction * end_length as f64, + (end_stroke_width, color), + ); + } + ArrowheadStyle::Cone => { + let arrow_length = width * 2.4; + + painter.arrow( + end, + end + direction * arrow_length, + (subgizmo.config.visuals.stroke_width * 1.2, color), + ); + } } } @@ -194,7 +207,12 @@ pub(crate) fn draw_plane(subgizmo: &SubGizmoConfig, ui: &Ui ); } -pub(crate) fn draw_circle(subgizmo: &SubGizmoConfig, ui: &Ui, radius: f64) { +pub(crate) fn draw_circle( + subgizmo: &SubGizmoConfig, + ui: &Ui, + radius: f64, + filled: bool, +) { if subgizmo.opacity <= 1e-4 { return; } @@ -217,9 +235,11 @@ pub(crate) fn draw_circle(subgizmo: &SubGizmoConfig, ui: &U subgizmo.config.viewport, ); - let stroke = (subgizmo.config.visuals.stroke_width, color); - - painter.circle(radius, stroke); + if filled { + painter.filled_circle(radius, color); + } else { + painter.circle(radius, (subgizmo.config.visuals.stroke_width, color)); + } } pub(crate) fn plane_bitangent(direction: GizmoDirection) -> DVec3 { @@ -270,6 +290,6 @@ pub(crate) fn inner_circle_radius(subgizmo: &SubGizmoConfig /// Radius to use for outer circle subgizmos pub(crate) fn outer_circle_radius(subgizmo: &SubGizmoConfig) -> f64 { (subgizmo.config.scale_factor - * (subgizmo.config.visuals.gizmo_size + subgizmo.config.visuals.stroke_width * 5.0)) + * (subgizmo.config.visuals.gizmo_size + subgizmo.config.visuals.stroke_width + 5.0)) as f64 } diff --git a/src/subgizmo/rotation.rs b/src/subgizmo/rotation.rs index d4395bb..09436be 100644 --- a/src/subgizmo/rotation.rs +++ b/src/subgizmo/rotation.rs @@ -92,7 +92,7 @@ impl SubGizmo for RotationSubGizmo { }) } - fn draw(&self, ui: &Ui) { + fn draw(&mut self, ui: &Ui) { let state = self.state(ui); let config = self.config; diff --git a/src/subgizmo/scale.rs b/src/subgizmo/scale.rs index 5153571..4fd16ca 100644 --- a/src/subgizmo/scale.rs +++ b/src/subgizmo/scale.rs @@ -5,7 +5,7 @@ use crate::math::{round_to_interval, world_to_screen}; use crate::subgizmo::common::{ draw_arrow, draw_circle, draw_plane, inner_circle_radius, outer_circle_radius, pick_arrow, - pick_circle, pick_plane, plane_bitangent, plane_tangent, + pick_circle, pick_plane, plane_bitangent, plane_tangent, ArrowheadStyle, }; use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState, TransformKind}; use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray}; @@ -72,12 +72,12 @@ impl SubGizmo for ScaleSubGizmo { }) } - fn draw(&self, ui: &Ui) { + fn draw(&mut self, ui: &Ui) { match (self.transform_kind, self.direction) { - (TransformKind::Axis, _) => draw_arrow(self, ui), + (TransformKind::Axis, _) => draw_arrow(self, ui, ArrowheadStyle::Square), (TransformKind::Plane, GizmoDirection::Screen) => { - draw_circle(self, ui, inner_circle_radius(self)); - draw_circle(self, ui, outer_circle_radius(self)); + draw_circle(self, ui, inner_circle_radius(self), false); + draw_circle(self, ui, outer_circle_radius(self), false); } (TransformKind::Plane, _) => draw_plane(self, ui), } diff --git a/src/subgizmo/translation.rs b/src/subgizmo/translation.rs index 7283e43..0b2b9fa 100644 --- a/src/subgizmo/translation.rs +++ b/src/subgizmo/translation.rs @@ -5,7 +5,7 @@ use crate::math::{intersect_plane, ray_to_ray, round_to_interval}; use crate::subgizmo::common::{ draw_arrow, draw_circle, draw_plane, inner_circle_radius, pick_arrow, pick_circle, pick_plane, - plane_bitangent, plane_global_origin, plane_tangent, + plane_bitangent, plane_global_origin, plane_tangent, ArrowheadStyle, }; use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState, TransformKind}; use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray}; @@ -73,11 +73,11 @@ impl SubGizmo for TranslationSubGizmo { }) } - fn draw(&self, ui: &Ui) { + fn draw(&mut self, ui: &Ui) { match (self.transform_kind, self.direction) { - (TransformKind::Axis, _) => draw_arrow(self, ui), + (TransformKind::Axis, _) => draw_arrow(self, ui, ArrowheadStyle::Cone), (TransformKind::Plane, GizmoDirection::Screen) => { - draw_circle(self, ui, inner_circle_radius(self)); + draw_circle(self, ui, inner_circle_radius(self), false); } (TransformKind::Plane, _) => draw_plane(self, ui), }