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 <dannymcgee@gmail.com>
This commit is contained in:
Brooks Palin
2024-03-05 16:16:01 -07:00
committed by GitHub
parent 970f5b25bd
commit 9aaf15287a
6 changed files with 374 additions and 5 deletions
@@ -92,7 +92,6 @@ export class TransformToolbarElement extends LitElement {
<sts-icon icon="scale">Scale</sts-icon>
<input type="checkbox"
name="scale"
disabled
?checked=${Boolean(this._type & TransformType.Scale)}
@input=${this.#onTypeChange}
/>
+3
View File
@@ -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);
+11
View File
@@ -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,
@@ -9,6 +9,8 @@ use super::material::GizmoMaterial;
pub(super) struct GizmoMeshes {
pub translate_line: Handle<Mesh>,
pub translate_arrow: Handle<Mesh>,
pub scale_cube: Handle<Mesh>,
pub uniform_scale_cube: Handle<Mesh>,
pub translate_plane: Handle<Mesh>,
pub translate_ss: Handle<Mesh>,
pub rotate_ring: Handle<Mesh>,
@@ -26,6 +28,9 @@ pub(super) struct GizmoMats {
pub translate_xz: Handle<GizmoMaterial>,
pub translate_xy: Handle<GizmoMaterial>,
pub translate_ss: Handle<GizmoMaterial>,
pub scale_x: Handle<GizmoMaterial>,
pub scale_y: Handle<GizmoMaterial>,
pub scale_z: Handle<GizmoMaterial>,
pub rotate_x: Handle<GizmoMaterial>,
pub rotate_y: Handle<GizmoMaterial>,
pub rotate_z: Handle<GizmoMaterial>,
@@ -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,
+1 -4
View File
@@ -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);
+342
View File
@@ -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::<Pointer<Over>>::run(on_gizmo_element_hover),
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
On::<Pointer<DragStart>>::run(on_scale_start),
On::<Pointer<Drag>>::run(on_scale),
On::<Pointer<DragEnd>>::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<Pointer<DragStart>>,
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<Pointer<DragEnd>>,
mut cmd: Commands,
q_initial_transforms: Query<Entity, With<InitialTransform>>,
) {
for ent in q_initial_transforms.iter() {
cmd.entity(ent).remove::<InitialTransform>();
}
}
pub(super) fn on_scale(
event: Listener<Pointer<Drag>>,
mut q_root: Query<(&mut Transform, &InitialTransform, &InitialHitPoint)>,
q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
q_gizmo: Query<(&GlobalTransform, &TransformGizmo), Without<MainCamera>>,
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);
}
_ => {}
}
}