use bevy::camera::visibility::RenderLayers;
use bevy::prelude::*;
use bevy_kana::ToF32;
use super::convex_hull;
use super::labels;
use super::labels::BoundsLabel;
use super::labels::MarginLabel;
use super::labels::MarginLabelParams;
use super::screen_space;
use super::types::FitTargetGizmo;
use super::types::FitTargetOverlayConfig;
use super::types::FitTargetViewportMarginPcts;
use crate::components::CurrentFitTarget;
use crate::components::FitOverlay;
use crate::constants::TOLERANCE;
use crate::fit::Edge;
use crate::support;
use crate::support::CameraBasis;
use crate::support::ScreenSpaceBounds;
const fn calculate_edge_color(
edge: Edge,
h_balanced: bool,
v_balanced: bool,
config: &FitTargetOverlayConfig,
) -> Color {
match edge {
Edge::Left | Edge::Right => {
if h_balanced {
config.balanced_color
} else {
config.unbalanced_color
}
},
Edge::Top | Edge::Bottom => {
if v_balanced {
config.balanced_color
} else {
config.unbalanced_color
}
},
}
}
fn create_screen_corners(
bounds: &ScreenSpaceBounds,
camera: &CameraBasis,
avg_depth: f32,
is_ortho: bool,
) -> [Vec3; 4] {
[
screen_space::normalized_to_world(
bounds.min_norm_x,
bounds.min_norm_y,
camera,
avg_depth,
is_ortho,
),
screen_space::normalized_to_world(
bounds.max_norm_x,
bounds.min_norm_y,
camera,
avg_depth,
is_ortho,
),
screen_space::normalized_to_world(
bounds.max_norm_x,
bounds.max_norm_y,
camera,
avg_depth,
is_ortho,
),
screen_space::normalized_to_world(
bounds.min_norm_x,
bounds.max_norm_y,
camera,
avg_depth,
is_ortho,
),
]
}
fn draw_rectangle(
gizmos: &mut Gizmos<FitTargetGizmo>,
corners: &[Vec3; 4],
config: &FitTargetOverlayConfig,
) {
for i in 0..4 {
let next = (i + 1) % 4;
gizmos.line(corners[i], corners[next], config.rectangle_color);
}
}
fn draw_silhouette(
gizmos: &mut Gizmos<FitTargetGizmo>,
vertices: &[Vec3],
camera: &CameraBasis,
avg_depth: f32,
is_ortho: bool,
color: Color,
) {
let projected = convex_hull::project_vertices_to_2d(vertices, camera, is_ortho);
let hull = convex_hull::convex_hull_2d(&projected);
if hull.len() < 2 {
return;
}
for i in 0..hull.len() {
let next = (i + 1) % hull.len();
let start =
screen_space::normalized_to_world(hull[i].0, hull[i].1, camera, avg_depth, is_ortho);
let end = screen_space::normalized_to_world(
hull[next].0,
hull[next].1,
camera,
avg_depth,
is_ortho,
);
gizmos.line(start, end, color);
}
}
struct DrawContext<'a> {
camera: Entity,
bounds: &'a ScreenSpaceBounds,
camera_basis: &'a CameraBasis,
avg_depth: f32,
is_ortho: bool,
viewport_size: Option<Vec2>,
}
fn draw_margin_lines_and_labels(
commands: &mut Commands,
gizmos: &mut Gizmos<FitTargetGizmo>,
label_query: &mut Query<(Entity, &MarginLabel, &mut Text, &mut Node, &mut TextColor)>,
ctx: &DrawContext,
config: &FitTargetOverlayConfig,
) -> Vec<Edge> {
let camera = ctx.camera;
let bounds = ctx.bounds;
let camera_basis = ctx.camera_basis;
let avg_depth = ctx.avg_depth;
let is_ortho = ctx.is_ortho;
let viewport_size = ctx.viewport_size;
let h_balanced = screen_space::is_horizontally_balanced(bounds, TOLERANCE);
let v_balanced = screen_space::is_vertically_balanced(bounds, TOLERANCE);
let mut visible_edges: Vec<Edge> = Vec::new();
for edge in [Edge::Left, Edge::Right, Edge::Top, Edge::Bottom] {
let Some((boundary_x, boundary_y)) = screen_space::boundary_edge_center(bounds, edge)
else {
continue;
};
visible_edges.push(edge);
let (screen_x, screen_y) = screen_space::screen_edge_center(bounds, edge);
let boundary_pos = screen_space::normalized_to_world(
boundary_x,
boundary_y,
camera_basis,
avg_depth,
is_ortho,
);
let screen_pos = screen_space::normalized_to_world(
screen_x,
screen_y,
camera_basis,
avg_depth,
is_ortho,
);
let color = calculate_edge_color(edge, h_balanced, v_balanced, config);
gizmos.line(boundary_pos, screen_pos, color);
let Some(vp) = viewport_size else {
continue;
};
let percentage = screen_space::margin_percentage(bounds, edge);
let text = format!("margin: {percentage:.3}%");
let label_screen_pos = labels::calculate_label_pixel_position(edge, bounds, vp);
labels::update_or_create_margin_label(
commands,
label_query,
MarginLabelParams {
camera,
edge,
text,
color,
screen_pos: label_screen_pos,
viewport_size: vp,
},
);
}
visible_edges
}
fn cleanup_stale_margin_labels(
commands: &mut Commands,
label_query: &Query<(Entity, &MarginLabel, &mut Text, &mut Node, &mut TextColor)>,
camera: Entity,
visible_edges: &[Edge],
) {
for (entity, label, _, _, _) in label_query {
if label.camera == camera && !visible_edges.contains(&label.edge) {
commands.entity(entity).despawn();
}
}
}
pub(super) fn on_remove_fit_visualization(
trigger: On<Remove, FitOverlay>,
mut commands: Commands,
label_query: Query<(Entity, &MarginLabel)>,
bounds_label_query: Query<(Entity, &BoundsLabel)>,
) {
let camera = trigger.entity;
commands
.entity(camera)
.try_remove::<FitTargetViewportMarginPcts>();
for (entity, label) in &label_query {
if label.camera == camera {
commands.entity(entity).despawn();
}
}
for (entity, label) in &bounds_label_query {
if label.camera == camera {
commands.entity(entity).despawn();
}
}
}
pub(super) fn sync_gizmo_render_layers(
mut config_store: ResMut<GizmoConfigStore>,
viz_config: Res<FitTargetOverlayConfig>,
camera_query: Query<Option<&RenderLayers>, With<FitOverlay>>,
) {
let (gizmo_config, _) = config_store.config_mut::<FitTargetGizmo>();
gizmo_config.line.width = viz_config.line_width;
gizmo_config.depth_bias = -1.0;
if let Some(Some(layers)) = camera_query.iter().next() {
gizmo_config.render_layers = layers.clone();
}
}
pub(super) fn draw_fit_target_bounds(
mut commands: Commands,
mut gizmos: Gizmos<FitTargetGizmo>,
config: Res<FitTargetOverlayConfig>,
camera_query: Query<
(
Entity,
&Camera,
&GlobalTransform,
&Projection,
&CurrentFitTarget,
),
With<FitOverlay>,
>,
mesh_query: Query<&Mesh3d>,
children_query: Query<&Children>,
global_transform_query: Query<&GlobalTransform>,
meshes: Res<Assets<Mesh>>,
mut label_query: Query<(Entity, &MarginLabel, &mut Text, &mut Node, &mut TextColor)>,
mut bounds_label_query: Query<(Entity, &BoundsLabel, &mut Node), Without<MarginLabel>>,
) {
for (camera, camera_component, camera_global, projection, current_target) in &camera_query {
let Some((vertices, _)) = support::extract_mesh_vertices(
current_target.0,
&children_query,
&mesh_query,
&global_transform_query,
&meshes,
) else {
continue;
};
draw_bounds_for_camera(
&mut commands,
&mut gizmos,
&config,
&BoundsCamera {
entity: camera,
camera_component,
camera_global,
projection,
},
&vertices,
&mut label_query,
&mut bounds_label_query,
);
}
}
struct BoundsCamera<'a> {
entity: Entity,
camera_component: &'a Camera,
camera_global: &'a GlobalTransform,
projection: &'a Projection,
}
fn draw_bounds_for_camera(
commands: &mut Commands,
gizmos: &mut Gizmos<FitTargetGizmo>,
config: &FitTargetOverlayConfig,
camera_data: &BoundsCamera,
vertices: &[Vec3],
label_query: &mut Query<(Entity, &MarginLabel, &mut Text, &mut Node, &mut TextColor)>,
bounds_label_query: &mut Query<(Entity, &BoundsLabel, &mut Node), Without<MarginLabel>>,
) {
let camera = camera_data.entity;
let camera_component = camera_data.camera_component;
let camera_global = camera_data.camera_global;
let projection = camera_data.projection;
let camera_basis = CameraBasis::from_global_transform(camera_global);
let Some(aspect_ratio) =
support::projection_aspect_ratio(projection, camera_component.logical_viewport_size())
else {
return;
};
let Some((bounds, depths)) =
ScreenSpaceBounds::from_points(vertices, camera_global, projection, aspect_ratio)
else {
return;
};
let avg_depth = depths.sum / depths.count.to_f32();
let is_ortho = matches!(projection, Projection::Orthographic(_));
let viewport_size = camera_component.logical_viewport_size();
commands
.entity(camera)
.try_insert(FitTargetViewportMarginPcts::from_bounds(&bounds));
let corners = create_screen_corners(&bounds, &camera_basis, avg_depth, is_ortho);
draw_rectangle(gizmos, &corners, config);
draw_silhouette(
gizmos,
vertices,
&camera_basis,
avg_depth,
is_ortho,
config.silhouette_color,
);
if let Some(vp) = viewport_size {
let upper_left = screen_space::norm_to_viewport(
bounds.min_norm_x,
bounds.max_norm_y,
bounds.half_extent_x,
bounds.half_extent_y,
vp,
);
labels::update_or_create_bounds_label(
commands,
bounds_label_query,
camera,
labels::bounds_label_position(upper_left),
);
}
let draw_ctx = DrawContext {
camera,
bounds: &bounds,
camera_basis: &camera_basis,
avg_depth,
is_ortho,
viewport_size,
};
let visible_edges =
draw_margin_lines_and_labels(commands, gizmos, label_query, &draw_ctx, config);
cleanup_stale_margin_labels(commands, label_query, camera, &visible_edges);
}