Skip to content

Commit

Permalink
Initial ArbBall rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
urholaukkarinen committed Feb 1, 2024
1 parent b05bcd9 commit 24a14b9
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 74 deletions.
38 changes: 28 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
};
Expand Down Expand Up @@ -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);
}
Expand All @@ -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] {
[
Expand Down Expand Up @@ -345,15 +358,19 @@ impl Gizmo {

/// Calculate a world space ray from current mouse position
fn pointer_ray(&self, ui: &Ui) -> Option<Ray> {
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,
})
}
}

Expand Down Expand Up @@ -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,
}
Expand Down
62 changes: 34 additions & 28 deletions src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,62 @@ impl Painter3d {
}
}

pub fn arc(
&self,
radius: f64,
start_angle: f64,
end_angle: f64,
stroke: impl Into<Stroke>,
) -> 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<Pos2> {
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;

points.push(DVec3::new(x, 0.0, z));
}

let points = points
points
.into_iter()
.filter_map(|point| self.vec3_to_pos2(point))
.collect::<Vec<_>>();
.collect::<Vec<_>>()
}

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<Stroke>,
) -> 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<Stroke>) -> 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<Stroke>) {
let mut points: [Pos2; 2] = Default::default();

Expand Down
6 changes: 4 additions & 2 deletions src/subgizmo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,7 +55,7 @@ pub(crate) trait SubGizmoBase: 'static {
fn is_active(&self) -> bool;
}

impl<T: 'static> SubGizmoBase for SubGizmoConfig<T> {
impl<T: SubGizmoState> SubGizmoBase for SubGizmoConfig<T> {
fn id(&self) -> Id {
self.id
}
Expand Down Expand Up @@ -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<GizmoResult>;
/// Draw the subgizmo
fn draw(&self, ui: &Ui);
fn draw(&mut self, ui: &Ui);
}

impl<T> SubGizmoConfig<T>
Expand Down
79 changes: 79 additions & 0 deletions src/subgizmo/arcball.rs
Original file line number Diff line number Diff line change
@@ -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<ArcballState>;

impl SubGizmo for ArcballSubGizmo {
fn pick(&mut self, ui: &Ui, ray: Ray) -> Option<f64> {
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<GizmoResult> {
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<T: SubGizmoState>(subgizmo: &SubGizmoConfig<T>) -> 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 {}
Loading

0 comments on commit 24a14b9

Please sign in to comment.