use crate::ecs::{EditorWorld, GrabAxis, GrabConstraint, GrabEntry, GrabSession};
use crate::systems::input;
use nightshade::ecs::input::resources::MouseState;
use nightshade::ecs::transform::commands::mark_local_transform_dirty;
use nightshade::ecs::window::resources::ViewportRect;
use nightshade::prelude::*;
use nightshade::render::wgpu::passes::geometry::{UiLayer, UiRect};
const AXIS_LINE_LENGTH_PX: f32 = 4000.0;
const AXIS_LINE_THICKNESS_PX: f32 = 1.5;
const GRAB_Z_INDEX: i32 = 6_000;
const X_COLOR: Vec4 = Vec4::new(0.95, 0.30, 0.30, 1.0);
const Y_COLOR: Vec4 = Vec4::new(0.40, 0.95, 0.30, 1.0);
const Z_COLOR: Vec4 = Vec4::new(0.30, 0.55, 0.95, 1.0);
const FREE_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 0.45);
pub fn tick(editor_world: &mut EditorWorld, world: &mut World) {
let keyboard = &world.resources.input.keyboard;
let mouse = *nightshade::ecs::input::access::mouse_for_active(world);
let g_pressed = keyboard.is_key_pressed(KeyCode::KeyG);
let x_pressed = keyboard.is_key_pressed(KeyCode::KeyX);
let y_pressed = keyboard.is_key_pressed(KeyCode::KeyY);
let z_pressed = keyboard.is_key_pressed(KeyCode::KeyZ);
let enter_pressed =
keyboard.is_key_pressed(KeyCode::Enter) || keyboard.is_key_pressed(KeyCode::NumpadEnter);
let escape_pressed = keyboard.is_key_pressed(KeyCode::Escape);
let shift_held =
keyboard.is_key_pressed(KeyCode::ShiftLeft) || keyboard.is_key_pressed(KeyCode::ShiftRight);
let ctrl_held = keyboard.is_key_pressed(KeyCode::ControlLeft)
|| keyboard.is_key_pressed(KeyCode::ControlRight);
let left_mouse_pressed = mouse.state.contains(MouseState::LEFT_JUST_PRESSED);
let right_mouse_pressed = mouse.state.contains(MouseState::RIGHT_JUST_PRESSED);
let g_edge =
g_pressed && !editor_world.resources.grab.g_was_pressed && !ctrl_held && !shift_held;
let x_edge = x_pressed && !editor_world.resources.grab.x_was_pressed;
let y_edge = y_pressed && !editor_world.resources.grab.y_was_pressed;
let z_edge = z_pressed && !editor_world.resources.grab.z_was_pressed;
let enter_edge = enter_pressed && !editor_world.resources.grab.enter_was_pressed;
let escape_edge = escape_pressed && !editor_world.resources.grab.escape_was_pressed;
let left_click_edge = left_mouse_pressed;
let right_click_edge = right_mouse_pressed;
editor_world.resources.grab.g_was_pressed = g_pressed;
editor_world.resources.grab.x_was_pressed = x_pressed;
editor_world.resources.grab.y_was_pressed = y_pressed;
editor_world.resources.grab.z_was_pressed = z_pressed;
editor_world.resources.grab.enter_was_pressed = enter_pressed;
editor_world.resources.grab.escape_was_pressed = escape_pressed;
editor_world.resources.grab.left_mouse_was_pressed = left_mouse_pressed;
editor_world.resources.grab.right_mouse_was_pressed = right_mouse_pressed;
if editor_world.resources.grab.session.is_none() {
if !g_edge {
return;
}
if input::ui_capturing(world) {
return;
}
if !input::mouse_in_active_viewport(world) {
return;
}
try_start_session(editor_world, world, mouse.position);
return;
}
if escape_edge || right_click_edge {
cancel_session(editor_world, world);
return;
}
if x_edge {
toggle_constraint(editor_world, GrabAxis::X, shift_held);
rebase_session_origin(editor_world, world, mouse.position);
}
if y_edge {
toggle_constraint(editor_world, GrabAxis::Y, shift_held);
rebase_session_origin(editor_world, world, mouse.position);
}
if z_edge {
toggle_constraint(editor_world, GrabAxis::Z, shift_held);
rebase_session_origin(editor_world, world, mouse.position);
}
apply_grab_movement(editor_world, world, mouse.position);
if enter_edge || left_click_edge {
commit_session(editor_world, world);
return;
}
draw_constraint_overlay(editor_world, world);
}
pub fn is_active(editor_world: &EditorWorld) -> bool {
editor_world.resources.grab.session.is_some()
}
fn try_start_session(editor_world: &mut EditorWorld, world: &World, mouse_position: Vec2) {
if editor_world.resources.ui.selected_entities.is_empty() {
return;
}
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let Some(primary) = editor_world.resources.ui.selected_entity else {
return;
};
let Some(primary_transform) = world.core.get_global_transform(primary) else {
return;
};
let primary_world_origin = primary_transform.translation();
let Some((view_projection, viewport, camera_position)) = camera_view(world, camera_entity)
else {
return;
};
let inverse_view_projection = view_projection
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
let camera_transform = match world.core.get_global_transform(camera_entity) {
Some(transform) => transform,
None => return,
};
let camera_forward = camera_transform.forward_vector();
let plane_normal_world = camera_forward.normalize();
let Some(initial_world_hit) = mouse_ray_plane_hit(
mouse_position,
&viewport,
&inverse_view_projection,
camera_position,
primary_world_origin,
plane_normal_world,
) else {
return;
};
let mut entries: Vec<GrabEntry> = Vec::new();
for entity in editor_world.resources.ui.selected_entities.clone() {
let Some(local_transform) = world.core.get_local_transform(entity) else {
continue;
};
let Some(global_transform) = world.core.get_global_transform(entity) else {
continue;
};
entries.push(GrabEntry {
entity,
original_local_translation: local_transform.translation,
reference_world_translation: global_transform.translation(),
});
}
if entries.is_empty() {
return;
}
editor_world.resources.grab.session = Some(GrabSession {
entries,
primary_world_origin,
initial_world_hit,
plane_normal_world,
camera_entity,
constraint: GrabConstraint::Free,
});
}
fn toggle_constraint(editor_world: &mut EditorWorld, axis: GrabAxis, shift_held: bool) {
let Some(session) = editor_world.resources.grab.session.as_mut() else {
return;
};
let next = if shift_held {
GrabConstraint::Plane(axis)
} else {
GrabConstraint::Axis(axis)
};
session.constraint = if session.constraint == next {
GrabConstraint::Free
} else {
next
};
}
fn apply_grab_movement(editor_world: &mut EditorWorld, world: &mut World, mouse_position: Vec2) {
let session_clone = match editor_world.resources.grab.session.as_ref() {
Some(session) => session.clone(),
None => return,
};
let Some(current_hit) = current_mouse_hit(world, &session_clone, mouse_position) else {
return;
};
let raw_delta = current_hit - session_clone.initial_world_hit;
let constrained_delta = constrain_delta(raw_delta, session_clone.constraint);
for entry in &session_clone.entries {
let new_world_translation = entry.reference_world_translation + constrained_delta;
let parent_inverse = world
.core
.get_parent(entry.entity)
.and_then(|parent| parent.0)
.and_then(|parent_entity| world.core.get_global_transform(parent_entity).copied())
.map(|parent_global| {
parent_global
.0
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity)
});
let new_local_translation = match parent_inverse {
Some(inverse) => {
let homogeneous = inverse
* nalgebra_glm::Vec4::new(
new_world_translation.x,
new_world_translation.y,
new_world_translation.z,
1.0,
);
Vec3::new(homogeneous.x, homogeneous.y, homogeneous.z)
}
None => new_world_translation,
};
if let Some(transform) = world.core.get_local_transform_mut(entry.entity) {
transform.translation = new_local_translation;
}
mark_local_transform_dirty(world, entry.entity);
}
}
fn constrain_delta(delta: Vec3, constraint: GrabConstraint) -> Vec3 {
match constraint {
GrabConstraint::Free => delta,
GrabConstraint::Axis(axis) => {
let unit = axis.unit();
unit * nalgebra_glm::dot(&delta, &unit)
}
GrabConstraint::Plane(axis) => {
let unit = axis.unit();
delta - unit * nalgebra_glm::dot(&delta, &unit)
}
}
}
fn current_mouse_hit(world: &World, session: &GrabSession, mouse_position: Vec2) -> Option<Vec3> {
let (view_projection, viewport, camera_position) = camera_view(world, session.camera_entity)?;
let inverse_view_projection = view_projection
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
mouse_ray_plane_hit(
mouse_position,
&viewport,
&inverse_view_projection,
camera_position,
session.primary_world_origin,
session.plane_normal_world,
)
}
fn rebase_session_origin(editor_world: &mut EditorWorld, world: &World, mouse_position: Vec2) {
let session_snapshot = match editor_world.resources.grab.session.as_ref() {
Some(session) => session.clone(),
None => return,
};
let Some(new_hit) = current_mouse_hit(world, &session_snapshot, mouse_position) else {
return;
};
let Some(active) = editor_world.resources.grab.session.as_mut() else {
return;
};
for entry in active.entries.iter_mut() {
if let Some(global) = world.core.get_global_transform(entry.entity) {
entry.reference_world_translation = global.translation();
}
}
if let Some(last_entry) = active.entries.last()
&& let Some(global) = world.core.get_global_transform(last_entry.entity)
{
active.primary_world_origin = global.translation();
}
active.initial_world_hit = new_hit;
}
fn camera_view(
world: &World,
camera_entity: Entity,
) -> Option<(nalgebra_glm::Mat4, ViewportRect, Vec3)> {
let camera = world.core.get_camera(camera_entity).copied()?;
let camera_transform = world.core.get_global_transform(camera_entity)?;
let view_matrix = camera_transform
.0
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
let viewport = world
.resources
.window
.camera_tile_rects
.get(&camera_entity)
.copied()
.or(world.resources.window.active_viewport_rect)
.filter(|rect| rect.width > 0.0 && rect.height > 0.0)?;
let aspect = viewport.width / viewport.height;
let projection_matrix = camera.projection.matrix_with_aspect(aspect);
let view_projection = projection_matrix * view_matrix;
Some((view_projection, viewport, camera_transform.translation()))
}
fn mouse_ray_plane_hit(
mouse_position: Vec2,
viewport: &ViewportRect,
inverse_view_projection: &nalgebra_glm::Mat4,
camera_position: Vec3,
plane_origin: Vec3,
plane_normal: Vec3,
) -> Option<Vec3> {
let ndc_x = ((mouse_position.x - viewport.x) / viewport.width) * 2.0 - 1.0;
let ndc_y = 1.0 - ((mouse_position.y - viewport.y) / viewport.height) * 2.0;
let near_clip = inverse_view_projection * nalgebra_glm::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
if near_clip.w.abs() < 1e-6 {
return None;
}
let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
let direction = near_world - camera_position;
let direction_magnitude = direction.magnitude();
if direction_magnitude < 1e-6 {
return None;
}
let direction_unit = direction / direction_magnitude;
let denominator = nalgebra_glm::dot(&direction_unit, &plane_normal);
if denominator.abs() < 1e-4 {
return None;
}
let parameter =
nalgebra_glm::dot(&(plane_origin - camera_position), &plane_normal) / denominator;
if parameter < 0.0 {
return None;
}
Some(camera_position + direction_unit * parameter)
}
fn cancel_session(editor_world: &mut EditorWorld, world: &mut World) {
let Some(session) = editor_world.resources.grab.session.take() else {
return;
};
for entry in &session.entries {
if let Some(transform) = world.core.get_local_transform_mut(entry.entity) {
transform.translation = entry.original_local_translation;
}
mark_local_transform_dirty(world, entry.entity);
}
}
fn commit_session(editor_world: &mut EditorWorld, world: &mut World) {
let Some(session) = editor_world.resources.grab.session.take() else {
return;
};
let mut steps: Vec<crate::undo::UndoableOperation> = Vec::new();
for entry in &session.entries {
let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entry.entity) else {
continue;
};
let Some(end) = world.core.get_local_transform(entry.entity).copied() else {
continue;
};
let old = LocalTransform {
translation: entry.original_local_translation,
rotation: end.rotation,
scale: end.scale,
};
if old.translation == end.translation {
continue;
}
steps.push(crate::undo::UndoableOperation::TransformChanged {
uuid,
old,
new: end,
});
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entry.entity,
);
}
if steps.is_empty() {
return;
}
if steps.len() == 1 {
let step = steps.into_iter().next().unwrap();
editor_world.resources.undo.push(step, "Grab");
} else {
editor_world
.resources
.undo
.push(crate::undo::UndoableOperation::Batch { steps }, "Grab");
}
editor_world.resources.ui_handles.inspector.dirty = true;
}
fn draw_constraint_overlay(editor_world: &EditorWorld, world: &mut World) {
let Some(session) = editor_world.resources.grab.session.as_ref() else {
return;
};
let Some((view_projection, viewport, _)) = camera_view(world, session.camera_entity) else {
return;
};
let Some(origin_screen) = project(&view_projection, &viewport, session.primary_world_origin)
else {
return;
};
let clip = Rect::new(viewport.x, viewport.y, viewport.width, viewport.height);
match session.constraint {
GrabConstraint::Free => {
push_marker(world, origin_screen, FREE_COLOR, Some(clip));
}
GrabConstraint::Axis(axis) => {
let color = axis_color(axis);
let direction = projected_axis_direction(
&view_projection,
&viewport,
session.primary_world_origin,
axis.unit(),
);
push_infinite_line(world, origin_screen, direction, color, Some(clip));
push_marker(world, origin_screen, color, Some(clip));
}
GrabConstraint::Plane(axis) => {
let other = perpendicular_axes(axis);
for (other_axis, _) in &other {
let color = axis_color(*other_axis);
let direction = projected_axis_direction(
&view_projection,
&viewport,
session.primary_world_origin,
other_axis.unit(),
);
push_infinite_line(world, origin_screen, direction, color, Some(clip));
}
push_marker(world, origin_screen, axis_color(axis), Some(clip));
}
}
}
fn perpendicular_axes(locked: GrabAxis) -> [(GrabAxis, Vec3); 2] {
match locked {
GrabAxis::X => [
(GrabAxis::Y, Vec3::new(0.0, 1.0, 0.0)),
(GrabAxis::Z, Vec3::new(0.0, 0.0, 1.0)),
],
GrabAxis::Y => [
(GrabAxis::X, Vec3::new(1.0, 0.0, 0.0)),
(GrabAxis::Z, Vec3::new(0.0, 0.0, 1.0)),
],
GrabAxis::Z => [
(GrabAxis::X, Vec3::new(1.0, 0.0, 0.0)),
(GrabAxis::Y, Vec3::new(0.0, 1.0, 0.0)),
],
}
}
fn axis_color(axis: GrabAxis) -> Vec4 {
match axis {
GrabAxis::X => X_COLOR,
GrabAxis::Y => Y_COLOR,
GrabAxis::Z => Z_COLOR,
}
}
fn projected_axis_direction(
view_projection: &nalgebra_glm::Mat4,
viewport: &ViewportRect,
origin_world: Vec3,
axis_world: Vec3,
) -> Vec2 {
let Some(origin_screen) = project(view_projection, viewport, origin_world) else {
return Vec2::new(1.0, 0.0);
};
let Some(tip_screen) = project(view_projection, viewport, origin_world + axis_world) else {
return Vec2::new(1.0, 0.0);
};
let delta = tip_screen - origin_screen;
let magnitude = delta.magnitude();
if magnitude < 1e-4 {
Vec2::new(1.0, 0.0)
} else {
delta / magnitude
}
}
fn project(
view_projection: &nalgebra_glm::Mat4,
viewport: &ViewportRect,
world_position: Vec3,
) -> Option<Vec2> {
let clip = view_projection
* nalgebra_glm::Vec4::new(world_position.x, world_position.y, world_position.z, 1.0);
if clip.w <= 0.0 {
return None;
}
let ndc_x = clip.x / clip.w;
let ndc_y = clip.y / clip.w;
let screen_x = viewport.x + (ndc_x + 1.0) * 0.5 * viewport.width;
let screen_y = viewport.y + (1.0 - ndc_y) * 0.5 * viewport.height;
Some(Vec2::new(screen_x, screen_y))
}
fn push_infinite_line(
world: &mut World,
origin: Vec2,
direction: Vec2,
color: Vec4,
clip: Option<Rect>,
) {
let from = origin - direction * AXIS_LINE_LENGTH_PX;
let to = origin + direction * AXIS_LINE_LENGTH_PX;
push_segment(world, from, to, color, AXIS_LINE_THICKNESS_PX, clip);
}
fn push_segment(
world: &mut World,
from: Vec2,
to: Vec2,
color: Vec4,
thickness: f32,
clip: Option<Rect>,
) {
let delta = to - from;
let length = delta.magnitude();
if length < 0.001 {
return;
}
let midpoint = (from + to) * 0.5;
let angle = delta.y.atan2(delta.x);
let position = midpoint - Vec2::new(length * 0.5, thickness * 0.5);
world.resources.retained_ui.frame.rects.push(UiRect {
position,
size: Vec2::new(length, thickness),
color,
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation: angle,
clip_rect: clip,
layer: UiLayer::Background,
z_index: GRAB_Z_INDEX,
shadow: None,
effect_kind: 0,
effect_params: [0.0; 4],
quad_corners: None,
});
}
fn push_marker(world: &mut World, origin: Vec2, color: Vec4, clip: Option<Rect>) {
let size = 8.0;
let half = size * 0.5;
world.resources.retained_ui.frame.rects.push(UiRect {
position: origin - Vec2::new(half, half),
size: Vec2::new(size, size),
color,
corner_radius: half,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation: 0.0,
clip_rect: clip,
layer: UiLayer::Background,
z_index: GRAB_Z_INDEX + 1,
shadow: None,
effect_kind: 0,
effect_params: [0.0; 4],
quad_corners: None,
});
}