From 9aaf15287ae8abb83fef6b0c53afebb36514ddb6 Mon Sep 17 00:00:00 2001 From: Brooks Palin Date: Tue, 5 Mar 2024 16:16:01 -0700 Subject: [PATCH] Scaling Gizmo (#72) This PR adds the Scaling Gizmo to the StoryTeller Engine. Features - Objects can now be scaled by Axis or Plane Limitations - Currently there is no implementation for combining the scale gizmo with any of the other gizmo. This should be amended with further PRs. Let me know if there is anything that I missed! --------- Co-authored-by: Danny McGee --- .../src/lib/transform-toolbar.element.ts | 1 - studio/src/interaction/gizmos/common.rs | 3 + studio/src/interaction/gizmos/mod.rs | 11 + studio/src/interaction/gizmos/resources.rs | 17 + studio/src/interaction/gizmos/rotation.rs | 5 +- studio/src/interaction/gizmos/scale.rs | 342 ++++++++++++++++++ 6 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 studio/src/interaction/gizmos/scale.rs diff --git a/studio-web/src/lib/transform-toolbar.element.ts b/studio-web/src/lib/transform-toolbar.element.ts index 8a8f4e1..3e5bef7 100644 --- a/studio-web/src/lib/transform-toolbar.element.ts +++ b/studio-web/src/lib/transform-toolbar.element.ts @@ -92,7 +92,6 @@ export class TransformToolbarElement extends LitElement { Scale diff --git a/studio/src/interaction/gizmos/common.rs b/studio/src/interaction/gizmos/common.rs index 6c5e46f..bd1e5e0 100644 --- a/studio/src/interaction/gizmos/common.rs +++ b/studio/src/interaction/gizmos/common.rs @@ -84,3 +84,6 @@ pub(super) fn on_gizmo_element_unhover( let mat = ra_mats.get_mut(q_target.get(ev.target).unwrap()).unwrap(); mat.color *= 0.5; } + +#[derive(Component, Deref)] +pub(super) struct InitialHitPoint(pub Vec3); diff --git a/studio/src/interaction/gizmos/mod.rs b/studio/src/interaction/gizmos/mod.rs index 5ed674d..929721d 100644 --- a/studio/src/interaction/gizmos/mod.rs +++ b/studio/src/interaction/gizmos/mod.rs @@ -9,6 +9,7 @@ use self::{ material::GizmoMaterial, resources::{init_resources, GizmoMats, GizmoMeshes}, rotation::{spawn_rotation_gizmo, update_ss_ring_rotation}, + scale::spawn_scale_gizmo, translation::spawn_translation_gizmo, }; pub use common::TransformGizmo; @@ -21,6 +22,7 @@ mod common; mod material; mod resources; mod rotation; +mod scale; mod translation; const GIZMO_RENDER_LAYER: u8 = 1; @@ -130,6 +132,15 @@ fn spawn_gizmo( &r_mats, ); } + TransformType::SCALE => spawn_scale_gizmo( + &mut cmd, + target, + xform, + r_xform_mode.space, + normalizing_scale, + &r_meshes, + &r_mats, + ), ttype if ttype.contains(TransformType::TRANSLATE | TransformType::ROTATE) => { spawn_combo_translation_rotation_gizmo( &mut cmd, diff --git a/studio/src/interaction/gizmos/resources.rs b/studio/src/interaction/gizmos/resources.rs index bc92151..9bcc486 100644 --- a/studio/src/interaction/gizmos/resources.rs +++ b/studio/src/interaction/gizmos/resources.rs @@ -9,6 +9,8 @@ use super::material::GizmoMaterial; pub(super) struct GizmoMeshes { pub translate_line: Handle, pub translate_arrow: Handle, + pub scale_cube: Handle, + pub uniform_scale_cube: Handle, pub translate_plane: Handle, pub translate_ss: Handle, pub rotate_ring: Handle, @@ -26,6 +28,9 @@ pub(super) struct GizmoMats { pub translate_xz: Handle, pub translate_xy: Handle, pub translate_ss: Handle, + pub scale_x: Handle, + pub scale_y: Handle, + pub scale_z: Handle, pub rotate_x: Handle, pub rotate_y: Handle, pub rotate_z: Handle, @@ -78,6 +83,9 @@ pub(super) fn init_resources( subdivisions_sides: 6, })); + let scale_cube = ra_meshes.add(Mesh::from(shape::Cube { size: 0.2 })); + let uniform_scale_cube = ra_meshes.add(Mesh::from(shape::Cube { size: 0.333 })); + cmd.insert_resource(GizmoMeshes { translate_line, translate_arrow, @@ -86,6 +94,8 @@ pub(super) fn init_resources( rotate_ring, rotate_ball, rotate_ss, + scale_cube, + uniform_scale_cube, }); let translate_x = ra_mats.add(design_system::Color::RED.into()); @@ -104,6 +114,10 @@ pub(super) fn init_resources( let rotate_z = ra_mats.add(design_system::Color::BLUE.into()); let rotate_ss = ra_mats.add(Color::WHITE.into()); + let scale_x = ra_mats.add(design_system::Color::RED.into()); + let scale_y = ra_mats.add(design_system::Color::GREEN.into()); + let scale_z = ra_mats.add(design_system::Color::BLUE.into()); + let rotate_trackball = ra_mats.add(GizmoMaterial { color: Color::WHITE.with_a(0.0), blend_mode: AlphaMode::Blend, @@ -117,6 +131,9 @@ pub(super) fn init_resources( translate_xz, translate_xy, translate_ss, + scale_x, + scale_y, + scale_z, rotate_x, rotate_y, rotate_z, diff --git a/studio/src/interaction/gizmos/rotation.rs b/studio/src/interaction/gizmos/rotation.rs index f2ef516..3c0420e 100644 --- a/studio/src/interaction/gizmos/rotation.rs +++ b/studio/src/interaction/gizmos/rotation.rs @@ -8,7 +8,7 @@ use bevy_mod_picking::prelude::*; use bevy_mod_raycast::prelude::{Raycast, RaycastSettings}; use crate::{ - interaction::TransformSpace, + interaction::{gizmos::InitialHitPoint, TransformSpace}, math::{MatrixExtras, NearlyZero}, MainCamera, }; @@ -25,9 +25,6 @@ pub(super) enum RotationWidget { Trackball, } -#[derive(Component, Deref)] -pub(super) struct InitialHitPoint(pub Vec3); - #[derive(Component, Deref, DerefMut)] pub(super) struct LastHitPoint(pub Vec3); diff --git a/studio/src/interaction/gizmos/scale.rs b/studio/src/interaction/gizmos/scale.rs new file mode 100644 index 0000000..1babb0a --- /dev/null +++ b/studio/src/interaction/gizmos/scale.rs @@ -0,0 +1,342 @@ +use bevy::{math::vec3, pbr::NotShadowCaster, prelude::*, render::view::RenderLayers}; +use bevy_mod_picking::prelude::{Drag, DragEnd, DragStart, Listener, On, Out, Over, Pointer}; + +use crate::{interaction::TransformSpace, math::NearlyEq, MainCamera}; + +use super::{ + on_gizmo_element_hover, on_gizmo_element_unhover, GizmoMats, GizmoMeshes, InitialHitPoint, + InitialTransform, TransformGizmo, TransformGizmoBundle, GIZMO_RENDER_LAYER, +}; + +#[derive(Component)] +pub(super) enum ScaleWidget { + Axis(Vec3), + Plane(Vec3), + UniformScale, +} + +#[inline] +pub(super) fn spawn_scale_gizmo( + cmd: &mut Commands, + target: Entity, + target_global_transform: &GlobalTransform, + space: TransformSpace, + normalizing_scale: f32, + meshes: &GizmoMeshes, + mats: &GizmoMats, +) { + cmd.spawn(( + TransformGizmoBundle::new(target, target_global_transform, space, normalizing_scale), + On::>::run(on_gizmo_element_hover), + On::>::run(on_gizmo_element_unhover), + On::>::run(on_scale_start), + On::>::run(on_scale), + On::>::run(on_scale_end), + )) + .with_children(|cmd| { + use super::{CIRC_45, ROT_X, ROT_Y, ROT_Z}; + + // Center sphere + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::UniformScale, + MaterialMeshBundle { + mesh: meshes.uniform_scale_cube.clone(), + material: mats.translate_ss.clone(), + transform: Transform::IDENTITY, + ..default() + }, + )); + + // X + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::X), + MaterialMeshBundle { + mesh: meshes.translate_line.clone(), + material: mats.scale_x.clone(), + transform: Transform { + rotation: ROT_X, + translation: vec3(0.6, 0., 0.), + ..default() + }, + ..default() + }, + )); + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::X), + MaterialMeshBundle { + mesh: meshes.scale_cube.clone(), + material: mats.scale_x.clone(), + transform: Transform { + rotation: ROT_X, + translation: vec3(1., 0., 0.), + ..default() + }, + ..default() + }, + )); + + // Y + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::Y), + MaterialMeshBundle { + mesh: meshes.translate_line.clone(), + material: mats.scale_y.clone(), + transform: Transform { + rotation: ROT_Y, + translation: vec3(0., 0.6, 0.), + ..default() + }, + ..default() + }, + )); + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::Y), + MaterialMeshBundle { + mesh: meshes.scale_cube.clone(), + material: mats.scale_y.clone(), + transform: Transform { + rotation: ROT_Y, + translation: vec3(0., 1., 0.), + ..default() + }, + ..default() + }, + )); + + // Z + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::Z), + MaterialMeshBundle { + mesh: meshes.translate_line.clone(), + material: mats.scale_z.clone(), + transform: Transform { + rotation: ROT_Z, + translation: vec3(0., 0., 0.6), + ..default() + }, + ..default() + }, + )); + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Axis(Vec3::Z), + MaterialMeshBundle { + mesh: meshes.scale_cube.clone(), + material: mats.scale_z.clone(), + transform: Transform { + rotation: ROT_Z, + translation: vec3(0., 0., 1.), + ..default() + }, + ..default() + }, + )); + + // YZ Plane + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Plane(Vec3::X), + MaterialMeshBundle { + mesh: meshes.translate_plane.clone(), + material: mats.translate_yz.clone(), + transform: Transform { + rotation: ROT_X, + translation: vec3(0., CIRC_45, CIRC_45), + ..default() + }, + ..default() + }, + )); + // XZ Plane + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Plane(Vec3::Y), + MaterialMeshBundle { + mesh: meshes.translate_plane.clone(), + material: mats.translate_xz.clone(), + transform: Transform { + rotation: ROT_Y, + translation: vec3(CIRC_45, 0., CIRC_45), + ..default() + }, + ..default() + }, + )); + // XY Plane + cmd.spawn(( + NotShadowCaster, + RenderLayers::layer(GIZMO_RENDER_LAYER), + ScaleWidget::Plane(Vec3::Z), + MaterialMeshBundle { + mesh: meshes.translate_plane.clone(), + material: mats.translate_xy.clone(), + transform: Transform { + rotation: ROT_Z, + translation: vec3(CIRC_45, CIRC_45, 0.), + ..default() + }, + ..default() + }, + )); + }); +} + +pub(super) fn on_scale_start( + event: Listener>, + mut cmd: Commands, + q_xform: Query<(Entity, &GlobalTransform)>, + q_gizmos: Query<&TransformGizmo>, +) { + for gizmo_data in q_gizmos.iter() { + if let Ok((ent, &world_xform)) = q_xform.get(gizmo_data.target) { + let hit_point = event.hit.position.unwrap(); + + cmd.entity(ent) + .insert((InitialTransform(world_xform), InitialHitPoint(hit_point))); + } + } +} + +pub(super) fn on_scale_end( + _: Listener>, + mut cmd: Commands, + q_initial_transforms: Query>, +) { + for ent in q_initial_transforms.iter() { + cmd.entity(ent).remove::(); + } +} + +pub(super) fn on_scale( + event: Listener>, + mut q_root: Query<(&mut Transform, &InitialTransform, &InitialHitPoint)>, + q_camera: Query<(&Camera, &GlobalTransform), With>, + q_gizmo: Query<(&GlobalTransform, &TransformGizmo), Without>, + q_widget: Query<&ScaleWidget>, +) { + let (gizmo_xform, gizmo_data) = q_gizmo.single(); + let Ok((mut target_transform, init_global_transform, initial_hit_point)) = + q_root.get_mut(gizmo_data.target) + else { + error!("Scale gizmo target not found"); + return; + }; + let (cam, cam_xform) = q_camera.single(); + + // Find the initial origin point in screen-space and add the total screen-space drag distance + let (initial_scale, _, origin_ws) = init_global_transform.to_scale_rotation_translation(); + let origin_ss = cam + .world_to_viewport(cam_xform, **initial_hit_point) + .unwrap(); + let target_ss = origin_ss + event.distance; + + // Create a camera-to-world ray that passes through the screen-space target point + let ray = cam.viewport_to_world(cam_xform, target_ss).unwrap(); + + use ScaleWidget::*; + + match q_widget.get(event.target) { + Ok(Axis(axis)) => { + // Transform the translation axis into world-space + let axis_ws = gizmo_xform.affine().transform_vector3(*axis).normalize(); + + // Find the plane vector that's orthogonal to the translation axis + // and closest to parallel with the view direction + let axis_tan = axis_ws.cross(cam_xform.back()); + let plane_vector = axis_ws.cross(axis_tan); + + // Find the intersection of the camera-to-world ray against that plane + let Some(hit_dist) = ray.intersect_plane(origin_ws, plane_vector) else { + return; + }; + let hit_point = ray.origin + ray.direction * hit_dist; + + // Project the vector from origin -> hit point onto the translation + // axis to constrain the translation + let drag_vector = hit_point - origin_ws; + let projection_length = drag_vector.dot(axis_ws); + let target_ws = origin_ws + axis_ws * projection_length; + + let distance_from_initial_click = initial_hit_point.distance(origin_ws); + let distance_from_current = target_ws.distance(origin_ws); + + let scale_factor = distance_from_current / distance_from_initial_click; + + info!("Scale Factor {scale_factor}"); + + let (target_x, target_y, target_z) = initial_scale.into(); + let new_scale = if axis.nearly_eq(Vec3::Y) { + Vec3::new(target_x, target_y * scale_factor, target_z) + } else if axis.nearly_eq(Vec3::X) { + Vec3::new(target_x * scale_factor, target_y, target_z) + } else if axis.nearly_eq(Vec3::Z) { + Vec3::new(target_x, target_y, target_z * scale_factor) + } else { + unreachable!(); + }; + + info!("New Scale {:?}", new_scale); + + target_transform.scale = new_scale; + } + Ok(Plane(plane_normal)) => { + let normal_ws = gizmo_xform + .affine() + .transform_vector3(*plane_normal) + .normalize(); + let Some(hit_dist) = ray.intersect_plane(origin_ws, normal_ws) else { + return; + }; + let hit_point = ray.origin + ray.direction * hit_dist; + + let distance_from_initial_click = initial_hit_point.distance(origin_ws); + let distance_from_current = hit_point.distance(origin_ws); + + let scale = distance_from_current / distance_from_initial_click; + + let scale_offset = if plane_normal.nearly_eq(Vec3::Z) { + Vec3::new(scale, scale, 1.0) + } else if plane_normal.nearly_eq(Vec3::Y) { + Vec3::new(scale, 1.0, scale) + } else if plane_normal.nearly_eq(Vec3::X) { + Vec3::new(1.0, scale, scale) + } else { + unreachable!(); + }; + + target_transform.scale = initial_scale * scale_offset; + } + Ok(UniformScale) => { + let original_click_ss = cam + .world_to_viewport(cam_xform, **initial_hit_point) + .unwrap(); + + let sign = if original_click_ss.y > target_ss.y { + 1.0 + } else { + -1.0 + }; + let divisor = original_click_ss.y; + let distance_click = original_click_ss.distance(target_ss); + + target_transform.scale = + initial_scale + Vec3::splat((distance_click / divisor * sign) * 1.5); + } + _ => {} + } +}