diff --git a/Cargo.toml b/Cargo.toml index 9078cfa..b896d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui-gizmo" -version = "0.14.1" +version = "0.15.0" authors = ["Urho Laukkarinen "] edition = "2021" @@ -18,7 +18,7 @@ members = ["demo"] [dependencies] egui = "0.25.0" -glam = { version = "0.24", features = ["mint"] } +glam = { version = "0.25.0", features = ["mint"] } mint = "0.5" [profile.release] diff --git a/README.md b/README.md index 2d16a6d..19b9fd4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [Try it out in a web demo](https://urholaukkarinen.github.io/egui-gizmo/) -![Rotation](media/rotation.gif) +![Rotation](media/rotation.png) ![Translation](media/translation.png) ![Scale](media/scale.png) diff --git a/demo/Cargo.toml b/demo/Cargo.toml index 9a13386..7d34fc9 100644 --- a/demo/Cargo.toml +++ b/demo/Cargo.toml @@ -13,6 +13,10 @@ egui-gizmo = { path = ".." } [dependencies.bevy] version = "0.12.1" +[dependencies.bevy_math] +version = "0.12.1" +features = ["mint"] + [dependencies.bevy_egui] git = "https://github.com/urholaukkarinen/bevy_egui.git" rev="bc3dd1559e24ca0178ed1d2dfef07cb784437505" diff --git a/demo/src/main.rs b/demo/src/main.rs index c4a94db..780b98c 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -304,15 +304,20 @@ fn instructions_text(ui: &Ui) { } fn show_gizmo_status(ui: &Ui, response: GizmoResult) { - let length = Vec3::from(response.value).length(); - - let text = match response.mode { - GizmoMode::Rotate => format!("{:.1}°, {:.2} rad", length.to_degrees(), length), - - GizmoMode::Translate | GizmoMode::Scale => format!( - "dX: {:.2}, dY: {:.2}, dZ: {:.2}", - response.value[0], response.value[1], response.value[2] - ), + let text = if let Some(value) = response.value { + match response.mode { + GizmoMode::Rotate => { + let length = Vec3::from(value).length(); + format!("{:.1}°, {:.2} rad", length.to_degrees(), length) + } + + GizmoMode::Translate | GizmoMode::Scale => format!( + "dX: {:.2}, dY: {:.2}, dZ: {:.2}", + value[0], value[1], value[2] + ), + } + } else { + String::new() }; let rect = ui.clip_rect(); diff --git a/media/rotation.gif b/media/rotation.gif deleted file mode 100644 index 2e641d8..0000000 Binary files a/media/rotation.gif and /dev/null differ diff --git a/media/rotation.png b/media/rotation.png new file mode 100644 index 0000000..852d622 Binary files /dev/null and b/media/rotation.png differ diff --git a/media/scale.png b/media/scale.png index ed0f2ab..5fb91a1 100644 Binary files a/media/scale.png and b/media/scale.png differ diff --git a/media/translation.png b/media/translation.png index 6a10765..0f211fe 100644 Binary files a/media/translation.png and b/media/translation.png differ diff --git a/src/lib.rs b/src/lib.rs index 61a831f..939ad51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,11 +28,14 @@ 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::rotation::RotationParams; +use crate::subgizmo::scale::ScaleParams; +use crate::subgizmo::translation::TranslationParams; use crate::subgizmo::{ - RotationSubGizmo, ScaleSubGizmo, SubGizmo, TransformKind, TranslationSubGizmo, + ArcballSubGizmo, RotationSubGizmo, ScaleSubGizmo, SubGizmo, TransformKind, TranslationSubGizmo, }; mod math; @@ -136,7 +139,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()), }; @@ -182,7 +188,7 @@ impl Gizmo { if let Some((_, result)) = active_subgizmo.zip(result) { self.config.translation = Vec3::from(result.translation).as_dvec3(); - self.config.rotation = Quat::from(result.rotation).as_f64(); + self.config.rotation = Quat::from(result.rotation).as_dquat(); self.config.scale = Vec3::from(result.scale).as_dvec3(); } @@ -193,8 +199,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,116 +216,165 @@ impl Gizmo { .map(|(_, subgizmo)| subgizmo) } + /// Create arcball subgizmo + fn new_arcball(&self) -> [ArcballSubGizmo; 1] { + [ArcballSubGizmo::new(self.id.with("arc"), self.config, ())] + } + /// Create subgizmos for rotation fn new_rotation(&self) -> [RotationSubGizmo; 4] { [ RotationSubGizmo::new( self.id.with("rx"), self.config, - GizmoDirection::X, - TransformKind::Axis, + RotationParams { + direction: GizmoDirection::X, + }, ), RotationSubGizmo::new( self.id.with("ry"), self.config, - GizmoDirection::Y, - TransformKind::Axis, + RotationParams { + direction: GizmoDirection::Y, + }, ), RotationSubGizmo::new( self.id.with("rz"), self.config, - GizmoDirection::Z, - TransformKind::Axis, + RotationParams { + direction: GizmoDirection::Z, + }, ), RotationSubGizmo::new( self.id.with("rs"), self.config, - GizmoDirection::Screen, - TransformKind::Axis, + RotationParams { + direction: GizmoDirection::View, + }, ), ] } /// Create subgizmos for translation - fn new_translation(&self) -> [TranslationSubGizmo; 6] { + fn new_translation(&self) -> [TranslationSubGizmo; 7] { [ + TranslationSubGizmo::new( + self.id.with("txs"), + self.config, + TranslationParams { + direction: GizmoDirection::View, + transform_kind: TransformKind::Plane, + }, + ), TranslationSubGizmo::new( self.id.with("tx"), self.config, - GizmoDirection::X, - TransformKind::Axis, + TranslationParams { + direction: GizmoDirection::X, + transform_kind: TransformKind::Axis, + }, ), TranslationSubGizmo::new( self.id.with("ty"), self.config, - GizmoDirection::Y, - TransformKind::Axis, + TranslationParams { + direction: GizmoDirection::Y, + transform_kind: TransformKind::Axis, + }, ), TranslationSubGizmo::new( self.id.with("tz"), self.config, - GizmoDirection::Z, - TransformKind::Axis, + TranslationParams { + direction: GizmoDirection::Z, + transform_kind: TransformKind::Axis, + }, ), TranslationSubGizmo::new( self.id.with("tyz"), self.config, - GizmoDirection::X, - TransformKind::Plane, + TranslationParams { + direction: GizmoDirection::X, + transform_kind: TransformKind::Plane, + }, ), TranslationSubGizmo::new( self.id.with("txz"), self.config, - GizmoDirection::Y, - TransformKind::Plane, + TranslationParams { + direction: GizmoDirection::Y, + transform_kind: TransformKind::Plane, + }, ), TranslationSubGizmo::new( self.id.with("txy"), self.config, - GizmoDirection::Z, - TransformKind::Plane, + TranslationParams { + direction: GizmoDirection::Z, + transform_kind: TransformKind::Plane, + }, ), ] } /// Create subgizmos for scale - fn new_scale(&self) -> [ScaleSubGizmo; 6] { + fn new_scale(&self) -> [ScaleSubGizmo; 7] { [ + ScaleSubGizmo::new( + self.id.with("txs"), + self.config, + ScaleParams { + direction: GizmoDirection::View, + transform_kind: TransformKind::Plane, + }, + ), ScaleSubGizmo::new( self.id.with("sx"), self.config, - GizmoDirection::X, - TransformKind::Axis, + ScaleParams { + direction: GizmoDirection::X, + transform_kind: TransformKind::Axis, + }, ), ScaleSubGizmo::new( self.id.with("sy"), self.config, - GizmoDirection::Y, - TransformKind::Axis, + ScaleParams { + direction: GizmoDirection::Y, + transform_kind: TransformKind::Axis, + }, ), ScaleSubGizmo::new( self.id.with("sz"), self.config, - GizmoDirection::Z, - TransformKind::Axis, + ScaleParams { + direction: GizmoDirection::Z, + transform_kind: TransformKind::Axis, + }, ), ScaleSubGizmo::new( self.id.with("syz"), self.config, - GizmoDirection::X, - TransformKind::Plane, + ScaleParams { + direction: GizmoDirection::X, + transform_kind: TransformKind::Plane, + }, ), ScaleSubGizmo::new( self.id.with("sxz"), self.config, - GizmoDirection::Y, - TransformKind::Plane, + ScaleParams { + direction: GizmoDirection::Y, + transform_kind: TransformKind::Plane, + }, ), ScaleSubGizmo::new( self.id.with("sxy"), self.config, - GizmoDirection::Z, - TransformKind::Plane, + ScaleParams { + direction: GizmoDirection::Z, + transform_kind: TransformKind::Plane, + }, ), ] } @@ -333,15 +388,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, + }) } } @@ -357,7 +416,7 @@ pub struct GizmoResult { /// Mode of the active subgizmo pub mode: GizmoMode, /// Total scale, rotation or translation of the current gizmo activation, depending on mode - pub value: [f32; 3], + pub value: Option<[f32; 3]>, } impl GizmoResult { @@ -400,8 +459,8 @@ pub enum GizmoDirection { Y, /// Gizmo points in the Z-direction Z, - /// Gizmo points towards the screen - Screen, + /// Gizmo points in the view direction + View, } /// Controls the visual style of the gizmo @@ -413,13 +472,13 @@ pub struct GizmoVisuals { pub y_color: Color32, /// Color of the z axis pub z_color: Color32, - /// Color of the screen direction axis + /// Color of the forward axis pub s_color: Color32, /// Alpha of the gizmo color when inactive pub inactive_alpha: f32, /// Alpha of the gizmo color when highlighted/active pub highlight_alpha: f32, - /// Color to use for highlighted and active axes. By default the axis color is used with `highlight_alpha` + /// Color to use for highlighted and active axes. By default, the axis color is used with `highlight_alpha` pub highlight_color: Option, /// Width (thickness) of the gizmo strokes pub stroke_width: f32, @@ -556,12 +615,14 @@ impl GizmoConfig { /// Whether local orientation is used pub(crate) fn local_space(&self) -> bool { + // Scale mode only works in local space self.orientation == GizmoOrientation::Local || self.mode == GizmoMode::Scale } } #[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..3a95529 100644 --- a/src/subgizmo.rs +++ b/src/subgizmo.rs @@ -1,19 +1,20 @@ use std::hash::Hash; -use std::marker::PhantomData; +use std::ops::Deref; -use egui::{Color32, Id, Ui}; -use glam::DVec3; +use egui::{Id, Ui}; -use crate::{GizmoConfig, GizmoDirection, GizmoResult, Ray, WidgetData}; +use crate::{GizmoConfig, GizmoResult, Ray}; +pub(crate) use arcball::ArcballSubGizmo; pub(crate) use rotation::RotationSubGizmo; pub(crate) use scale::ScaleSubGizmo; pub(crate) use translation::TranslationSubGizmo; -mod common; -mod rotation; -mod scale; -mod translation; +pub(crate) mod arcball; +pub(crate) mod common; +pub(crate) mod rotation; +pub(crate) mod scale; +pub(crate) mod translation; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum TransformKind { @@ -21,14 +22,15 @@ pub(crate) enum TransformKind { Plane, } -pub(crate) trait SubGizmoState: Default + Copy + Clone + Send + Sync + 'static {} -impl WidgetData for T where T: SubGizmoState {} +pub(crate) trait SubGizmoKind: 'static { + type Params; + type State: Copy + Clone + Send + Sync + Default + 'static; +} -pub(crate) struct SubGizmoConfig { +pub(crate) struct SubGizmoConfig { id: Id, + /// Configuration of the full gizmo pub(crate) config: GizmoConfig, - pub(crate) direction: GizmoDirection, - pub(crate) transform_kind: TransformKind, /// Whether this subgizmo is focused this frame pub(crate) focused: bool, /// Whether this subgizmo is active this frame @@ -36,8 +38,16 @@ pub(crate) struct SubGizmoConfig { /// Opacity of the subgizmo for this frame. /// A fully invisible subgizmo cannot be interacted with. pub(crate) opacity: f32, + /// Additional parameters depending on the subgizmo kind + params: T::Params, +} - _phantom: PhantomData, +impl Deref for SubGizmoConfig { + type Target = T::Params; + + fn deref(&self) -> &Self::Target { + &self.params + } } pub(crate) trait SubGizmoBase: 'static { @@ -53,7 +63,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,80 +92,33 @@ 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 where - T: SubGizmoState, + T: SubGizmoKind, { - pub fn new( - id_source: impl Hash, - config: GizmoConfig, - direction: GizmoDirection, - transform_kind: TransformKind, - ) -> Self { + pub fn new(id_source: impl Hash, config: GizmoConfig, params: T::Params) -> Self { Self { id: Id::new(id_source), config, - direction, - transform_kind, focused: false, active: false, opacity: 0.0, - _phantom: Default::default(), - } - } - - pub fn local_normal(&self) -> DVec3 { - match self.direction { - GizmoDirection::X => DVec3::X, - GizmoDirection::Y => DVec3::Y, - GizmoDirection::Z => DVec3::Z, - GizmoDirection::Screen => -self.config.view_forward(), + params, } } - pub fn normal(&self) -> DVec3 { - let mut normal = self.local_normal(); - - if self.config.local_space() && self.direction != GizmoDirection::Screen { - normal = self.config.rotation * normal; - } - - normal - } - - pub fn color(&self) -> Color32 { - let color = match self.direction { - GizmoDirection::X => self.config.visuals.x_color, - GizmoDirection::Y => self.config.visuals.y_color, - GizmoDirection::Z => self.config.visuals.z_color, - GizmoDirection::Screen => self.config.visuals.s_color, - }; - - let color = if self.focused { - self.config.visuals.highlight_color.unwrap_or(color) - } else { - color - }; - - let alpha = if self.focused { - self.config.visuals.highlight_alpha - } else { - self.config.visuals.inactive_alpha - }; - - color.linear_multiply(alpha) - } - - pub fn state(&self, ui: &Ui) -> T { - <_ as WidgetData>::load(ui.ctx(), self.id) + pub fn state(&self, ui: &Ui) -> T::State { + ui.ctx() + .memory_mut(|mem| *mem.data.get_temp_mut_or_default::(self.id)) } - pub fn update_state_with(&self, ui: &Ui, fun: impl FnOnce(&mut T)) { + pub fn update_state_with(&self, ui: &Ui, fun: impl FnOnce(&mut T::State)) { let mut state = self.state(ui); fun(&mut state); - state.save(ui.ctx(), self.id); + ui.ctx() + .memory_mut(|mem| mem.data.insert_temp(self.id, state)); } } diff --git a/src/subgizmo/arcball.rs b/src/subgizmo/arcball.rs new file mode 100644 index 0000000..20a5f7e --- /dev/null +++ b/src/subgizmo/arcball.rs @@ -0,0 +1,83 @@ +use egui::{Color32, Pos2, Ui}; +use glam::DQuat; + +use crate::math::screen_to_world; +use crate::subgizmo::common::{draw_circle, pick_circle}; +use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoKind}; +use crate::{GizmoConfig, GizmoMode, GizmoResult, Ray, WidgetData}; + +pub(crate) type ArcballSubGizmo = SubGizmoConfig; + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct ArcballState { + last_pos: Pos2, +} + +impl WidgetData for ArcballState {} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Arcball; + +impl SubGizmoKind for Arcball { + type Params = (); + type State = ArcballState; +} + +impl SubGizmo for ArcballSubGizmo { + fn pick(&mut self, ui: &Ui, ray: Ray) -> Option { + let pick_result = pick_circle(self, ray, arcball_radius(&self.config), true); + if !pick_result.picked { + return None; + } + + self.update_state_with(ui, |state: &mut ArcballState| { + state.last_pos = 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_pos; + + 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_pos, 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_pos = ray.screen_pos; + }); + + let new_rotation = quat * self.config.rotation; + + Some(GizmoResult { + scale: self.config.scale.as_vec3().into(), + rotation: new_rotation.as_quat().into(), + translation: self.config.translation.as_vec3().into(), + mode: GizmoMode::Rotate, + value: None, + }) + } + + fn draw(&mut self, ui: &Ui) { + self.opacity = if self.focused { 0.10 } else { 0.0 }; + + draw_circle(self, ui, Color32::WHITE, arcball_radius(&self.config), true); + } +} + +/// Radius to use for outer circle subgizmos +pub(crate) fn arcball_radius(config: &GizmoConfig) -> f64 { + (config.scale_factor * (config.visuals.gizmo_size + config.visuals.stroke_width - 5.0)) as f64 +} diff --git a/src/subgizmo/common.rs b/src/subgizmo/common.rs index 59a3445..863bb23 100644 --- a/src/subgizmo/common.rs +++ b/src/subgizmo/common.rs @@ -1,11 +1,11 @@ use crate::math::{ray_to_plane_origin, segment_to_segment}; -use egui::{Stroke, Ui}; +use egui::{Color32, Stroke, Ui}; use std::ops::RangeInclusive; use crate::painter::Painter3d; -use crate::subgizmo::{SubGizmoConfig, SubGizmoState}; -use crate::{GizmoDirection, GizmoMode, Ray}; -use glam::{DMat4, DVec3}; +use crate::subgizmo::{SubGizmoConfig, SubGizmoKind}; +use crate::{GizmoConfig, GizmoDirection, Ray}; +use glam::{DMat3, DMat4, DQuat, DVec3}; const ARROW_FADE: RangeInclusive = 0.95..=0.99; const PLANE_FADE: RangeInclusive = 0.70..=0.86; @@ -18,9 +18,23 @@ pub(crate) struct PickResult { pub t: f64, } -pub(crate) fn pick_arrow(subgizmo: &SubGizmoConfig, ray: Ray) -> PickResult { - let origin = subgizmo.config.translation; - let dir = subgizmo.normal(); +#[derive(Copy, Clone, PartialEq)] +pub(crate) enum ArrowheadStyle { + Cone, + Square, +} + +pub(crate) fn pick_arrow( + subgizmo: &SubGizmoConfig, + ray: Ray, + direction: GizmoDirection, +) -> PickResult { + let width = (subgizmo.config.scale_factor * subgizmo.config.visuals.stroke_width) as f64; + + let dir = gizmo_normal(&subgizmo.config, direction); + let start = + subgizmo.config.translation + (dir * (width * 0.5 + inner_circle_radius(&subgizmo.config))); + let length = (subgizmo.config.scale_factor * subgizmo.config.visuals.gizmo_size) as f64; let ray_length = 1e+14; @@ -28,12 +42,12 @@ pub(crate) fn pick_arrow(subgizmo: &SubGizmoConfig, ray: Ra let (ray_t, subgizmo_t) = segment_to_segment( ray.origin, ray.origin + ray.direction * ray_length, - origin, - origin + dir * length, + start, + start + dir * length, ); let ray_point = ray.origin + ray.direction * ray_length * ray_t; - let subgizmo_point = origin + dir * length * subgizmo_t; + let subgizmo_point = start + dir * length * subgizmo_t; let dist = (ray_point - subgizmo_point).length(); let dot = subgizmo.config.gizmo_view_forward.dot(dir).abs(); @@ -51,10 +65,14 @@ pub(crate) fn pick_arrow(subgizmo: &SubGizmoConfig, ray: Ra } } -pub(crate) fn pick_plane(subgizmo: &SubGizmoConfig, ray: Ray) -> PickResult { - let origin = plane_global_origin(subgizmo); +pub(crate) fn pick_plane( + subgizmo: &SubGizmoConfig, + ray: Ray, + direction: GizmoDirection, +) -> PickResult { + let origin = plane_global_origin(&subgizmo.config, direction); - let normal = subgizmo.normal(); + let normal = gizmo_normal(&subgizmo.config, direction); let (t, dist_from_origin) = ray_to_plane_origin(normal, origin, ray.origin, ray.direction); @@ -63,13 +81,13 @@ pub(crate) fn pick_plane(subgizmo: &SubGizmoConfig, ray: Ra let dot = subgizmo .config .gizmo_view_forward - .dot(subgizmo.normal()) + .dot(gizmo_normal(&subgizmo.config, direction)) .abs(); let visibility = (1.0 - ((1.0 - dot) - *PLANE_FADE.start()) / (*PLANE_FADE.end() - *PLANE_FADE.start())) .min(1.0); - let picked = visibility > 0.0 && dist_from_origin <= plane_size(subgizmo); + let picked = visibility > 0.0 && dist_from_origin <= plane_size(&subgizmo.config); PickResult { subgizmo_point: ray_point, @@ -79,12 +97,46 @@ pub(crate) fn pick_plane(subgizmo: &SubGizmoConfig, ray: Ra } } -pub(crate) fn draw_arrow(subgizmo: &SubGizmoConfig, ui: &Ui) { - if subgizmo.opacity <= 0.0001 { +pub(crate) fn pick_circle( + subgizmo: &SubGizmoConfig, + ray: Ray, + radius: f64, + filled: bool, +) -> PickResult { + let config = &subgizmo.config; + let origin = config.translation; + let normal = -subgizmo.config.view_forward(); + + let (t, dist_from_gizmo_origin) = + ray_to_plane_origin(normal, origin, ray.origin, ray.direction); + + let hit_pos = ray.origin + ray.direction * t; + + let picked = if filled { + dist_from_gizmo_origin <= radius + config.focus_distance as f64 + } else { + (dist_from_gizmo_origin - radius).abs() <= config.focus_distance as f64 + }; + + PickResult { + subgizmo_point: hit_pos, + visibility: 1.0, + picked, + t, + } +} + +pub(crate) fn draw_arrow( + subgizmo: &SubGizmoConfig, + ui: &Ui, + direction: GizmoDirection, + arrowhead_style: ArrowheadStyle, +) { + if subgizmo.opacity <= 1e-4 { return; } - let color = subgizmo.color().gamma_multiply(subgizmo.opacity); + let color = gizmo_color(subgizmo, direction).gamma_multiply(subgizmo.opacity); let transform = if subgizmo.config.local_space() { DMat4::from_rotation_translation(subgizmo.config.rotation, subgizmo.config.translation) @@ -98,40 +150,47 @@ pub(crate) fn draw_arrow(subgizmo: &SubGizmoConfig, ui: &Ui subgizmo.config.viewport, ); - let direction = subgizmo.local_normal(); - let width = subgizmo.config.scale_factor * subgizmo.config.visuals.stroke_width; - let length = subgizmo.config.scale_factor * subgizmo.config.visuals.gizmo_size; + let direction = gizmo_local_normal(&subgizmo.config, direction); + let width = (subgizmo.config.scale_factor * subgizmo.config.visuals.stroke_width) as f64; + let length = (subgizmo.config.scale_factor * subgizmo.config.visuals.gizmo_size) as f64; - let start = direction * width as f64; - let end = direction * length as f64; + let start = direction * (width * 0.5 + inner_circle_radius(&subgizmo.config)); + 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 as f64, - (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), + ); + } } } -pub(crate) fn draw_plane(subgizmo: &SubGizmoConfig, ui: &Ui) { - if subgizmo.opacity <= 0.0001 { +pub(crate) fn draw_plane( + subgizmo: &SubGizmoConfig, + ui: &Ui, + direction: GizmoDirection, +) { + if subgizmo.opacity <= 1e-4 { return; } - let color = subgizmo.color().gamma_multiply(subgizmo.opacity); + let color = gizmo_color(subgizmo, direction).gamma_multiply(subgizmo.opacity); let transform = if subgizmo.config.local_space() { DMat4::from_rotation_translation(subgizmo.config.rotation, subgizmo.config.translation) @@ -145,10 +204,10 @@ pub(crate) fn draw_plane(subgizmo: &SubGizmoConfig, ui: &Ui subgizmo.config.viewport, ); - let scale = plane_size(subgizmo) * 0.5; - let a = plane_binormal(subgizmo.direction) * scale; - let b = plane_tangent(subgizmo.direction) * scale; - let origin = plane_local_origin(subgizmo); + let scale = plane_size(&subgizmo.config) * 0.5; + let a = plane_bitangent(direction) * scale; + let b = plane_tangent(direction) * scale; + let origin = plane_local_origin(&subgizmo.config, direction); painter.polygon( &[ @@ -162,12 +221,48 @@ pub(crate) fn draw_plane(subgizmo: &SubGizmoConfig, ui: &Ui ); } -pub(crate) fn plane_binormal(direction: GizmoDirection) -> DVec3 { +pub(crate) fn draw_circle( + subgizmo: &SubGizmoConfig, + ui: &Ui, + color: Color32, + radius: f64, + filled: bool, +) { + if subgizmo.opacity <= 1e-4 { + return; + } + + let color = color.gamma_multiply(subgizmo.opacity); + + let rotation = { + let forward = subgizmo.config.view_forward(); + let right = subgizmo.config.view_right(); + let up = subgizmo.config.view_up(); + + DQuat::from_mat3(&DMat3::from_cols(up, -forward, -right)) + }; + + let transform = DMat4::from_rotation_translation(rotation, subgizmo.config.translation); + + let painter = Painter3d::new( + ui.painter().clone(), + subgizmo.config.view_projection * transform, + subgizmo.config.viewport, + ); + + 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 { match direction { GizmoDirection::X => DVec3::Y, GizmoDirection::Y => DVec3::Z, GizmoDirection::Z => DVec3::X, - GizmoDirection::Screen => DVec3::ZERO, // Unused + GizmoDirection::View => DVec3::ZERO, // Unused } } @@ -176,28 +271,82 @@ pub(crate) fn plane_tangent(direction: GizmoDirection) -> DVec3 { GizmoDirection::X => DVec3::Z, GizmoDirection::Y => DVec3::X, GizmoDirection::Z => DVec3::Y, - GizmoDirection::Screen => DVec3::ZERO, // Unused + GizmoDirection::View => DVec3::ZERO, // Unused } } -pub(crate) fn plane_size(subgizmo: &SubGizmoConfig) -> f64 { - (subgizmo.config.scale_factor - * (subgizmo.config.visuals.gizmo_size * 0.1 + subgizmo.config.visuals.stroke_width * 2.0)) +pub(crate) fn plane_size(config: &GizmoConfig) -> f64 { + (config.scale_factor * (config.visuals.gizmo_size * 0.1 + config.visuals.stroke_width * 2.0)) as f64 } -pub(crate) fn plane_local_origin(subgizmo: &SubGizmoConfig) -> DVec3 { - let offset = subgizmo.config.scale_factor * subgizmo.config.visuals.gizmo_size * 0.4; +pub(crate) fn plane_local_origin(config: &GizmoConfig, direction: GizmoDirection) -> DVec3 { + let offset = config.scale_factor * config.visuals.gizmo_size * 0.5; - let a = plane_binormal(subgizmo.direction); - let b = plane_tangent(subgizmo.direction); + let a = plane_bitangent(direction); + let b = plane_tangent(direction); (a + b) * offset as f64 } -pub(crate) fn plane_global_origin(subgizmo: &SubGizmoConfig) -> DVec3 { - let mut origin = plane_local_origin(subgizmo); - if subgizmo.config.local_space() { - origin = subgizmo.config.rotation * origin; +pub(crate) fn plane_global_origin(config: &GizmoConfig, direction: GizmoDirection) -> DVec3 { + let mut origin = plane_local_origin(config, direction); + if config.local_space() { + origin = config.rotation * origin; + } + origin + config.translation +} + +/// Radius to use for inner circle subgizmos +pub(crate) fn inner_circle_radius(config: &GizmoConfig) -> f64 { + (config.scale_factor * config.visuals.gizmo_size) as f64 * 0.2 +} + +/// Radius to use for outer circle subgizmos +pub(crate) fn outer_circle_radius(config: &GizmoConfig) -> f64 { + (config.scale_factor * (config.visuals.gizmo_size + config.visuals.stroke_width + 5.0)) as f64 +} + +pub fn gizmo_local_normal(config: &GizmoConfig, direction: GizmoDirection) -> DVec3 { + match direction { + GizmoDirection::X => DVec3::X, + GizmoDirection::Y => DVec3::Y, + GizmoDirection::Z => DVec3::Z, + GizmoDirection::View => -config.view_forward(), + } +} + +pub fn gizmo_normal(config: &GizmoConfig, direction: GizmoDirection) -> DVec3 { + let mut normal = gizmo_local_normal(config, direction); + + if config.local_space() && direction != GizmoDirection::View { + normal = config.rotation * normal; } - origin + subgizmo.config.translation + + normal +} + +pub fn gizmo_color( + subgizmo: &SubGizmoConfig, + direction: GizmoDirection, +) -> Color32 { + let color = match direction { + GizmoDirection::X => subgizmo.config.visuals.x_color, + GizmoDirection::Y => subgizmo.config.visuals.y_color, + GizmoDirection::Z => subgizmo.config.visuals.z_color, + GizmoDirection::View => subgizmo.config.visuals.s_color, + }; + + let color = if subgizmo.focused { + subgizmo.config.visuals.highlight_color.unwrap_or(color) + } else { + color + }; + + let alpha = if subgizmo.focused { + subgizmo.config.visuals.highlight_alpha + } else { + subgizmo.config.visuals.inactive_alpha + }; + + color.linear_multiply(alpha) } diff --git a/src/subgizmo/rotation.rs b/src/subgizmo/rotation.rs index 051d5e6..f55d141 100644 --- a/src/subgizmo/rotation.rs +++ b/src/subgizmo/rotation.rs @@ -5,17 +5,39 @@ use glam::{DMat3, DMat4, DQuat, DVec2, DVec3}; use crate::math::{ray_to_plane_origin, rotation_align, round_to_interval, world_to_screen}; use crate::painter::Painter3d; -use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState}; +use crate::subgizmo::common::{gizmo_color, gizmo_local_normal, gizmo_normal, outer_circle_radius}; +use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoKind}; use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray}; -pub(crate) type RotationSubGizmo = SubGizmoConfig; +pub(crate) type RotationSubGizmo = SubGizmoConfig; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct RotationParams { + pub direction: GizmoDirection, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct RotationState { + start_axis_angle: f32, + start_rotation_angle: f32, + last_rotation_angle: f32, + current_delta: f32, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Rotation; + +impl SubGizmoKind for Rotation { + type Params = RotationParams; + type State = RotationState; +} impl SubGizmo for RotationSubGizmo { fn pick(&mut self, ui: &Ui, ray: Ray) -> Option { - let radius = arc_radius(self) as f64; + let radius = arc_radius(self); let config = self.config; let origin = config.translation; - let normal = self.normal(); + let normal = gizmo_normal(&self.config, self.direction); let tangent = tangent(self); let (t, dist_from_gizmo_origin) = @@ -28,7 +50,7 @@ impl SubGizmo for RotationSubGizmo { let offset = (nearest_circle_pos - origin).normalize(); - let angle = if self.direction == GizmoDirection::Screen { + let angle = if self.direction == GizmoDirection::View { f64::atan2(tangent.cross(normal).dot(offset), tangent.dot(offset)) } else { let mut forward = config.view_forward(); @@ -80,18 +102,22 @@ impl SubGizmo for RotationSubGizmo { }); let new_rotation = - DQuat::from_axis_angle(self.normal(), -angle_delta) * self.config.rotation; + DQuat::from_axis_angle(gizmo_normal(&self.config, self.direction), -angle_delta) + * self.config.rotation; Some(GizmoResult { scale: self.config.scale.as_vec3().into(), - rotation: new_rotation.as_f32().into(), + rotation: new_rotation.as_quat().into(), translation: self.config.translation.as_vec3().into(), mode: GizmoMode::Rotate, - value: (self.normal().as_vec3() * state.current_delta).to_array(), + value: Some( + (gizmo_normal(&self.config, self.direction).as_vec3() * state.current_delta) + .to_array(), + ), }) } - fn draw(&self, ui: &Ui) { + fn draw(&mut self, ui: &Ui) { let state = self.state(ui); let config = self.config; @@ -102,10 +128,10 @@ impl SubGizmo for RotationSubGizmo { config.viewport, ); - let color = self.color(); + let color = gizmo_color(self, self.direction); let stroke = (config.visuals.stroke_width, color); - let radius = arc_radius(self) as f64; + let radius = arc_radius(self); if !self.active { let angle = arc_angle(self); @@ -149,8 +175,10 @@ impl SubGizmo for RotationSubGizmo { /// Calculates angle of the rotation axis arc. /// The arc is a semicircle, which turns into a full circle when viewed /// directly from the front. -fn arc_angle(subgizmo: &SubGizmoConfig) -> f64 { - let dot = subgizmo.normal().dot(subgizmo.config.view_forward()).abs(); +fn arc_angle(subgizmo: &SubGizmoConfig) -> f64 { + let dot = gizmo_normal(&subgizmo.config, subgizmo.direction) + .dot(subgizmo.config.view_forward()) + .abs(); let min_dot = 0.990; let max_dot = 0.995; @@ -163,8 +191,8 @@ fn arc_angle(subgizmo: &SubGizmoConfig) -> f64 { } /// Calculates a matrix used when rendering the rotation axis. -fn rotation_matrix(subgizmo: &SubGizmoConfig) -> DMat4 { - if subgizmo.direction == GizmoDirection::Screen { +fn rotation_matrix(subgizmo: &SubGizmoConfig) -> DMat4 { + if subgizmo.direction == GizmoDirection::View { let forward = subgizmo.config.view_forward(); let right = subgizmo.config.view_right(); let up = subgizmo.config.view_up(); @@ -175,7 +203,7 @@ fn rotation_matrix(subgizmo: &SubGizmoConfig) -> DMat4 { } // First rotate towards the gizmo normal - let local_normal = subgizmo.local_normal(); + let local_normal = gizmo_local_normal(&subgizmo.config, subgizmo.direction); let rotation = rotation_align(DVec3::Y, local_normal); let mut rotation = DQuat::from_mat3(&rotation); let config = subgizmo.config; @@ -185,7 +213,7 @@ fn rotation_matrix(subgizmo: &SubGizmoConfig) -> DMat4 { } let tangent = tangent(subgizmo); - let normal = subgizmo.normal(); + let normal = gizmo_normal(&subgizmo.config, subgizmo.direction); let mut forward = config.view_forward(); if config.left_handed { forward *= -1.0; @@ -198,7 +226,7 @@ fn rotation_matrix(subgizmo: &SubGizmoConfig) -> DMat4 { DMat4::from_rotation_translation(rotation, config.translation) } -fn rotation_angle(subgizmo: &SubGizmoConfig, ui: &Ui) -> Option { +fn rotation_angle(subgizmo: &SubGizmoConfig, ui: &Ui) -> Option { let cursor_pos = ui.input(|i| i.pointer.hover_pos())?; let viewport = subgizmo.config.viewport; let gizmo_pos = world_to_screen(viewport, subgizmo.config.mvp, DVec3::new(0.0, 0.0, 0.0))?; @@ -213,44 +241,36 @@ fn rotation_angle(subgizmo: &SubGizmoConfig, ui: &Ui) -> Option) -> DVec3 { +fn tangent(subgizmo: &SubGizmoConfig) -> DVec3 { let mut tangent = match subgizmo.direction { GizmoDirection::X | GizmoDirection::Y => DVec3::Z, GizmoDirection::Z => -DVec3::Y, - GizmoDirection::Screen => -subgizmo.config.view_right(), + GizmoDirection::View => -subgizmo.config.view_right(), }; - if subgizmo.config.local_space() && subgizmo.direction != GizmoDirection::Screen { + if subgizmo.config.local_space() && subgizmo.direction != GizmoDirection::View { tangent = subgizmo.config.rotation * tangent; } tangent } -fn arc_radius(subgizmo: &SubGizmoConfig) -> f32 { - let mut radius = subgizmo.config.visuals.gizmo_size; - - if subgizmo.direction == GizmoDirection::Screen { - // Screen axis should be a little bit larger - radius += subgizmo.config.visuals.stroke_width + 5.0; +fn arc_radius(subgizmo: &SubGizmoConfig) -> f64 { + if subgizmo.direction == GizmoDirection::View { + outer_circle_radius(&subgizmo.config) + } else { + (subgizmo.config.scale_factor * subgizmo.config.visuals.gizmo_size) as f64 } - - subgizmo.config.scale_factor * radius -} - -#[derive(Default, Debug, Copy, Clone)] -pub(crate) struct RotationState { - start_axis_angle: f32, - start_rotation_angle: f32, - last_rotation_angle: f32, - current_delta: f32, } - -impl SubGizmoState for RotationState {} diff --git a/src/subgizmo/scale.rs b/src/subgizmo/scale.rs index 78fb125..7eb43ba 100644 --- a/src/subgizmo/scale.rs +++ b/src/subgizmo/scale.rs @@ -4,18 +4,47 @@ use glam::DVec3; use crate::math::{round_to_interval, world_to_screen}; use crate::subgizmo::common::{ - draw_arrow, draw_plane, pick_arrow, pick_plane, plane_binormal, plane_tangent, + draw_arrow, draw_circle, draw_plane, gizmo_color, gizmo_local_normal, inner_circle_radius, + outer_circle_radius, pick_arrow, pick_circle, pick_plane, plane_bitangent, plane_tangent, + ArrowheadStyle, }; -use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState, TransformKind}; -use crate::{GizmoMode, GizmoResult, Ray}; +use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoKind, TransformKind}; +use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray}; -pub(crate) type ScaleSubGizmo = SubGizmoConfig; +pub(crate) type ScaleSubGizmo = SubGizmoConfig; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ScaleParams { + pub direction: GizmoDirection, + pub transform_kind: TransformKind, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct ScaleState { + start_scale: DVec3, + start_delta: f64, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Scale; + +impl SubGizmoKind for Scale { + type Params = ScaleParams; + type State = ScaleState; +} impl SubGizmo for ScaleSubGizmo { fn pick(&mut self, ui: &Ui, ray: Ray) -> Option { - let pick_result = match self.transform_kind { - TransformKind::Axis => pick_arrow(self, ray), - TransformKind::Plane => pick_plane(self, ray), + let pick_result = match (self.transform_kind, self.direction) { + (TransformKind::Plane, GizmoDirection::View) => { + let mut result = pick_circle(self, ray, inner_circle_radius(&self.config), true); + if !result.picked { + result = pick_circle(self, ray, outer_circle_radius(&self.config), false); + } + result + } + (TransformKind::Plane, _) => pick_plane(self, ray, self.direction), + (TransformKind::Axis, _) => pick_arrow(self, ray, self.direction), }; let start_delta = distance_from_origin_2d(self, ui)?; @@ -44,12 +73,12 @@ impl SubGizmo for ScaleSubGizmo { } delta = delta.max(1e-4) - 1.0; - let direction = if self.transform_kind == TransformKind::Plane { - let binormal = plane_binormal(self.direction); - let tangent = plane_tangent(self.direction); - (binormal + tangent).normalize() - } else { - self.local_normal() + let direction = match (self.transform_kind, self.direction) { + (TransformKind::Axis, _) => gizmo_local_normal(&self.config, self.direction), + (TransformKind::Plane, GizmoDirection::View) => DVec3::ONE, + (TransformKind::Plane, _) => { + (plane_bitangent(self.direction) + plane_tangent(self.direction)).normalize() + } }; let offset = DVec3::ONE + (direction * delta); @@ -57,30 +86,40 @@ impl SubGizmo for ScaleSubGizmo { Some(GizmoResult { scale: new_scale.as_vec3().into(), - rotation: self.config.rotation.as_f32().into(), + rotation: self.config.rotation.as_quat().into(), translation: self.config.translation.as_vec3().into(), mode: GizmoMode::Scale, - value: offset.as_vec3().to_array(), + value: Some(offset.as_vec3().to_array()), }) } - fn draw(&self, ui: &Ui) { - match self.transform_kind { - TransformKind::Axis => draw_arrow(self, ui), - TransformKind::Plane => draw_plane(self, ui), + fn draw(&mut self, ui: &Ui) { + match (self.transform_kind, self.direction) { + (TransformKind::Axis, _) => { + draw_arrow(self, ui, self.direction, ArrowheadStyle::Square); + } + (TransformKind::Plane, GizmoDirection::View) => { + draw_circle( + self, + ui, + gizmo_color(self, self.direction), + inner_circle_radius(&self.config), + false, + ); + draw_circle( + self, + ui, + gizmo_color(self, self.direction), + outer_circle_radius(&self.config), + false, + ); + } + (TransformKind::Plane, _) => draw_plane(self, ui, self.direction), } } } -#[derive(Default, Debug, Copy, Clone)] -pub(crate) struct ScaleState { - start_scale: DVec3, - start_delta: f64, -} - -impl SubGizmoState for ScaleState {} - -fn distance_from_origin_2d(subgizmo: &SubGizmoConfig, ui: &Ui) -> Option { +fn distance_from_origin_2d(subgizmo: &SubGizmoConfig, ui: &Ui) -> Option { let cursor_pos = ui.input(|i| i.pointer.hover_pos())?; let viewport = subgizmo.config.viewport; let gizmo_pos = world_to_screen(viewport, subgizmo.config.mvp, DVec3::new(0.0, 0.0, 0.0))?; diff --git a/src/subgizmo/translation.rs b/src/subgizmo/translation.rs index a7d0d1c..582dec3 100644 --- a/src/subgizmo/translation.rs +++ b/src/subgizmo/translation.rs @@ -4,19 +4,44 @@ use glam::DVec3; use crate::math::{intersect_plane, ray_to_ray, round_to_interval}; use crate::subgizmo::common::{ - draw_arrow, draw_plane, pick_arrow, pick_plane, plane_binormal, plane_global_origin, - plane_tangent, + draw_arrow, draw_circle, draw_plane, gizmo_color, gizmo_normal, inner_circle_radius, + pick_arrow, pick_circle, pick_plane, plane_bitangent, plane_global_origin, plane_tangent, + ArrowheadStyle, }; -use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoState, TransformKind}; -use crate::{GizmoMode, GizmoResult, Ray}; +use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoKind, TransformKind}; +use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray}; -pub(crate) type TranslationSubGizmo = SubGizmoConfig; +pub(crate) type TranslationSubGizmo = SubGizmoConfig; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct TranslationParams { + pub direction: GizmoDirection, + pub transform_kind: TransformKind, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct TranslationState { + start_point: DVec3, + last_point: DVec3, + current_delta: DVec3, +} + +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Translation; + +impl SubGizmoKind for Translation { + type Params = TranslationParams; + type State = TranslationState; +} impl SubGizmo for TranslationSubGizmo { fn pick(&mut self, ui: &Ui, ray: Ray) -> Option { - let pick_result = match self.transform_kind { - TransformKind::Axis => pick_arrow(self, ray), - TransformKind::Plane => pick_plane(self, ray), + let pick_result = match (self.transform_kind, self.direction) { + (TransformKind::Plane, GizmoDirection::View) => { + pick_circle(self, ray, inner_circle_radius(&self.config), true) + } + (TransformKind::Plane, _) => pick_plane(self, ray, self.direction), + (TransformKind::Axis, _) => pick_arrow(self, ray, self.direction), }; self.opacity = pick_result.visibility as _; @@ -40,7 +65,11 @@ impl SubGizmo for TranslationSubGizmo { let mut new_point = if self.transform_kind == TransformKind::Axis { point_on_axis(self, ray) } else { - point_on_plane(self.normal(), plane_global_origin(self), ray)? + point_on_plane( + gizmo_normal(&self.config, self.direction), + plane_global_origin(&self.config, self.direction), + ray, + )? }; let mut new_delta = new_point - state.start_point; @@ -63,34 +92,34 @@ impl SubGizmo for TranslationSubGizmo { Some(GizmoResult { scale: self.config.scale.as_vec3().into(), - rotation: self.config.rotation.as_f32().into(), + rotation: self.config.rotation.as_quat().into(), translation: new_translation.as_vec3().into(), mode: GizmoMode::Translate, - value: state.current_delta.as_vec3().to_array(), + value: Some(state.current_delta.as_vec3().to_array()), }) } - fn draw(&self, ui: &Ui) { - match self.transform_kind { - TransformKind::Axis => draw_arrow(self, ui), - TransformKind::Plane => draw_plane(self, ui), + fn draw(&mut self, ui: &Ui) { + match (self.transform_kind, self.direction) { + (TransformKind::Axis, _) => draw_arrow(self, ui, self.direction, ArrowheadStyle::Cone), + (TransformKind::Plane, GizmoDirection::View) => { + draw_circle( + self, + ui, + gizmo_color(self, self.direction), + inner_circle_radius(&self.config), + false, + ); + } + (TransformKind::Plane, _) => draw_plane(self, ui, self.direction), } } } -#[derive(Default, Debug, Copy, Clone)] -pub(crate) struct TranslationState { - start_point: DVec3, - last_point: DVec3, - current_delta: DVec3, -} - -impl SubGizmoState for TranslationState {} - /// Finds the nearest point on line that points in translation subgizmo direction -fn point_on_axis(subgizmo: &SubGizmoConfig, ray: Ray) -> DVec3 { +fn point_on_axis(subgizmo: &SubGizmoConfig, ray: Ray) -> DVec3 { let origin = subgizmo.config.translation; - let direction = subgizmo.normal(); + let direction = gizmo_normal(&subgizmo.config, subgizmo.direction); let (_ray_t, subgizmo_t) = ray_to_ray(ray.origin, ray.direction, origin, direction); @@ -112,7 +141,7 @@ fn point_on_plane(plane_normal: DVec3, plane_origin: DVec3, ray: Ray) -> Option< } } -fn snap_translation_vector(subgizmo: &SubGizmoConfig, new_delta: DVec3) -> DVec3 { +fn snap_translation_vector(subgizmo: &SubGizmoConfig, new_delta: DVec3) -> DVec3 { let delta_length = new_delta.length(); if delta_length > 1e-5 { new_delta / delta_length @@ -122,21 +151,21 @@ fn snap_translation_vector(subgizmo: &SubGizmoConfig, new_delt } } -fn snap_translation_plane(subgizmo: &SubGizmoConfig, new_delta: DVec3) -> DVec3 { - let mut binormal = plane_binormal(subgizmo.direction); +fn snap_translation_plane(subgizmo: &SubGizmoConfig, new_delta: DVec3) -> DVec3 { + let mut bitangent = plane_bitangent(subgizmo.direction); let mut tangent = plane_tangent(subgizmo.direction); if subgizmo.config.local_space() { - binormal = subgizmo.config.rotation * binormal; + bitangent = subgizmo.config.rotation * bitangent; tangent = subgizmo.config.rotation * tangent; } - let cb = new_delta.cross(-binormal); + let cb = new_delta.cross(-bitangent); let ct = new_delta.cross(tangent); let lb = cb.length(); let lt = ct.length(); - let n = subgizmo.normal(); + let n = gizmo_normal(&subgizmo.config, subgizmo.direction); if lb > 1e-5 && lt > 1e-5 { - binormal * round_to_interval(lt, subgizmo.config.snap_distance as f64) * (ct / lt).dot(n) + bitangent * round_to_interval(lt, subgizmo.config.snap_distance as f64) * (ct / lt).dot(n) + tangent * round_to_interval(lb, subgizmo.config.snap_distance as f64) * (cb / lb).dot(n)