diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 70e5bbc..dd0b764 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -12,7 +12,7 @@ import styles from "./app.element.scss?inline"; import { match } from "@storyteller/utility"; // FIXME: Figure out how to generate the correct code from `wasm_bindgen` -enum InteractionTransform { +enum TransformType { TRANSLATE = 0b0001, ROTATE = 0b0010, } @@ -92,9 +92,9 @@ export class AppElement extends LitElement { }); let transform = 0; - if (this.translateGizmoEnabled) transform |= InteractionTransform.TRANSLATE; - if (this.rotateGizmoEnabled) transform |= InteractionTransform.ROTATE; - // TODO: Scale + if (this.translateGizmoEnabled) transform |= TransformType.TRANSLATE; + if (this.rotateGizmoEnabled) transform |= TransformType.ROTATE; + // TODO: Scale? studio.setInteractionTransform(transform); } diff --git a/studio/src/interaction/gizmos/composition.rs b/studio/src/interaction/gizmos/composition.rs new file mode 100644 index 0000000..920fda7 --- /dev/null +++ b/studio/src/interaction/gizmos/composition.rs @@ -0,0 +1,271 @@ +use std::mem; + +use bevy::{prelude::Color, utils::default}; + +use crate::{design_system, interaction::InteractionMode}; + +use super::{ + model::{Axes, Control, Gizmo, GizmoColor, GizmoElement, Shape}, + TransformType, +}; + +impl Gizmo { + pub(super) fn update_composition(&mut self, mode: &InteractionMode) { + let indices = Indices::collect_from(self); + self.remove_extras(mode.transform, &indices); + self.add_missing(mode.transform, &indices); + } + + fn remove_extras(&mut self, ttype: TransformType, indices: &Indices) { + let mut to_remove = + Vec::::with_capacity(mem::size_of::() / mem::size_of::()); + + match ttype { + TransformType::TRANSLATE_ROTATE => { + to_remove.extend_from_slice(&[ + indices.xlate_x, + indices.xlate_y, + indices.xlate_z, + indices.xlate_xy, + indices.xlate_xz, + indices.xlate_yz, + ]); + } + TransformType::TRANSLATE => { + to_remove.extend_from_slice(&[ + indices.rotate_screen, + indices.rotate_x, + indices.rotate_y, + indices.rotate_z, + ]); + } + TransformType::ROTATE => { + to_remove.extend_from_slice(&[ + indices.xlate_screen, + indices.xlate_x, + indices.xlate_x_cone, + indices.xlate_y, + indices.xlate_y_cone, + indices.xlate_z, + indices.xlate_z_cone, + indices.xlate_xy, + indices.xlate_xz, + indices.xlate_yz, + ]); + } + _ => { + self.0.clear(); + return; + } + } + + to_remove.sort(); + + for idx in to_remove.into_iter().rev().filter_map(|idx| { + if idx == -1 { + None + } else { + Some(idx as usize) + } + }) { + self.0.swap_remove(idx); + } + } + + fn add_missing(&mut self, ttype: TransformType, indices: &Indices) { + if ttype.contains(TransformType::ROTATE) { + if indices.rotate_screen == -1 { + self.0.push(GizmoElement { + shape: Shape::Ring(default()), + color: Color::WHITE.into(), + control: Control::Rotation(Axes::SCREEN_Z), + }); + } + if indices.rotate_x == -1 { + self.0.push(GizmoElement { + shape: Shape::Ring(default()), + color: design_system::Color::RED.into(), + control: Control::Rotation(Axes::X), + }); + } + if indices.rotate_y == -1 { + self.0.push(GizmoElement { + shape: Shape::Ring(default()), + color: design_system::Color::GREEN.into(), + control: Control::Rotation(Axes::Y), + }); + } + if indices.rotate_z == -1 { + self.0.push(GizmoElement { + shape: Shape::Ring(default()), + color: design_system::Color::BLUE.into(), + control: Control::Rotation(Axes::Z), + }); + } + } + + if ttype.contains(TransformType::TRANSLATE) { + if indices.xlate_screen == -1 { + self.0.push(GizmoElement { + shape: Shape::Ring(default()), + color: Color::WHITE.into(), + control: Control::Translation(Axes::SCREEN_XY), + }); + } + if indices.xlate_x_cone == -1 { + self.0.push(GizmoElement { + shape: Shape::Cone(default()), + color: design_system::Color::RED.into(), + control: Control::Translation(Axes::X), + }); + } + if indices.xlate_y_cone == -1 { + self.0.push(GizmoElement { + shape: Shape::Cone(default()), + color: design_system::Color::GREEN.into(), + control: Control::Translation(Axes::Y), + }); + } + if indices.xlate_z_cone == -1 { + self.0.push(GizmoElement { + shape: Shape::Cone(default()), + color: design_system::Color::BLUE.into(), + control: Control::Translation(Axes::Z), + }); + } + + if !ttype.contains(TransformType::ROTATE) { + if indices.xlate_x == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: design_system::Color::RED.into(), + control: Control::Translation(Axes::X), + }); + } + if indices.xlate_y == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: design_system::Color::GREEN.into(), + control: Control::Translation(Axes::Y), + }); + } + if indices.xlate_z == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: design_system::Color::BLUE.into(), + control: Control::Translation(Axes::Z), + }); + } + if indices.xlate_xy == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: GizmoColor::Gradient( + design_system::Color::RED, + design_system::Color::GREEN, + ), + control: Control::Translation(Axes::XY), + }); + } + if indices.xlate_xz == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: GizmoColor::Gradient( + design_system::Color::RED, + design_system::Color::BLUE, + ), + control: Control::Translation(Axes::XZ), + }); + } + if indices.xlate_yz == -1 { + self.0.push(GizmoElement { + shape: Shape::Line(default()), + color: GizmoColor::Gradient( + design_system::Color::GREEN, + design_system::Color::BLUE, + ), + control: Control::Translation(Axes::YZ), + }); + } + } + } + } +} + +struct Indices { + xlate_screen: i32, + xlate_x: i32, + xlate_x_cone: i32, + xlate_y: i32, + xlate_y_cone: i32, + xlate_z: i32, + xlate_z_cone: i32, + xlate_xy: i32, + xlate_xz: i32, + xlate_yz: i32, + rotate_screen: i32, + rotate_x: i32, + rotate_y: i32, + rotate_z: i32, +} + +impl Default for Indices { + fn default() -> Self { + Self { + xlate_screen: -1, + xlate_x: -1, + xlate_x_cone: -1, + xlate_y: -1, + xlate_y_cone: -1, + xlate_z: -1, + xlate_z_cone: -1, + xlate_xy: -1, + xlate_xz: -1, + xlate_yz: -1, + rotate_screen: -1, + rotate_x: -1, + rotate_y: -1, + rotate_z: -1, + } + } +} + +impl Indices { + fn collect_from(gizmo: &Gizmo) -> Self { + let mut result = Indices::default(); + + for (idx, GizmoElement { control, shape, .. }) in gizmo.0.iter().enumerate() { + match control { + Control::Translation(axes) => match *axes { + Axes::SCREEN_XY => result.xlate_screen = idx as i32, + Axes::XY => result.xlate_xy = idx as i32, + Axes::XZ => result.xlate_xz = idx as i32, + Axes::YZ => result.xlate_yz = idx as i32, + axis => match shape { + Shape::Line { .. } => match axis { + Axes::X => result.xlate_x = idx as i32, + Axes::Y => result.xlate_y = idx as i32, + Axes::Z => result.xlate_z = idx as i32, + _ => unreachable!(), + }, + Shape::Cone { .. } => match axis { + Axes::X => result.xlate_x_cone = idx as i32, + Axes::Y => result.xlate_y_cone = idx as i32, + Axes::Z => result.xlate_z_cone = idx as i32, + _ => unreachable!(), + }, + _ => unreachable!(), + }, + }, + Control::Rotation(axes) => match *axes { + Axes::SCREEN_Z => result.rotate_screen = idx as i32, + Axes::X => result.rotate_x = idx as i32, + Axes::Y => result.rotate_y = idx as i32, + Axes::Z => result.rotate_z = idx as i32, + _ => unreachable!(), + }, + } + } + + result + } +} diff --git a/studio/src/interaction/gizmos/mod.rs b/studio/src/interaction/gizmos/mod.rs index 27d0ebe..feff9ef 100644 --- a/studio/src/interaction/gizmos/mod.rs +++ b/studio/src/interaction/gizmos/mod.rs @@ -1,14 +1,17 @@ +mod composition; mod model; mod view; use bevy::prelude::*; -use super::{InteractionMode, InteractionTransform, Selected, TransformSpace}; +use super::{InteractionMode, Selected, TransformSpace, TransformType}; pub(super) struct StudioGizmosPlugin; impl Plugin for StudioGizmosPlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, view::draw); + app.add_systems(PreUpdate, (model::update_spawns, view::configure)) + .add_systems(Update, model::update) + .add_systems(PostUpdate, view::draw); } } diff --git a/studio/src/interaction/gizmos/model.rs b/studio/src/interaction/gizmos/model.rs index e69de29..2d23359 100644 --- a/studio/src/interaction/gizmos/model.rs +++ b/studio/src/interaction/gizmos/model.rs @@ -0,0 +1,315 @@ +use bevy::prelude::*; +use bitflags::bitflags; + +use super::{InteractionMode, Selected, TransformSpace, TransformType}; + +pub(super) fn update_spawns( + mut cmd: Commands, + q_needs_spawn: Query, Without)>, + q_needs_despawn: Query, With)>, +) { + // TODO: Is there a way to take a component from one entity and move it onto + // another entity without recreating the component from scratch? Would + // save us some unnecessary reallocations since Gizmo holds a vector + for ent in q_needs_despawn.iter() { + cmd.entity(ent).remove::(); + } + + for ent in q_needs_spawn.iter() { + cmd.entity(ent).insert(Gizmo::default()); + } +} + +pub(super) fn update( + mut q_gizmo: Query<(&mut Gizmo, &GlobalTransform)>, + q_camera_xform: Query<&Transform, With>, + mode: Res, +) { + let Ok((mut gizmo, world_xform)) = q_gizmo.get_single_mut() else { + return; + }; + let Ok(cam_xform) = q_camera_xform.get_single() else { + return; + }; + + gizmo.update_composition(&mode); + + let xform = world_xform.compute_transform(); + let basis = match mode.space { + TransformSpace::Local => xform, + TransformSpace::World => Transform::IDENTITY, + }; + + for el in gizmo.0.iter_mut() { + el.update(mode.transform, &xform, &basis, cam_xform); + } +} + +#[derive(Component, Clone, Default, Debug)] +pub(super) struct Gizmo(pub Vec); + +#[derive(Clone, Copy, Debug)] +pub(super) struct GizmoElement { + pub shape: Shape, + pub color: GizmoColor, + pub control: Control, +} + +impl GizmoElement { + #[inline] + fn update( + &mut self, + ttype: TransformType, + xform: &Transform, + basis: &Transform, + cam_xform: &Transform, + ) { + let dist = Vec3::distance(cam_xform.translation, xform.translation); + let base_size = dist / 20.; + + match self.control { + Control::Translation(axes) => match axes { + Axes::SCREEN_XY => { + self.update_xlation_ring(xform, cam_xform, base_size); + } + Axes::X | Axes::Y | Axes::Z => match self.shape { + Shape::Line(_) => { + self.update_xlation_line(axes, xform, basis, base_size); + } + Shape::Cone(_) => { + self.update_xlation_cone(ttype, axes, xform, basis, base_size); + } + _ => unreachable!(), + }, + Axes::XY | Axes::XZ | Axes::YZ => { + self.update_xlation_crossbar(axes, xform, basis, base_size); + } + _ => unreachable!(), + }, + Control::Rotation(axes) => match axes { + Axes::SCREEN_Z => { + self.update_rotation_ring_screenspace(xform, cam_xform, base_size); + } + Axes::X | Axes::Y | Axes::Z => { + self.update_rotation_ring(axes, xform, basis, base_size); + } + _ => unreachable!(), + }, + } + } + + #[inline] + fn update_xlation_ring(&mut self, xform: &Transform, cam_xform: &Transform, base_size: f32) { + let Shape::Ring(ring) = &mut self.shape else { + panic!("Wrong shape for Screen XY Translation: {:?}", self.shape) + }; + + ring.origin = xform.translation; + ring.normal = cam_xform.forward(); + ring.radius = base_size * 0.2; + } + + #[inline] + fn update_xlation_line( + &mut self, + axis: Axes, + xform: &Transform, + basis: &Transform, + base_size: f32, + ) { + let Shape::Line(line) = &mut self.shape else { + unreachable!() + }; + + let direction = match axis { + Axes::X => basis.local_x(), + Axes::Y => basis.local_y(), + Axes::Z => basis.local_z(), + _ => unreachable!(), + }; + + line.origin = xform.translation + direction * base_size * 0.25; + line.direction = direction; + line.length = base_size * 1.35; + } + + #[inline] + fn update_xlation_cone( + &mut self, + ttype: TransformType, + axis: Axes, + xform: &Transform, + basis: &Transform, + base_size: f32, + ) { + let Shape::Cone(cone) = &mut self.shape else { + unreachable!() + }; + + let offset_factor = if ttype.contains(TransformType::ROTATE) { + 1.4 + } else { + 1.2 + }; + + let direction = match axis { + Axes::X => basis.local_x(), + Axes::Y => basis.local_y(), + Axes::Z => basis.local_z(), + _ => unreachable!(), + }; + + cone.base = xform.translation + direction * base_size * offset_factor; + cone.direction = direction; + cone.radius = base_size * 0.1; + cone.length = base_size * 0.4; + } + + #[inline] + fn update_xlation_crossbar( + &mut self, + plane: Axes, + xform: &Transform, + basis: &Transform, + base_size: f32, + ) { + let Shape::Line(line) = &mut self.shape else { + unreachable!() + }; + + let (a, b) = match plane { + Axes::XY => (basis.local_x(), basis.local_y()), + Axes::XZ => (basis.local_x(), basis.local_z()), + Axes::YZ => (basis.local_y(), basis.local_z()), + _ => unreachable!(), + }; + + let start = xform.translation + a * base_size * 0.925; + let end = xform.translation + b * base_size * 0.925; + + let vector = end - start; + let length = vector.length(); + let direction = vector / length; + + line.origin = start; + line.direction = direction; + line.length = length; + } + + #[inline] + fn update_rotation_ring_screenspace( + &mut self, + xform: &Transform, + cam_xform: &Transform, + base_size: f32, + ) { + let Shape::Ring(ring) = &mut self.shape else { + unreachable!() + }; + + ring.origin = xform.translation; + ring.normal = cam_xform.forward(); + ring.radius = base_size * 1.2; + } + + #[inline] + fn update_rotation_ring( + &mut self, + axis: Axes, + xform: &Transform, + basis: &Transform, + base_size: f32, + ) { + let Shape::Ring(ring) = &mut self.shape else { + unreachable!() + }; + + let normal = match axis { + Axes::X => basis.local_x(), + Axes::Y => basis.local_y(), + Axes::Z => basis.local_z(), + _ => unreachable!(), + }; + + ring.origin = xform.translation; + ring.normal = normal; + ring.radius = base_size; + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) enum GizmoColor { + Single(Color), + Gradient(Color, Color), +} + +impl From for GizmoColor { + fn from(value: Color) -> Self { + Self::Single(value) + } +} + +impl From<(Color, Color)> for GizmoColor { + fn from(value: (Color, Color)) -> Self { + Self::Gradient(value.0, value.1) + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) enum Shape { + Ring(Ring), + Line(Line), + Cone(Cone), +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct Ring { + pub origin: Vec3, + pub normal: Vec3, + pub radius: f32, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct Arc { + pub center: Vec3, + pub axis: Vec3, + pub start: Vec3, + pub length: f32, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct Line { + pub origin: Vec3, + pub direction: Vec3, + pub length: f32, +} + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct Cone { + pub base: Vec3, + pub direction: Vec3, + pub radius: f32, + pub length: f32, +} + +#[derive(Clone, Copy, Debug)] +pub(super) enum Control { + Translation(Axes), + Rotation(Axes), +} + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub (super) struct Axes: u8 { + const X = 0b0001; + const Y = 0b0010; + const Z = 0b0100; + const SCREEN = 0b1000; + const XY = 0b0011; + const XZ = 0b0101; + const YZ = 0b0110; + const XYZ = 0b0111; + const SCREEN_XY = 0b1011; + const SCREEN_Z = 0b1100; + } +} diff --git a/studio/src/interaction/gizmos/view.rs b/studio/src/interaction/gizmos/view.rs index 213dc38..69a7ed6 100644 --- a/studio/src/interaction/gizmos/view.rs +++ b/studio/src/interaction/gizmos/view.rs @@ -2,240 +2,338 @@ use std::f32::consts; use bevy::{prelude::*, window::PrimaryWindow}; -use crate::{design_system, math::Lerp}; +// use crate::{design_system, math::Lerp}; -use super::{InteractionMode, InteractionTransform, Selected, TransformSpace}; +#[rustfmt::skip] +use super::{ + model::{Arc, Axes, Control, Gizmo, GizmoColor, Ring, Shape}, + // InteractionMode, Selected, TransformSpace, TransformType, +}; -#[derive(Clone, Copy)] -enum GizmoVariant { - Full, - Partial, +// #[derive(Clone, Copy)] +// enum GizmoVariant { +// Full, +// Partial, +// } + +pub(super) fn configure( + q_window: Query<&Window, With>, + mut config: ResMut, +) { + let scale_factor = q_window.single().scale_factor() as f32; + config.line_width = 4. * scale_factor; + config.line_perspective = false; + config.depth_bias = -1.; } pub(super) fn draw( - q_selection: Query<&GlobalTransform, With>, + q_gizmo: Query<&Gizmo>, q_camera_xform: Query<&Transform, With>, - q_window: Query<&Window, With>, - mode: Res, mut gizmos: Gizmos, - mut config: ResMut, ) { - if !q_selection.is_empty() { - // TODO: Use an app state or something so we only have to set this up once - let scale_factor = q_window.single().scale_factor() as f32; - config.line_width = 4. * scale_factor; - config.line_perspective = false; - config.depth_bias = -1.; + let cam_xform = q_camera_xform.single(); - let camera_xform = q_camera_xform.single(); + // TODO (maybe): + // 1. Map GizmoElements to a new Vec<(Shape, Color)> + // 2. Bisect the rotator rings and push the back sides into the vec + // 3. Sort by camera depth + // 4. Draw + // + // (Starting to feel like a really gross hack -- wish I had a better + // grasp of Bevy's render pipeline so we didn't have to lean so hard on + // the Gizmos API) - // TODO: We only want to render one gizmo -- this is just for testing - for world_xform in q_selection.iter() { - let xform = world_xform.compute_transform(); - // Note: Scaling the gizmo size like this isn't perfect due to - // distortion from the perspective projection. It bugged me at - // first, but even Blender exhibits this type of gizmo distortion if - // you dial the camera's focal length way down, so I'm gonna call it - // good enough. - let dist = Vec3::distance(camera_xform.translation, xform.translation); - let size = dist / 20.; + for gizmo in q_gizmo.iter() { + for el in gizmo.0.iter() { + match el.shape { + Shape::Ring(ring) => { + let GizmoColor::Single(color) = el.color else { + unreachable!(); + }; - let variant = if mode - .transform - .contains(InteractionTransform::TRANSLATE | InteractionTransform::ROTATE) - { - GizmoVariant::Partial - } else { - GizmoVariant::Full - }; + if let Control::Rotation(Axes::X | Axes::Y | Axes::Z) = el.control { + let (front, _back) = bisect_rotator_ring(&ring, cam_xform); + gizmos.arc(front.center, front.axis, front.start, front.length, color); + // TODO + // gizmos.arc( + // back.center, + // back.axis, + // back.start, + // back.length, + // color.with_a(0.2), + // ); + } else { + gizmos.circle(ring.origin, ring.normal, ring.radius, color); + } + } + Shape::Line(line) => { + // TODO: Camera-direction fade + let start = line.origin; + let end = line.origin + line.direction * line.length; - if mode.transform.contains(InteractionTransform::TRANSLATE) { - draw_translate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); - } - if mode.transform.contains(InteractionTransform::ROTATE) { - draw_rotate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); + match el.color { + GizmoColor::Single(color) => { + gizmos.line(start, end, color); + } + GizmoColor::Gradient(color_a, color_b) => { + gizmos.line_gradient(start, end, color_a, color_b); + } + } + } + Shape::Cone(cone) => { + // TODO: Camera-direction fade + let GizmoColor::Single(color) = el.color else { + unreachable!() + }; + gizmos.cone(cone.base, cone.direction, cone.radius, cone.length, color); + } } } } } -// FIXME: We might need more control over the Z-ordering / depth bias than the -// Bevy gizmos give us. As best as I can tell, the elements are drawn in -// reverse invocation order (the methods called last are rendered first, -// like the commands are being popped from a LIFO stack), but it's not -// 100% consistent and there seems to be lots of flickering / Z-fighting. -// Might not be a big deal, so I'm inclined to roll with it for now and -// see how users respond. +fn bisect_rotator_ring(ring: &Ring, cam_xform: &Transform) -> (Arc, Arc) { + let plane_intersection = Vec3::cross(ring.normal, cam_xform.forward()).normalize(); + let start_a = ring.origin + plane_intersection * ring.radius; + let start_b = ring.origin - plane_intersection * ring.radius; -#[inline] -fn draw_translate_gizmo( - gizmos: &mut Gizmos, - variant: GizmoVariant, - xform: &Transform, - camera_xform: &Transform, - space: TransformSpace, - size: f32, -) { - let origin = xform.translation; - let basis = match space { - TransformSpace::Local => xform, - TransformSpace::World => &Transform::IDENTITY, + let arc_a = Arc { + center: ring.origin, + axis: ring.normal, + start: start_a, + length: consts::PI, }; - for (direction, mut color) in [ - (basis.local_x(), design_system::Color::RED), - (basis.local_y(), design_system::Color::GREEN), - (basis.local_z(), design_system::Color::BLUE), - ] { - // Fade the arrow out when it's pointed directly at the camera. - // (Prevents overlapping/obscuring the center screen-space ring, or just - // generally getting in the way when it's not very useful to grab onto.) - let alpha = (1. - Vec3::dot(camera_xform.forward(), direction).abs()) - .remap((0., 0.1), (0., 1.)) - .clamp(0., 1.); + let mut arc_b = arc_a; + arc_b.start = start_b; - color = color.with_a(alpha); - - // In "Partial" mode, we only draw the center ring and the arrow tips to - // leave room for the rotator rings. - if matches!(variant, GizmoVariant::Full) { - // Draw a line from just outside the center ring to the tip of the arrow - let start = origin + direction * size * 0.25; - let end = origin + direction * size * 1.6; - gizmos.line(start, end, color); - - // Draw a line from the midpoint of this arrow to the midpoint of - // the next one. This "crossbar" can be used to translate the object - // on the plane defined by the corresponding axes. - let midpoint_self = origin + ((end - origin) / 2.); - let (ortho, mut color_ortho) = get_ortho(basis, direction); - - // Fade the crossbar when its plane is edge-on with the camera, just - // like we did for the arrow pointing toward/away from the camera. - let ortho_alpha = (1. - Vec3::dot(camera_xform.forward(), ortho).abs()) - .remap((0., 0.1), (0., 1.)) - .clamp(0., 1.); - - color_ortho = color_ortho.with_a(f32::min(ortho_alpha, alpha)); - - let ortho_end = origin + ortho * size * 1.6; - let midpoint_ortho = origin + ((ortho_end - origin) / 2.); - gizmos.line_gradient(midpoint_self, midpoint_ortho, color, color_ortho); - } - - // Draw the arrow tip - let factor = match variant { - GizmoVariant::Full => 1.2, - GizmoVariant::Partial => 1.4, - }; - let cone_base = origin + direction * size * factor; - - let radius = size * 0.1; - let length = size * 0.4; - gizmos.cone(cone_base, direction, radius, length, color); - } - - // Draw the center ring. This can be dragged around to translate the object - // in screen-space (i.e. on the camera's XY plane). - gizmos.circle(origin, camera_xform.forward(), size * 0.2, Color::WHITE); + (arc_a, arc_b) } -// Dumb helper function for grabbing the normal vector/color for the translation -// gizmo's next arrow. We use this to determine where and how to render the -// planar translation "crossbars". -#[inline] -fn get_ortho(xform: &Transform, compare: Vec3) -> (Vec3, Color) { - let x = xform.local_x(); - let y = xform.local_y(); - let z = xform.local_z(); +// pub(super) fn draw_old( +// q_selection: Query<&GlobalTransform, With>, +// q_camera_xform: Query<&Transform, With>, +// q_window: Query<&Window, With>, +// mode: Res, +// mut gizmos: Gizmos, +// mut config: ResMut, +// ) { +// if !q_selection.is_empty() { +// // TODO: Use an app state or something so we only have to set this up once +// let scale_factor = q_window.single().scale_factor() as f32; +// config.line_width = 4. * scale_factor; +// config.line_perspective = false; +// config.depth_bias = -1.; - let diff_x = (compare - x).length_squared(); - let diff_y = (compare - y).length_squared(); - let diff_z = (compare - z).length_squared(); +// let camera_xform = q_camera_xform.single(); - if diff_x < diff_y && diff_x < diff_z { - (y, design_system::Color::GREEN) - } else if diff_y < diff_x && diff_y < diff_z { - (z, design_system::Color::BLUE) - } else { - (x, design_system::Color::RED) - } -} +// // TODO: We only want to render one gizmo -- this is just for testing +// for world_xform in q_selection.iter() { +// let xform = world_xform.compute_transform(); +// // Note: Scaling the gizmo size like this isn't perfect due to +// // distortion from the perspective projection. It bugged me at +// // first, but even Blender exhibits this type of gizmo distortion if +// // you dial the camera's focal length way down, so I'm gonna call it +// // good enough. +// let dist = Vec3::distance(camera_xform.translation, xform.translation); +// let size = dist / 20.; -#[inline] -fn draw_rotate_gizmo( - gizmos: &mut Gizmos, - variant: GizmoVariant, - xform: &Transform, - camera_xform: &Transform, - space: TransformSpace, - radius: f32, -) { - let origin = xform.translation; - let basis = match space { - TransformSpace::Local => xform, - TransformSpace::World => &Transform::IDENTITY, - }; - let camera_fwd = camera_xform.forward(); +// let variant = if mode +// .transform +// .contains(TransformType::TRANSLATE | TransformType::ROTATE) +// { +// GizmoVariant::Partial +// } else { +// GizmoVariant::Full +// }; - // Draw a ring around the whole gizmo for rotating the object in screen - // space (i.e. around the camera's forward vector). - gizmos.circle(origin, camera_fwd, radius * 1.2, Color::WHITE); +// if mode.transform.contains(TransformType::TRANSLATE) { +// draw_translate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); +// } +// if mode.transform.contains(TransformType::ROTATE) { +// draw_rotate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); +// } +// } +// } +// } - // Draw semicircles for the main basis vector axes, bisected by the camera's - // XY plane. The `RotatorRings` argument determines which side of the plane - // to draw on. - draw_rotator_axis_rings( - gizmos, - RotatorRings::Near, - origin, - camera_fwd, - basis, - radius, - ); +// // FIXME: We might need more control over the Z-ordering / depth bias than the +// // Bevy gizmos give us. As best as I can tell, the elements are drawn in +// // reverse invocation order (the methods called last are rendered first, +// // like the commands are being popped from a LIFO stack), but it's not +// // 100% consistent and there seems to be lots of flickering / Z-fighting. +// // Might not be a big deal, so I'm inclined to roll with it for now and +// // see how users respond. - // We skip drawing the far-side of the rings in "Partial" mode to cut down - // on visual noise and leave less room for mis-clicks. - if matches!(variant, GizmoVariant::Full) { - draw_rotator_axis_rings(gizmos, RotatorRings::Far, origin, camera_fwd, basis, radius); - } -} +// #[inline] +// fn draw_translate_gizmo( +// gizmos: &mut Gizmos, +// variant: GizmoVariant, +// xform: &Transform, +// camera_xform: &Transform, +// space: TransformSpace, +// size: f32, +// ) { +// let origin = xform.translation; +// let basis = match space { +// TransformSpace::Local => xform, +// TransformSpace::World => &Transform::IDENTITY, +// }; -enum RotatorRings { - Near, - Far, -} +// for (direction, mut color) in [ +// (basis.local_x(), design_system::Color::RED), +// (basis.local_y(), design_system::Color::GREEN), +// (basis.local_z(), design_system::Color::BLUE), +// ] { +// // Fade the arrow out when it's pointed directly at the camera. +// // (Prevents overlapping/obscuring the center screen-space ring, or just +// // generally getting in the way when it's not very useful to grab onto.) +// let alpha = (1. - Vec3::dot(camera_xform.forward(), direction).abs()) +// .remap((0., 0.1), (0., 1.)) +// .clamp(0., 1.); -#[inline] -fn draw_rotator_axis_rings( - gizmos: &mut Gizmos, - which: RotatorRings, - origin: Vec3, - camera_fwd: Vec3, - basis: &Transform, - radius: f32, -) { - for (normal, color) in [ - (basis.local_x(), design_system::Color::RED), - (basis.local_y(), design_system::Color::GREEN), - (basis.local_z(), design_system::Color::BLUE), - ] { - // The line of intersection between two planes will be mutually - // orthogonal to the normal vectors of those planes, so we can find the - // points of intersection between the ring and the camera plane by - // taking the cross product of the camera direction and the rotation - // axis, and scaling it by the ring's radius. The order of the operands - // determines which of the intersection points we get. - let (color, perp) = match which { - RotatorRings::Near => (color, Vec3::cross(normal, camera_fwd)), - RotatorRings::Far => (color.with_a(0.2), Vec3::cross(camera_fwd, normal)), - }; +// color = color.with_a(alpha); - let start = origin + perp.normalize() * radius; +// // In "Partial" mode, we only draw the center ring and the arrow tips to +// // leave room for the rotator rings. +// if matches!(variant, GizmoVariant::Full) { +// // Draw a line from just outside the center ring to the tip of the arrow +// let start = origin + direction * size * 0.25; +// let end = origin + direction * size * 1.6; +// gizmos.line(start, end, color); - gizmos.arc(origin, normal, start, consts::PI, color); - } -} +// // Draw a line from the midpoint of this arrow to the midpoint of +// // the next one. This "crossbar" can be used to translate the object +// // on the plane defined by the corresponding axes. +// let midpoint_self = origin + ((end - origin) / 2.); +// let (ortho, mut color_ortho) = get_ortho(basis, direction); + +// // Fade the crossbar when its plane is edge-on with the camera, just +// // like we did for the arrow pointing toward/away from the camera. +// let ortho_alpha = (1. - Vec3::dot(camera_xform.forward(), ortho).abs()) +// .remap((0., 0.1), (0., 1.)) +// .clamp(0., 1.); + +// color_ortho = color_ortho.with_a(f32::min(ortho_alpha, alpha)); + +// let ortho_end = origin + ortho * size * 1.6; +// let midpoint_ortho = origin + ((ortho_end - origin) / 2.); +// gizmos.line_gradient(midpoint_self, midpoint_ortho, color, color_ortho); +// } + +// // Draw the arrow tip +// let factor = match variant { +// GizmoVariant::Full => 1.2, +// GizmoVariant::Partial => 1.4, +// }; +// let cone_base = origin + direction * size * factor; + +// let radius = size * 0.1; +// let length = size * 0.4; +// gizmos.cone(cone_base, direction, radius, length, color); +// } + +// // Draw the center ring. This can be dragged around to translate the object +// // in screen-space (i.e. on the camera's XY plane). +// gizmos.circle(origin, camera_xform.forward(), size * 0.2, Color::WHITE); +// } + +// // Dumb helper function for grabbing the normal vector/color for the translation +// // gizmo's next arrow. We use this to determine where and how to render the +// // planar translation "crossbars". +// #[inline] +// fn get_ortho(xform: &Transform, compare: Vec3) -> (Vec3, Color) { +// let x = xform.local_x(); +// let y = xform.local_y(); +// let z = xform.local_z(); + +// let diff_x = (compare - x).length_squared(); +// let diff_y = (compare - y).length_squared(); +// let diff_z = (compare - z).length_squared(); + +// if diff_x < diff_y && diff_x < diff_z { +// (y, design_system::Color::GREEN) +// } else if diff_y < diff_x && diff_y < diff_z { +// (z, design_system::Color::BLUE) +// } else { +// (x, design_system::Color::RED) +// } +// } + +// #[inline] +// fn draw_rotate_gizmo( +// gizmos: &mut Gizmos, +// variant: GizmoVariant, +// xform: &Transform, +// camera_xform: &Transform, +// space: TransformSpace, +// radius: f32, +// ) { +// let origin = xform.translation; +// let basis = match space { +// TransformSpace::Local => xform, +// TransformSpace::World => &Transform::IDENTITY, +// }; +// let camera_fwd = camera_xform.forward(); + +// // Draw a ring around the whole gizmo for rotating the object in screen +// // space (i.e. around the camera's forward vector). +// gizmos.circle(origin, camera_fwd, radius * 1.2, Color::WHITE); + +// // Draw semicircles for the main basis vector axes, bisected by the camera's +// // XY plane. The `RotatorRings` argument determines which side of the plane +// // to draw on. +// draw_rotator_axis_rings( +// gizmos, +// RotatorRings::Near, +// origin, +// camera_fwd, +// basis, +// radius, +// ); + +// // We skip drawing the far-side of the rings in "Partial" mode to cut down +// // on visual noise and leave less room for mis-clicks. +// if matches!(variant, GizmoVariant::Full) { +// draw_rotator_axis_rings(gizmos, RotatorRings::Far, origin, camera_fwd, basis, radius); +// } +// } + +// enum RotatorRings { +// Near, +// Far, +// } + +// #[inline] +// fn draw_rotator_axis_rings( +// gizmos: &mut Gizmos, +// which: RotatorRings, +// origin: Vec3, +// camera_fwd: Vec3, +// basis: &Transform, +// radius: f32, +// ) { +// for (normal, color) in [ +// (basis.local_x(), design_system::Color::RED), +// (basis.local_y(), design_system::Color::GREEN), +// (basis.local_z(), design_system::Color::BLUE), +// ] { +// // The line of intersection between two planes will be mutually +// // orthogonal to the normal vectors of those planes, so we can find the +// // points of intersection between the ring and the camera plane by +// // taking the cross product of the camera direction and the rotation +// // axis, and scaling it by the ring's radius. The order of the operands +// // determines which of the intersection points we get. +// let (color, perp) = match which { +// RotatorRings::Near => (color, Vec3::cross(normal, camera_fwd)), +// RotatorRings::Far => (color.with_a(0.2), Vec3::cross(camera_fwd, normal)), +// }; + +// let start = origin + perp.normalize() * radius; + +// gizmos.arc(origin, normal, start, consts::PI, color); +// } +// } trait GizmoExtras { /// Draw an arc around `center` with angular `length` (in radians), diff --git a/studio/src/interaction/mod.rs b/studio/src/interaction/mod.rs index ca53f82..4d8a85c 100644 --- a/studio/src/interaction/mod.rs +++ b/studio/src/interaction/mod.rs @@ -46,28 +46,29 @@ pub struct SelectionMode(SelectionChannel); #[derive(Resource)] pub struct InteractionMode { - pub transform: InteractionTransform, + pub transform: TransformType, pub space: TransformSpace, } impl Default for InteractionMode { fn default() -> Self { Self { - transform: InteractionTransform::TRANSLATE | InteractionTransform::ROTATE, + transform: TransformType::TRANSLATE | TransformType::ROTATE, space: default(), } } } #[repr(transparent)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[wasm_bindgen] -pub struct InteractionTransform(u8); +pub struct TransformType(u8); bitflags! { - impl InteractionTransform: u8 { - const TRANSLATE = 0b0001; - const ROTATE = 0b0010; + impl TransformType: u8 { + const TRANSLATE = 0b0001; + const ROTATE = 0b0010; + const TRANSLATE_ROTATE = 0b0011; } } @@ -91,13 +92,10 @@ pub mod wasm { use crate::wasm::{self, WasmMessageBuffer}; - use super::{ - InteractionMode, InteractionTransform, SelectionChannel, SelectionMode, TransformSpace, - }; + use super::{InteractionMode, SelectionChannel, SelectionMode, TransformSpace, TransformType}; static SELECT_MODE_BUF: WasmMessageBuffer = WasmMessageBuffer::new(); - static INTERACTION_TRANSFORM_BUF: WasmMessageBuffer = - WasmMessageBuffer::new(); + static INTERACTION_TRANSFORM_BUF: WasmMessageBuffer = WasmMessageBuffer::new(); static INTERACTION_SPACE_BUF: WasmMessageBuffer = WasmMessageBuffer::new(); #[wasm_bindgen(js_name = setSelectionMode)] @@ -107,7 +105,7 @@ pub mod wasm { #[wasm_bindgen(js_name = setInteractionTransform)] pub fn set_interaction_transform(flags: u8) { - wasm::write_message!(INTERACTION_TRANSFORM_BUF, InteractionTransform(flags)); + wasm::write_message!(INTERACTION_TRANSFORM_BUF, TransformType(flags)); } #[wasm_bindgen(js_name = setTransformSpace)]