use bevy::camera::CameraUpdateSystems;
use bevy::picking::Pickable;
use bevy::prelude::*;
use bevy::transform::TransformSystems;
use bevy::ui::{ComputedNode, ComputedUiTargetCamera, UiSystems};
use crate::model::{box_outside, pin_to_edge};
use crate::skin::SAFE_EDGE_PERCENT;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Debug, Default, Clone, PartialEq)]
pub enum OffScreen {
#[default]
Hide,
Pin,
}
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Debug, Clone)]
#[relationship(relationship_target = WorldAnchors)]
#[require(
Node,
Pickable = Pickable::IGNORE,
Visibility = Visibility::Hidden,
)]
pub struct WorldAnchor {
#[relationship]
target: Entity,
pub offset: Vec3,
pub pivot: Vec2,
pub off_screen: OffScreen,
pub camera: Option<Entity>,
}
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
#[relationship_target(relationship = WorldAnchor, linked_spawn)]
pub struct WorldAnchors(Vec<Entity>);
const FOOT: Vec2 = Vec2::new(0.5, 1.0);
impl Default for WorldAnchor {
fn default() -> Self {
Self {
target: Entity::PLACEHOLDER,
offset: Vec3::ZERO,
pivot: FOOT,
off_screen: OffScreen::Hide,
camera: None,
}
}
}
impl WorldAnchor {
pub fn new(target: Entity) -> Self {
Self {
target,
..default()
}
}
pub fn above(target: Entity, height: f32) -> Self {
Self::new(target).offset(Vec3::Y * height)
}
#[must_use]
pub fn offset(mut self, offset: Vec3) -> Self {
self.offset = offset;
self
}
#[must_use]
pub fn pivot(mut self, pivot: Vec2) -> Self {
self.pivot = pivot;
self
}
#[must_use]
pub fn pinned(mut self) -> Self {
self.off_screen = OffScreen::Pin;
self
}
#[must_use]
pub fn camera(mut self, camera: Entity) -> Self {
self.camera = Some(camera);
self
}
pub fn target(&self) -> Entity {
self.target
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, Clone, PartialEq)]
pub struct AnchorPinned {
pub direction: Vec2,
}
enum Placed {
At { at: Vec2, pinned: Option<Vec2> },
Hidden,
}
#[allow(clippy::type_complexity)]
pub fn update_world_anchors(
mut commands: Commands,
ui_scale: Option<Res<UiScale>>,
cameras: Query<(&Camera, &GlobalTransform)>,
transforms: Query<&GlobalTransform>,
mut anchored: Query<(
Entity,
&WorldAnchor,
&mut Node,
&mut UiTransform,
&mut Visibility,
&ComputedNode,
Option<&ComputedUiTargetCamera>,
Option<&AnchorPinned>,
)>,
) {
let scale = ui_scale.map_or(1.0, |s| s.0).max(f32::EPSILON);
for (entity, anchor, mut node, mut transform, mut visibility, computed, ui_camera, pinned) in
&mut anchored
{
let placed = place(
anchor,
computed,
ui_camera.and_then(ComputedUiTargetCamera::get),
scale,
&cameras,
&transforms,
);
let (visible, pin) = match placed {
Placed::At { at, pinned } => {
if node.position_type != PositionType::Absolute {
node.position_type = PositionType::Absolute;
}
let (left, top) = (Val::Px(at.x / scale), Val::Px(at.y / scale));
if node.left != left {
node.left = left;
}
if node.top != top {
node.top = top;
}
let pivot = Val2::percent(-anchor.pivot.x * 100.0, -anchor.pivot.y * 100.0);
if transform.translation != pivot {
transform.translation = pivot;
}
(true, pinned)
}
Placed::Hidden => (false, None),
};
let want = if visible {
Visibility::Inherited
} else {
Visibility::Hidden
};
if *visibility != want {
*visibility = want;
}
let want_pin = pin.map(|direction| AnchorPinned { direction });
if pinned.copied() != want_pin {
match want_pin {
Some(pin) => {
commands.entity(entity).insert(pin);
}
None => {
commands.entity(entity).remove::<AnchorPinned>();
}
}
}
}
}
fn place(
anchor: &WorldAnchor,
computed: &ComputedNode,
ui_camera: Option<Entity>,
scale: f32,
cameras: &Query<(&Camera, &GlobalTransform)>,
transforms: &Query<&GlobalTransform>,
) -> Placed {
let Some(camera) = anchor.camera.or(ui_camera) else {
return Placed::Hidden;
};
let (Ok((camera, camera_at)), Ok(target_at)) =
(cameras.get(camera), transforms.get(anchor.target))
else {
return Placed::Hidden;
};
let Some(view) = camera.logical_viewport_rect() else {
return Placed::Hidden;
};
let world = target_at.translation() + anchor.offset;
let size = computed.size() * computed.inverse_scale_factor() * scale;
let pivot = anchor.pivot;
let projected = camera
.world_to_viewport(camera_at, world)
.ok()
.map(|p| p - view.min)
.filter(|p| p.is_finite());
let extent = view.size();
match anchor.off_screen {
OffScreen::Hide => match projected {
Some(at) => {
let min = at - pivot * size;
if box_outside(
(min.x, min.y),
(min.x + size.x, min.y + size.y),
(0.0, 0.0),
(extent.x, extent.y),
) {
Placed::Hidden
} else {
Placed::At { at, pinned: None }
}
}
None => Placed::Hidden,
},
OffScreen::Pin => {
let margin = extent * SAFE_EDGE_PERCENT / 100.0;
let lo = margin + pivot * size;
let hi = extent - margin - (Vec2::ONE - pivot) * size;
let centre = extent / 2.0;
let at = projected.unwrap_or_else(|| {
let local = camera_at.affine().inverse().transform_point3(world);
let away = Vec2::new(local.x, -local.y);
centre + away.try_normalize().unwrap_or(Vec2::NEG_Y) * extent.length()
});
let behind = projected.is_none();
let at_edge = pin_to_edge(
(at.x, at.y),
(centre.x, centre.y),
(lo.x, lo.y),
(hi.x, hi.y),
);
let at_edge = Vec2::new(at_edge.0, at_edge.1);
let inside = !behind && at_edge == at;
if inside {
Placed::At { at, pinned: None }
} else {
let direction = (at - centre).try_normalize().unwrap_or(Vec2::NEG_Y);
Placed::At {
at: at_edge,
pinned: Some(direction),
}
}
}
}
}
pub(crate) fn add_systems(app: &mut App) {
app.add_systems(
PostUpdate,
update_world_anchors
.after(TransformSystems::Propagate)
.after(CameraUpdateSystems)
.before(UiSystems::Prepare),
);
}