use super::overlays_enabled::active_camera_overlays_enabled;
use super::state::{
GizmoMode, GizmoPlanarScaleDrag, GizmoPlanarTranslationDrag, GizmoRotationDrag, GizmoScaleDrag,
GizmoTranslationDrag,
};
use crate::ecs::input::resources::MouseState;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::window::resources::ViewportRect;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::{UiLayer, UiRect};
use nalgebra_glm::{Mat4, Vec2, Vec3, Vec4};
const ORIGIN_HANDLE_PX: f32 = 8.0;
const ORIGIN_HIT_THRESHOLD_PX: f32 = ORIGIN_HANDLE_PX * 0.7;
const Z_INDEX: i32 = 5_000;
const TRIANGLE_EFFECT_KIND: u32 = 8;
const SCALE_DRAG_SENSITIVITY: f32 = 1.0;
const MIN_SCALE_COMPONENT: f32 = 0.001;
const PLANE_HANDLE_INNER_FRACTION: f32 = 0.30;
const PLANE_HANDLE_OUTER_FRACTION: f32 = 0.55;
const ROTATION_RING_SEGMENTS: usize = 128;
const ROTATION_HIT_THRESHOLD_PX: f32 = 8.0;
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 HOVER_COLOR: Vec4 = Vec4::new(1.0, 0.85, 0.20, 1.0);
const YZ_PLANE_COLOR: Vec4 = Vec4::new(0.85, 0.30, 0.30, 0.45);
const XZ_PLANE_COLOR: Vec4 = Vec4::new(0.40, 0.85, 0.30, 0.45);
const XY_PLANE_COLOR: Vec4 = Vec4::new(0.30, 0.55, 0.85, 0.45);
const PLANE_HOVER_COLOR: Vec4 = Vec4::new(1.0, 0.85, 0.20, 0.65);
const AXIS_COLORS: [Vec4; 3] = [X_COLOR, Y_COLOR, Z_COLOR];
const PLANE_COLORS: [Vec4; 3] = [YZ_PLANE_COLOR, XZ_PLANE_COLOR, XY_PLANE_COLOR];
#[derive(Clone, Copy, PartialEq, Eq)]
enum DrawHead {
Triangle,
Box,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ActiveMode {
LocalTranslation,
GlobalTranslation,
Scale,
Rotation,
CompositeLocal,
CompositeGlobal,
}
#[derive(Clone, Copy)]
struct GizmoSizing {
axis_screen_length_px: f32,
shaft_thickness_px: f32,
shaft_hover_thickness_px: f32,
head_length_px: f32,
head_thickness_px: f32,
scale_box_px: f32,
head_hit_extension_px: f32,
scale_hit_extension_px: f32,
hit_threshold_px: f32,
}
const DEFAULT_SIZING: GizmoSizing = GizmoSizing {
axis_screen_length_px: 90.0,
shaft_thickness_px: 3.0,
shaft_hover_thickness_px: 4.5,
head_length_px: 14.0,
head_thickness_px: 10.0,
scale_box_px: 12.0,
head_hit_extension_px: 7.0,
scale_hit_extension_px: 6.0,
hit_threshold_px: 8.0,
};
const TRANSLATION_SIZING: GizmoSizing = GizmoSizing {
axis_screen_length_px: 130.0,
shaft_thickness_px: 5.0,
shaft_hover_thickness_px: 7.0,
head_length_px: 22.0,
head_thickness_px: 16.0,
scale_box_px: 12.0,
head_hit_extension_px: 11.0,
scale_hit_extension_px: 6.0,
hit_threshold_px: 12.0,
};
fn sizing_for_mode(mode: ActiveMode) -> GizmoSizing {
match mode {
ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => TRANSLATION_SIZING,
_ => DEFAULT_SIZING,
}
}
#[derive(Clone, Copy)]
enum HoveredHandle {
None,
Origin,
Axis(usize),
Plane(usize),
Ring(usize),
}
pub fn gizmo_overlay_system(world: &mut World) {
world.resources.user_interface.hud_wants_pointer = false;
if !world.resources.retained_ui.enabled {
return;
}
if !active_camera_overlays_enabled(world) {
clear_all_drags(world);
return;
}
let mode = world.resources.user_interface.gizmos.mode;
let active_mode = resolve_active_mode(mode);
let sizing = sizing_for_mode(active_mode);
clear_inactive_drags(world, active_mode);
let Some(target) = world.resources.graphics.bounding_volume_selected_entity else {
clear_all_drags(world);
return;
};
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if target == camera_entity {
clear_all_drags(world);
return;
}
let Some(target_transform) = world.core.get_global_transform(target) else {
return;
};
let target_position = target_transform.translation();
let Some(camera) = world.core.get_camera(camera_entity).copied() else {
return;
};
let Some(camera_transform) = world.core.get_global_transform(camera_entity) else {
return;
};
let camera_position = camera_transform.translation();
let camera_forward = camera_transform.forward_vector();
let view_matrix = camera_transform
.0
.try_inverse()
.unwrap_or_else(Mat4::identity);
let viewport = match world
.resources
.window
.camera_tile_rects
.get(&camera_entity)
.copied()
.or(world.resources.window.active_viewport_rect)
{
Some(rect) if rect.width > 0.0 && rect.height > 0.0 => rect,
_ => return,
};
world.resources.user_interface.gizmos.current_clip = Some(crate::ecs::ui::types::Rect::new(
viewport.x,
viewport.y,
viewport.width,
viewport.height,
));
let aspect = viewport.width / viewport.height;
let projection_matrix = camera.projection.matrix_with_aspect(aspect);
let view_projection = projection_matrix * view_matrix;
let inverse_view_projection = view_projection.try_inverse().unwrap_or_else(Mat4::identity);
let view_distance =
nalgebra_glm::dot(&(target_position - camera_position), &camera_forward).max(0.001);
let axis_world_length = world_length_for_screen_pixels(
&camera.projection,
view_distance,
viewport.height,
sizing.axis_screen_length_px,
);
let axis_directions = axis_directions_for_mode(active_mode, &target_transform.0);
let Some(hit_origin_screen) = project(&view_projection, &viewport, target_position) else {
world.resources.user_interface.gizmos.current_clip = None;
return;
};
let mouse = crate::ecs::input::access::mouse_for_active(world);
let mouse_position = mouse.position;
let mouse_state = mouse.state;
let mouse_in_viewport = viewport.contains(mouse_position);
let pointer_over_panel = pointer_over_ui_panel(world);
let just_pressed = mouse_state.contains(MouseState::LEFT_JUST_PRESSED);
let just_released = mouse_state.contains(MouseState::LEFT_JUST_RELEASED);
let held = mouse_state.contains(MouseState::LEFT_CLICKED);
let mut hit_axis_ends: [Option<Vec2>; 3] = [None, None, None];
for (index, dir) in axis_directions.iter().enumerate() {
hit_axis_ends[index] = project(
&view_projection,
&viewport,
target_position + dir * axis_world_length,
);
}
let show_planes = !matches!(active_mode, ActiveMode::Rotation);
let mut plane_handles: [Option<PlaneHandleScreen>; 3] = [None, None, None];
if show_planes {
for (locked_axis, slot) in plane_handles.iter_mut().enumerate() {
*slot = compute_plane_handle_screen(
target_position,
&axis_directions,
locked_axis,
axis_world_length,
&view_projection,
&viewport,
);
}
}
let drag_active = drag_active_for_mode(world, active_mode);
let head_extension = match active_mode {
ActiveMode::Scale => sizing.scale_hit_extension_px,
ActiveMode::LocalTranslation
| ActiveMode::GlobalTranslation
| ActiveMode::CompositeLocal
| ActiveMode::CompositeGlobal => sizing.head_hit_extension_px,
ActiveMode::Rotation => 0.0,
};
let hovered = if drag_active || !mouse_in_viewport || pointer_over_panel {
HoveredHandle::None
} else {
compute_hovered_handle(&HoveredHandleQuery {
mode: active_mode,
mouse: mouse_position,
origin: hit_origin_screen,
axis_ends: &hit_axis_ends,
axis_head_extension: head_extension,
axis_hit_threshold: sizing.hit_threshold_px,
plane_handles: &plane_handles,
target_position,
axis_world_length,
axis_directions: &axis_directions,
view_projection: &view_projection,
viewport: &viewport,
})
};
let (hovered_axis, hovered_plane, hovered_ring) = match hovered {
HoveredHandle::Axis(index) => (Some(index), None, None),
HoveredHandle::Plane(index) => (None, Some(index), None),
HoveredHandle::Ring(index) => (None, None, Some(index)),
_ => (None, None, None),
};
let ctx = GizmoStepContext {
target,
target_position,
axis_world_length,
axis_directions: &axis_directions,
view_projection: &view_projection,
inverse_view_projection: &inverse_view_projection,
viewport: &viewport,
camera_position,
mouse_position,
hit_origin_screen,
hit_axis_ends: &hit_axis_ends,
plane_handles: &plane_handles,
hovered_axis,
hovered_plane,
hovered_ring,
just_pressed,
just_released,
held,
};
let mut mutated = false;
let mut active_axis_for_render: Option<usize> = None;
let mut active_plane_for_render: Option<usize> = None;
let mut active_ring_for_render: Option<usize> = None;
let hovered_origin_for_render = matches!(hovered, HoveredHandle::Origin);
match active_mode {
ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
active_plane_for_render = planar_translation_step(world, &ctx, &mut mutated);
if active_plane_for_render.is_none() {
active_axis_for_render = translation_step(world, &ctx, &mut mutated);
}
}
ActiveMode::Scale => {
active_axis_for_render = scale_step(world, &ctx, &mut mutated);
if active_axis_for_render.is_none() {
active_plane_for_render = planar_scale_step(world, &ctx, &mut mutated);
}
}
ActiveMode::Rotation => {
active_ring_for_render = rotation_step(world, &ctx, &mut mutated);
}
ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
active_plane_for_render = planar_translation_step(world, &ctx, &mut mutated);
if active_plane_for_render.is_none() {
active_axis_for_render = translation_step(world, &ctx, &mut mutated);
}
if active_plane_for_render.is_none() && active_axis_for_render.is_none() {
active_ring_for_render = rotation_step(world, &ctx, &mut mutated);
}
}
}
if mutated {
crate::ecs::transform::systems::run_systems(world);
}
let render_origin_world = world
.core
.get_global_transform(target)
.map(|transform| transform.translation())
.unwrap_or(target_position);
let Some(render_origin_screen) = project(&view_projection, &viewport, render_origin_world)
else {
world.resources.user_interface.gizmos.current_clip = None;
return;
};
let render_axis_directions = world
.core
.get_global_transform(target)
.map(|transform| axis_directions_for_mode(active_mode, &transform.0))
.unwrap_or(axis_directions);
render_handles(
world,
&RenderHandlesArgs {
mode: active_mode,
sizing,
origin_screen: render_origin_screen,
origin_world: render_origin_world,
axis_directions: &render_axis_directions,
axis_world_length,
active_axis: active_axis_for_render,
active_plane: active_plane_for_render,
active_ring: active_ring_for_render,
view_projection: &view_projection,
viewport: &viewport,
},
);
let any_active = active_axis_for_render.is_some()
|| active_plane_for_render.is_some()
|| active_ring_for_render.is_some()
|| hovered_origin_for_render;
push_origin_marker(world, render_origin_screen, any_active);
world.resources.user_interface.hud_wants_pointer = any_active;
world.resources.user_interface.gizmos.current_clip = None;
}
fn clear_all_drags(world: &mut World) {
let gizmos = &mut world.resources.user_interface.gizmos;
gizmos.translation_drag = None;
gizmos.scale_drag = None;
gizmos.rotation_drag = None;
gizmos.planar_scale_drag = None;
gizmos.planar_translation_drag = None;
}
fn clear_inactive_drags(world: &mut World, mode: ActiveMode) {
let gizmos = &mut world.resources.user_interface.gizmos;
match mode {
ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
gizmos.scale_drag = None;
gizmos.rotation_drag = None;
gizmos.planar_scale_drag = None;
}
ActiveMode::Scale => {
gizmos.translation_drag = None;
gizmos.rotation_drag = None;
gizmos.planar_translation_drag = None;
}
ActiveMode::Rotation => {
gizmos.translation_drag = None;
gizmos.scale_drag = None;
gizmos.planar_scale_drag = None;
gizmos.planar_translation_drag = None;
}
ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
gizmos.scale_drag = None;
gizmos.planar_scale_drag = None;
}
}
}
fn drag_active_for_mode(world: &World, mode: ActiveMode) -> bool {
let gizmos = &world.resources.user_interface.gizmos;
match mode {
ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
gizmos.translation_drag.is_some() || gizmos.planar_translation_drag.is_some()
}
ActiveMode::Scale => gizmos.scale_drag.is_some() || gizmos.planar_scale_drag.is_some(),
ActiveMode::Rotation => gizmos.rotation_drag.is_some(),
ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
gizmos.translation_drag.is_some()
|| gizmos.rotation_drag.is_some()
|| gizmos.planar_translation_drag.is_some()
}
}
}
fn resolve_active_mode(mode: GizmoMode) -> ActiveMode {
match mode {
GizmoMode::LocalTranslation => ActiveMode::LocalTranslation,
GizmoMode::GlobalTranslation => ActiveMode::GlobalTranslation,
GizmoMode::Scale => ActiveMode::Scale,
GizmoMode::Rotation => ActiveMode::Rotation,
GizmoMode::CompositeLocal => ActiveMode::CompositeLocal,
GizmoMode::CompositeGlobal => ActiveMode::CompositeGlobal,
}
}
fn axis_directions_for_mode(mode: ActiveMode, target_matrix: &Mat4) -> [Vec3; 3] {
match mode {
ActiveMode::LocalTranslation
| ActiveMode::Scale
| ActiveMode::Rotation
| ActiveMode::CompositeLocal => local_axes(target_matrix),
ActiveMode::GlobalTranslation | ActiveMode::CompositeGlobal => [
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
],
}
}
fn local_axes(matrix: &Mat4) -> [Vec3; 3] {
let column_dir = |index: usize| -> Vec3 {
let direction = Vec3::new(matrix[(0, index)], matrix[(1, index)], matrix[(2, index)]);
let magnitude = direction.magnitude();
if magnitude < 1e-6 {
match index {
0 => Vec3::new(1.0, 0.0, 0.0),
1 => Vec3::new(0.0, 1.0, 0.0),
_ => Vec3::new(0.0, 0.0, 1.0),
}
} else {
direction / magnitude
}
};
[column_dir(0), column_dir(1), column_dir(2)]
}
struct GizmoStepContext<'a> {
target: freecs::Entity,
target_position: Vec3,
axis_world_length: f32,
axis_directions: &'a [Vec3; 3],
view_projection: &'a Mat4,
inverse_view_projection: &'a Mat4,
viewport: &'a ViewportRect,
camera_position: Vec3,
mouse_position: Vec2,
hit_origin_screen: Vec2,
hit_axis_ends: &'a [Option<Vec2>; 3],
plane_handles: &'a [Option<PlaneHandleScreen>; 3],
hovered_axis: Option<usize>,
hovered_plane: Option<usize>,
hovered_ring: Option<usize>,
just_pressed: bool,
just_released: bool,
held: bool,
}
struct RingRender<'a> {
target_position: Vec3,
axis_world: Vec3,
radius_world: f32,
color: Vec4,
thickness: f32,
view_projection: &'a Mat4,
viewport: &'a ViewportRect,
}
#[derive(Clone, Copy)]
struct PlaneHandleScreen {
corners: [Vec2; 4],
centroid: Vec2,
locked_axis: usize,
center_world: Vec3,
}
struct HoveredHandleQuery<'a> {
mode: ActiveMode,
mouse: Vec2,
origin: Vec2,
axis_ends: &'a [Option<Vec2>; 3],
axis_head_extension: f32,
axis_hit_threshold: f32,
plane_handles: &'a [Option<PlaneHandleScreen>; 3],
target_position: Vec3,
axis_world_length: f32,
axis_directions: &'a [Vec3; 3],
view_projection: &'a Mat4,
viewport: &'a ViewportRect,
}
struct RenderHandlesArgs<'a> {
mode: ActiveMode,
sizing: GizmoSizing,
origin_screen: Vec2,
origin_world: Vec3,
axis_directions: &'a [Vec3; 3],
axis_world_length: f32,
active_axis: Option<usize>,
active_plane: Option<usize>,
active_ring: Option<usize>,
view_projection: &'a Mat4,
viewport: &'a ViewportRect,
}
fn pointer_over_ui_panel(world: &World) -> bool {
let pointer = world
.resources
.retained_ui
.interaction_for_active()
.hovered_entity
.or(world
.resources
.retained_ui
.interaction_for_active()
.active_entity);
let Some(entity) = pointer else { return false };
let mut current = entity;
loop {
if world.ui.get_ui_panel(current).is_some() {
return true;
}
match world.core.get_parent(current).and_then(|parent| parent.0) {
Some(parent) => current = parent,
None => return false,
}
}
}
fn compute_hovered_handle(query: &HoveredHandleQuery) -> HoveredHandle {
match query.mode {
ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation | ActiveMode::Scale => {
if let Some(index) = nearest_plane_handle(query.mouse, query.plane_handles) {
return HoveredHandle::Plane(index);
}
if let Some(index) = nearest_axis(
query.mouse,
query.origin,
query.axis_ends,
query.axis_head_extension,
query.axis_hit_threshold,
) {
return HoveredHandle::Axis(index);
}
if origin_hit(query.mouse, query.origin) {
return HoveredHandle::Origin;
}
HoveredHandle::None
}
ActiveMode::Rotation => {
if let Some(index) = nearest_rotation_ring(
query.mouse,
query.target_position,
query.axis_world_length,
query.axis_directions,
query.view_projection,
query.viewport,
) {
return HoveredHandle::Ring(index);
}
if origin_hit(query.mouse, query.origin) {
return HoveredHandle::Origin;
}
HoveredHandle::None
}
ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
if let Some(index) = nearest_plane_handle(query.mouse, query.plane_handles) {
return HoveredHandle::Plane(index);
}
if let Some(index) = nearest_axis(
query.mouse,
query.origin,
query.axis_ends,
query.axis_head_extension,
query.axis_hit_threshold,
) {
return HoveredHandle::Axis(index);
}
if let Some(index) = nearest_rotation_ring(
query.mouse,
query.target_position,
query.axis_world_length,
query.axis_directions,
query.view_projection,
query.viewport,
) {
return HoveredHandle::Ring(index);
}
if origin_hit(query.mouse, query.origin) {
return HoveredHandle::Origin;
}
HoveredHandle::None
}
}
}
fn translation_step(
world: &mut World,
ctx: &GizmoStepContext,
mutated: &mut bool,
) -> Option<usize> {
let drag = world.resources.user_interface.gizmos.translation_drag;
if drag.is_none()
&& ctx.just_pressed
&& let Some(axis_index) = ctx.hovered_axis
&& let Some(end) = ctx.hit_axis_ends[axis_index]
{
let initial_t = parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, end);
world.resources.user_interface.gizmos.translation_drag = Some(GizmoTranslationDrag {
axis: axis_index as u8,
initial_translation: ctx.target_position,
axis_world_direction: ctx.axis_directions[axis_index],
axis_world_length: ctx.axis_world_length,
initial_t,
});
}
let drag = world.resources.user_interface.gizmos.translation_drag;
let mut active_axis = ctx.hovered_axis;
if let Some(active) = drag {
active_axis = Some(active.axis as usize);
let drag_origin_screen = project(
ctx.view_projection,
ctx.viewport,
active.initial_translation,
);
let drag_end_screen = project(
ctx.view_projection,
ctx.viewport,
active.initial_translation + active.axis_world_direction * active.axis_world_length,
);
if ctx.held
&& let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
{
let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
let mut delta_world = (current_t - active.initial_t) * active.axis_world_length;
if let Some(step) = world.resources.user_interface.gizmos.translation_snap {
delta_world = crate::ecs::gizmos::state::snap_value(delta_world, step);
}
let new_world_translation =
active.initial_translation + active.axis_world_direction * delta_world;
apply_world_translation(world, ctx.target, new_world_translation);
*mutated = true;
}
if ctx.just_released || !ctx.held {
world.resources.user_interface.gizmos.translation_drag = None;
}
}
active_axis
}
fn scale_step(world: &mut World, ctx: &GizmoStepContext, mutated: &mut bool) -> Option<usize> {
let drag = world.resources.user_interface.gizmos.scale_drag;
if drag.is_none()
&& ctx.just_pressed
&& let Some(axis_index) = ctx.hovered_axis
&& let Some(end) = ctx.hit_axis_ends[axis_index]
{
let initial_t = parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, end);
let initial_scale = world
.core
.get_local_transform(ctx.target)
.map(|transform| transform.scale)
.unwrap_or_else(|| Vec3::new(1.0, 1.0, 1.0));
world.resources.user_interface.gizmos.scale_drag = Some(GizmoScaleDrag {
axis: axis_index as u8,
initial_scale,
axis_world_length: ctx.axis_world_length,
initial_t,
});
}
let drag = world.resources.user_interface.gizmos.scale_drag;
let mut active_axis = ctx.hovered_axis;
if let Some(active) = drag {
active_axis = Some(active.axis as usize);
let drag_origin_screen = project(ctx.view_projection, ctx.viewport, ctx.target_position);
let drag_end_screen = project(
ctx.view_projection,
ctx.viewport,
ctx.target_position
+ ctx.axis_directions[active.axis as usize] * active.axis_world_length,
);
if ctx.held
&& let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
{
let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
let delta_t = current_t - active.initial_t;
let scale_factor = (1.0 + delta_t * SCALE_DRAG_SENSITIVITY).max(MIN_SCALE_COMPONENT);
let mut new_scale = active.initial_scale;
match active.axis {
0 => new_scale.x = active.initial_scale.x * scale_factor,
1 => new_scale.y = active.initial_scale.y * scale_factor,
2 => new_scale.z = active.initial_scale.z * scale_factor,
_ => {}
}
new_scale.x = new_scale.x.max(MIN_SCALE_COMPONENT);
new_scale.y = new_scale.y.max(MIN_SCALE_COMPONENT);
new_scale.z = new_scale.z.max(MIN_SCALE_COMPONENT);
if let Some(step) = world.resources.user_interface.gizmos.scale_snap {
match active.axis {
0 => {
new_scale.x = crate::ecs::gizmos::state::snap_value(new_scale.x, step)
.max(MIN_SCALE_COMPONENT)
}
1 => {
new_scale.y = crate::ecs::gizmos::state::snap_value(new_scale.y, step)
.max(MIN_SCALE_COMPONENT)
}
2 => {
new_scale.z = crate::ecs::gizmos::state::snap_value(new_scale.z, step)
.max(MIN_SCALE_COMPONENT)
}
_ => {}
}
}
apply_local_scale(world, ctx.target, new_scale);
*mutated = true;
}
if ctx.just_released || !ctx.held {
world.resources.user_interface.gizmos.scale_drag = None;
}
}
active_axis
}
fn planar_scale_step(
world: &mut World,
ctx: &GizmoStepContext,
mutated: &mut bool,
) -> Option<usize> {
let drag = world.resources.user_interface.gizmos.planar_scale_drag;
if drag.is_none()
&& ctx.just_pressed
&& let Some(locked_axis) = ctx.hovered_plane
&& let Some(plane) = ctx.plane_handles[locked_axis].as_ref()
{
let initial_t =
parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, plane.centroid);
let initial_scale = world
.core
.get_local_transform(ctx.target)
.map(|transform| transform.scale)
.unwrap_or_else(|| Vec3::new(1.0, 1.0, 1.0));
let diagonal = plane.center_world - ctx.target_position;
let diagonal_length = diagonal.magnitude();
if diagonal_length > 1e-4 {
world.resources.user_interface.gizmos.planar_scale_drag = Some(GizmoPlanarScaleDrag {
locked_axis: locked_axis as u8,
initial_scale,
diagonal_world: diagonal / diagonal_length,
diagonal_world_length: diagonal_length,
initial_t,
});
}
}
let drag = world.resources.user_interface.gizmos.planar_scale_drag;
let mut active_plane = ctx.hovered_plane;
if let Some(active) = drag {
active_plane = Some(active.locked_axis as usize);
let drag_origin_screen = project(ctx.view_projection, ctx.viewport, ctx.target_position);
let drag_end_screen = project(
ctx.view_projection,
ctx.viewport,
ctx.target_position + active.diagonal_world * active.diagonal_world_length,
);
if ctx.held
&& let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
{
let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
let delta_t = current_t - active.initial_t;
let scale_factor = (1.0 + delta_t * SCALE_DRAG_SENSITIVITY).max(MIN_SCALE_COMPONENT);
let mut new_scale = active.initial_scale;
match active.locked_axis {
0 => {
new_scale.y = active.initial_scale.y * scale_factor;
new_scale.z = active.initial_scale.z * scale_factor;
}
1 => {
new_scale.x = active.initial_scale.x * scale_factor;
new_scale.z = active.initial_scale.z * scale_factor;
}
2 => {
new_scale.x = active.initial_scale.x * scale_factor;
new_scale.y = active.initial_scale.y * scale_factor;
}
_ => {}
}
new_scale.x = new_scale.x.max(MIN_SCALE_COMPONENT);
new_scale.y = new_scale.y.max(MIN_SCALE_COMPONENT);
new_scale.z = new_scale.z.max(MIN_SCALE_COMPONENT);
if let Some(step) = world.resources.user_interface.gizmos.scale_snap {
let snap = crate::ecs::gizmos::state::snap_value;
match active.locked_axis {
0 => {
new_scale.y = snap(new_scale.y, step).max(MIN_SCALE_COMPONENT);
new_scale.z = snap(new_scale.z, step).max(MIN_SCALE_COMPONENT);
}
1 => {
new_scale.x = snap(new_scale.x, step).max(MIN_SCALE_COMPONENT);
new_scale.z = snap(new_scale.z, step).max(MIN_SCALE_COMPONENT);
}
2 => {
new_scale.x = snap(new_scale.x, step).max(MIN_SCALE_COMPONENT);
new_scale.y = snap(new_scale.y, step).max(MIN_SCALE_COMPONENT);
}
_ => {}
}
}
apply_local_scale(world, ctx.target, new_scale);
*mutated = true;
}
if ctx.just_released || !ctx.held {
world.resources.user_interface.gizmos.planar_scale_drag = None;
}
}
active_plane
}
fn planar_translation_step(
world: &mut World,
ctx: &GizmoStepContext,
mutated: &mut bool,
) -> Option<usize> {
let drag = world
.resources
.user_interface
.gizmos
.planar_translation_drag;
if drag.is_none()
&& ctx.just_pressed
&& let Some(locked_axis) = ctx.hovered_plane
{
let plane_normal = ctx.axis_directions[locked_axis];
if let Some(initial_world_hit) = mouse_ray_plane_hit(ctx, plane_normal) {
world
.resources
.user_interface
.gizmos
.planar_translation_drag = Some(GizmoPlanarTranslationDrag {
locked_axis: locked_axis as u8,
initial_translation: ctx.target_position,
plane_normal_world: plane_normal,
initial_world_hit,
});
}
}
let drag = world
.resources
.user_interface
.gizmos
.planar_translation_drag;
let mut active_plane = ctx.hovered_plane;
if let Some(active) = drag {
active_plane = Some(active.locked_axis as usize);
if ctx.held
&& let Some(current_world_hit) = mouse_ray_plane_hit(ctx, active.plane_normal_world)
{
let mut delta = current_world_hit - active.initial_world_hit;
if let Some(step) = world.resources.user_interface.gizmos.translation_snap {
delta = crate::ecs::gizmos::state::snap_vec3(delta, step);
}
let new_world = active.initial_translation + delta;
apply_world_translation(world, ctx.target, new_world);
*mutated = true;
}
if ctx.just_released || !ctx.held {
world
.resources
.user_interface
.gizmos
.planar_translation_drag = None;
}
}
active_plane
}
fn rotation_step(world: &mut World, ctx: &GizmoStepContext, mutated: &mut bool) -> Option<usize> {
let drag = world.resources.user_interface.gizmos.rotation_drag;
if drag.is_none()
&& ctx.just_pressed
&& let Some(axis_index) = ctx.hovered_ring
{
let axis_world = ctx.axis_directions[axis_index];
if let Some(start_point) = mouse_ray_plane_hit(ctx, axis_world) {
let start_world_vector =
project_onto_plane(start_point - ctx.target_position, axis_world);
if start_world_vector.magnitude() > 1e-3 {
let initial_rotation = world
.core
.get_local_transform(ctx.target)
.map(|transform| transform.rotation)
.unwrap_or_else(nalgebra_glm::quat_identity);
world.resources.user_interface.gizmos.rotation_drag = Some(GizmoRotationDrag {
axis: axis_index as u8,
axis_world,
initial_rotation,
start_world_vector: start_world_vector.normalize(),
});
}
}
}
let drag = world.resources.user_interface.gizmos.rotation_drag;
let mut active_axis = ctx.hovered_ring;
if let Some(active) = drag {
active_axis = Some(active.axis as usize);
if ctx.held
&& let Some(current_point) = mouse_ray_plane_hit(ctx, active.axis_world)
{
let current_vector =
project_onto_plane(current_point - ctx.target_position, active.axis_world);
if current_vector.magnitude() > 1e-3 {
let current_unit = current_vector.normalize();
let cross_vec = active.start_world_vector.cross(¤t_unit);
let dot_product = nalgebra_glm::dot(&active.start_world_vector, ¤t_unit);
let mut signed_angle =
nalgebra_glm::dot(&cross_vec, &active.axis_world).atan2(dot_product);
if let Some(step) = world.resources.user_interface.gizmos.rotation_snap_radians {
signed_angle = crate::ecs::gizmos::state::snap_value(signed_angle, step);
}
let delta_world = nalgebra_glm::quat_angle_axis(signed_angle, &active.axis_world);
let new_local_rotation =
nalgebra_glm::quat_normalize(&(delta_world * active.initial_rotation));
apply_local_rotation(world, ctx.target, new_local_rotation);
*mutated = true;
}
}
if ctx.just_released || !ctx.held {
world.resources.user_interface.gizmos.rotation_drag = None;
}
}
active_axis
}
fn project_onto_plane(vector: Vec3, plane_normal: Vec3) -> Vec3 {
vector - plane_normal * nalgebra_glm::dot(&vector, &plane_normal)
}
fn mouse_ray_plane_hit(ctx: &GizmoStepContext, plane_normal: Vec3) -> Option<Vec3> {
let ndc_x = ((ctx.mouse_position.x - ctx.viewport.x) / ctx.viewport.width) * 2.0 - 1.0;
let ndc_y = 1.0 - ((ctx.mouse_position.y - ctx.viewport.y) / ctx.viewport.height) * 2.0;
let near_clip = ctx.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 - ctx.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 t = nalgebra_glm::dot(&(ctx.target_position - ctx.camera_position), &plane_normal)
/ denominator;
if t < 0.0 {
return None;
}
Some(ctx.camera_position + direction_unit * t)
}
fn compute_plane_handle_screen(
target_position: Vec3,
axis_directions: &[Vec3; 3],
locked_axis: usize,
axis_world_length: f32,
view_projection: &Mat4,
viewport: &ViewportRect,
) -> Option<PlaneHandleScreen> {
let (a_index, b_index) = match locked_axis {
0 => (1, 2),
1 => (0, 2),
2 => (0, 1),
_ => return None,
};
let a_dir = axis_directions[a_index];
let b_dir = axis_directions[b_index];
let inner = axis_world_length * PLANE_HANDLE_INNER_FRACTION;
let outer = axis_world_length * PLANE_HANDLE_OUTER_FRACTION;
let p00 = target_position + a_dir * inner + b_dir * inner;
let p10 = target_position + a_dir * outer + b_dir * inner;
let p11 = target_position + a_dir * outer + b_dir * outer;
let p01 = target_position + a_dir * inner + b_dir * outer;
let s00 = project(view_projection, viewport, p00)?;
let s10 = project(view_projection, viewport, p10)?;
let s11 = project(view_projection, viewport, p11)?;
let s01 = project(view_projection, viewport, p01)?;
let center_world = (p00 + p10 + p11 + p01) / 4.0;
let centroid = (s00 + s10 + s11 + s01) / 4.0;
Some(PlaneHandleScreen {
corners: [s00, s10, s11, s01],
centroid,
locked_axis,
center_world,
})
}
fn nearest_plane_handle(mouse: Vec2, planes: &[Option<PlaneHandleScreen>; 3]) -> Option<usize> {
let mut best: Option<(usize, f32)> = None;
for plane in planes.iter().flatten() {
if !point_in_quad(mouse, &plane.corners) {
continue;
}
let distance = (mouse - plane.centroid).magnitude();
match best {
Some((_, current)) if current <= distance => {}
_ => best = Some((plane.locked_axis, distance)),
}
}
best.map(|(index, _)| index)
}
fn point_in_quad(point: Vec2, corners: &[Vec2; 4]) -> bool {
let mut sign: f32 = 0.0;
for index in 0..4 {
let from = corners[index];
let to = corners[(index + 1) % 4];
let edge = to - from;
let to_point = point - from;
let cross = edge.x * to_point.y - edge.y * to_point.x;
if cross.abs() < 1e-4 {
continue;
}
if sign == 0.0 {
sign = cross.signum();
} else if sign * cross < 0.0 {
return false;
}
}
true
}
fn nearest_rotation_ring(
mouse: Vec2,
target_position: Vec3,
axis_world_length: f32,
axis_directions: &[Vec3; 3],
view_projection: &Mat4,
viewport: &ViewportRect,
) -> Option<usize> {
let mut best: Option<(usize, f32)> = None;
for (axis_index, axis_world) in axis_directions.iter().copied().enumerate() {
let basis = ring_basis(axis_world);
let mut previous: Option<Vec2> = None;
for sample in 0..=ROTATION_RING_SEGMENTS {
let theta = (sample as f32 / ROTATION_RING_SEGMENTS as f32) * std::f32::consts::TAU;
let world_point = target_position
+ (basis.0 * theta.cos() + basis.1 * theta.sin()) * axis_world_length;
let Some(screen_point) = project(view_projection, viewport, world_point) else {
previous = None;
continue;
};
if let Some(prev) = previous {
let distance = distance_point_to_segment(mouse, prev, screen_point);
if distance <= ROTATION_HIT_THRESHOLD_PX {
match best {
Some((_, current)) if current <= distance => {}
_ => best = Some((axis_index, distance)),
}
}
}
previous = Some(screen_point);
}
}
best.map(|(index, _)| index)
}
fn push_rotation_ring(world: &mut World, ring: &RingRender) {
let basis = ring_basis(ring.axis_world);
let mut previous: Option<Vec2> = None;
for sample in 0..=ROTATION_RING_SEGMENTS {
let theta = (sample as f32 / ROTATION_RING_SEGMENTS as f32) * std::f32::consts::TAU;
let world_point = ring.target_position
+ (basis.0 * theta.cos() + basis.1 * theta.sin()) * ring.radius_world;
let Some(screen_point) = project(ring.view_projection, ring.viewport, world_point) else {
previous = None;
continue;
};
if let Some(prev) = previous {
push_segment(world, prev, screen_point, ring.color, ring.thickness);
}
previous = Some(screen_point);
}
}
fn ring_basis(axis: Vec3) -> (Vec3, Vec3) {
let world_up = if axis.y.abs() > 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let u = world_up.cross(&axis).normalize();
let v = axis.cross(&u).normalize();
(u, v)
}
fn apply_local_rotation(
world: &mut World,
entity: freecs::Entity,
new_rotation: nalgebra_glm::Quat,
) {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.rotation = new_rotation;
}
mark_local_transform_dirty(world, entity);
}
fn world_length_for_screen_pixels(
projection: &crate::ecs::camera::components::Projection,
view_distance: f32,
viewport_height: f32,
desired_pixels: f32,
) -> f32 {
use crate::ecs::camera::components::Projection;
match projection {
Projection::Perspective(perspective) => {
let half_height_at_distance = view_distance * (perspective.y_fov_rad * 0.5).tan();
half_height_at_distance * 2.0 * desired_pixels / viewport_height
}
Projection::Orthographic(_) => desired_pixels / viewport_height,
}
}
fn project(view_projection: &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 nearest_axis(
mouse: Vec2,
origin: Vec2,
axis_ends: &[Option<Vec2>; 3],
head_extension: f32,
hit_threshold: f32,
) -> Option<usize> {
let mut best: Option<(usize, f32)> = None;
for (index, end) in axis_ends.iter().enumerate() {
let Some(end) = end else { continue };
let extended_end = extend_past_tip(origin, *end, head_extension);
let distance = distance_point_to_segment(mouse, origin, extended_end);
if distance > hit_threshold {
continue;
}
match best {
Some((_, current)) if current <= distance => {}
_ => best = Some((index, distance)),
}
}
best.map(|(index, _)| index)
}
fn extend_past_tip(origin: Vec2, tip: Vec2, extension: f32) -> Vec2 {
let delta = tip - origin;
let length = delta.magnitude();
if length < 0.001 {
return tip;
}
tip + delta * (extension / length)
}
fn origin_hit(mouse: Vec2, origin: Vec2) -> bool {
(mouse - origin).magnitude() <= ORIGIN_HIT_THRESHOLD_PX
}
fn distance_point_to_segment(point: Vec2, from: Vec2, to: Vec2) -> f32 {
let segment = to - from;
let length_squared = segment.norm_squared();
if length_squared < 0.0001 {
return (point - from).magnitude();
}
let t = nalgebra_glm::dot(&(point - from), &segment) / length_squared;
let clamped = t.clamp(0.0, 1.0);
let closest = from + segment * clamped;
(point - closest).magnitude()
}
fn parametric_t_on_segment(point: Vec2, from: Vec2, to: Vec2) -> f32 {
let segment = to - from;
let length_squared = segment.norm_squared();
if length_squared < 0.0001 {
return 0.0;
}
nalgebra_glm::dot(&(point - from), &segment) / length_squared
}
fn apply_world_translation(world: &mut World, entity: freecs::Entity, new_world: Vec3) {
let parent_inverse = world
.core
.get_parent(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(Mat4::identity));
let new_local = match parent_inverse {
Some(inverse) => {
let homogeneous =
inverse * nalgebra_glm::Vec4::new(new_world.x, new_world.y, new_world.z, 1.0);
Vec3::new(homogeneous.x, homogeneous.y, homogeneous.z)
}
None => new_world,
};
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.translation = new_local;
}
mark_local_transform_dirty(world, entity);
}
fn apply_local_scale(world: &mut World, entity: freecs::Entity, new_scale: Vec3) {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.scale = new_scale;
}
mark_local_transform_dirty(world, entity);
}
fn render_handles(world: &mut World, args: &RenderHandlesArgs) {
let sizing = args.sizing;
let draw_planes = !matches!(args.mode, ActiveMode::Rotation);
if draw_planes {
for (locked_axis, plane_color) in PLANE_COLORS.iter().copied().enumerate() {
let Some(plane) = compute_plane_handle_screen(
args.origin_world,
args.axis_directions,
locked_axis,
args.axis_world_length,
args.view_projection,
args.viewport,
) else {
continue;
};
let is_active = args.active_plane == Some(locked_axis);
let color = if is_active {
PLANE_HOVER_COLOR
} else {
plane_color
};
push_plane_solid(world, &plane.corners, color);
}
}
let mut axis_ends: [Option<Vec2>; 3] = [None, None, None];
for (index, dir) in args.axis_directions.iter().enumerate() {
axis_ends[index] = project(
args.view_projection,
args.viewport,
args.origin_world + dir * args.axis_world_length,
);
}
let head_kind = match args.mode {
ActiveMode::LocalTranslation
| ActiveMode::GlobalTranslation
| ActiveMode::CompositeLocal
| ActiveMode::CompositeGlobal
| ActiveMode::Rotation => DrawHead::Triangle,
ActiveMode::Scale => DrawHead::Box,
};
let draw_axes = matches!(
args.mode,
ActiveMode::LocalTranslation
| ActiveMode::GlobalTranslation
| ActiveMode::Scale
| ActiveMode::CompositeLocal
| ActiveMode::CompositeGlobal
);
if draw_axes {
for (index, end) in axis_ends.iter().enumerate() {
let Some(end) = end else { continue };
let is_active = args.active_axis == Some(index);
let color = if is_active {
HOVER_COLOR
} else {
AXIS_COLORS[index]
};
let thickness = if is_active {
sizing.shaft_hover_thickness_px
} else {
sizing.shaft_thickness_px
};
let head_inset = match head_kind {
DrawHead::Triangle => sizing.head_length_px,
DrawHead::Box => sizing.scale_box_px,
};
let delta = *end - args.origin_screen;
let length = delta.magnitude();
let segment_end = if length > head_inset {
args.origin_screen + delta * ((length - head_inset) / length)
} else {
args.origin_screen
};
push_segment(world, args.origin_screen, segment_end, color, thickness);
match head_kind {
DrawHead::Triangle => push_arrow_head(
world,
args.origin_screen,
*end,
color,
sizing.head_length_px,
sizing.head_thickness_px,
),
DrawHead::Box => {
push_scale_box(world, args.origin_screen, *end, color, sizing.scale_box_px)
}
}
}
}
let draw_rings = matches!(
args.mode,
ActiveMode::Rotation | ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal
);
if draw_rings {
for (axis_index, axis_color) in AXIS_COLORS.iter().copied().enumerate() {
let is_active = args.active_ring == Some(axis_index);
let color = if is_active { HOVER_COLOR } else { axis_color };
let thickness = if is_active {
sizing.shaft_hover_thickness_px
} else {
sizing.shaft_thickness_px
};
push_rotation_ring(
world,
&RingRender {
target_position: args.origin_world,
axis_world: args.axis_directions[axis_index],
radius_world: args.axis_world_length,
color,
thickness,
view_projection: args.view_projection,
viewport: args.viewport,
},
);
}
}
}
fn push_segment(world: &mut World, from: Vec2, to: Vec2, color: Vec4, thickness: f32) {
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);
push_rect(world, position, Vec2::new(length, thickness), color, angle);
}
fn push_arrow_head(
world: &mut World,
from: Vec2,
to: Vec2,
color: Vec4,
head_length: f32,
head_thickness: f32,
) {
let delta = to - from;
let length = delta.magnitude();
if length < 0.001 {
return;
}
let direction = delta / length;
let head_center = to - direction * (head_length * 0.5);
let angle = delta.y.atan2(delta.x);
let position = head_center - Vec2::new(head_length * 0.5, head_thickness * 0.5);
push_rect_with_effect(
world,
position,
Vec2::new(head_length, head_thickness),
color,
angle,
TRIANGLE_EFFECT_KIND,
);
}
fn push_scale_box(world: &mut World, from: Vec2, to: Vec2, color: Vec4, box_px: f32) {
let delta = to - from;
let length = delta.magnitude();
if length < 0.001 {
return;
}
let direction = delta / length;
let box_center = to - direction * (box_px * 0.5);
let angle = delta.y.atan2(delta.x);
let half = box_px * 0.5;
let position = box_center - Vec2::new(half, half);
push_rect(world, position, Vec2::new(box_px, box_px), color, angle);
}
fn push_plane_solid(world: &mut World, corners: &[Vec2; 4], color: Vec4) {
let bbox_min = Vec2::new(
corners.iter().map(|c| c.x).fold(f32::INFINITY, f32::min),
corners.iter().map(|c| c.y).fold(f32::INFINITY, f32::min),
);
let bbox_max = Vec2::new(
corners
.iter()
.map(|c| c.x)
.fold(f32::NEG_INFINITY, f32::max),
corners
.iter()
.map(|c| c.y)
.fold(f32::NEG_INFINITY, f32::max),
);
let bbox_size = bbox_max - bbox_min;
if bbox_size.x < 0.5 || bbox_size.y < 0.5 {
return;
}
let clip = world.resources.user_interface.gizmos.current_clip;
world.resources.retained_ui.frame.rects.push(UiRect {
position: bbox_min,
size: bbox_size,
color,
corner_radius: 0.0,
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: Z_INDEX,
shadow: None,
effect_kind: 0,
effect_params: [0.0; 4],
quad_corners: Some(*corners),
});
}
fn push_origin_marker(world: &mut World, origin: Vec2, active: bool) {
let color = if active {
Vec4::new(1.0, 1.0, 1.0, 1.0)
} else {
Vec4::new(0.85, 0.85, 0.85, 1.0)
};
let half = ORIGIN_HANDLE_PX * 0.5;
let position = origin - Vec2::new(half, half);
push_rect(
world,
position,
Vec2::new(ORIGIN_HANDLE_PX, ORIGIN_HANDLE_PX),
color,
0.0,
);
}
fn push_rect(world: &mut World, position: Vec2, size: Vec2, color: Vec4, rotation: f32) {
push_rect_with_effect(world, position, size, color, rotation, 0);
}
fn push_rect_with_effect(
world: &mut World,
position: Vec2,
size: Vec2,
color: Vec4,
rotation: f32,
effect_kind: u32,
) {
let clip = world.resources.user_interface.gizmos.current_clip;
world.resources.retained_ui.frame.rects.push(UiRect {
position,
size,
color,
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation,
clip_rect: clip,
layer: UiLayer::Background,
z_index: Z_INDEX,
shadow: None,
effect_kind,
effect_params: [0.0; 4],
quad_corners: None,
});
}