use bevy::prelude::*;
pub struct UiShapePlugin;
impl Plugin for UiShapePlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, update_ui_shape_positions);
}
}
#[derive(Component)]
pub struct UiShapeMarker {
pub offset: Vec2,
}
#[derive(Clone, Debug)]
pub struct ShapePath {
pub vertices: Vec<Vec2>,
pub closed: bool,
}
impl ShapePath {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
closed: false,
}
}
pub fn move_to(mut self, point: Vec2) -> Self {
self.vertices.clear();
self.vertices.push(point);
self
}
pub fn line_to(mut self, point: Vec2) -> Self {
self.vertices.push(point);
self
}
pub fn close(mut self) -> Self {
self.closed = true;
self
}
pub fn regular_polygon(sides: u32, radius: f32, center: Vec2) -> Self {
use std::f32::consts::PI;
let mut path = Self::new();
for i in 0..sides {
let angle = (i as f32) * 2.0 * PI / (sides as f32) - PI / 2.0;
let x = center.x + radius * angle.cos();
let y = center.y + radius * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
path.closed = true;
path
}
pub fn star(points: u32, outer_radius: f32, inner_radius: f32, center: Vec2) -> Self {
use std::f32::consts::PI;
let mut path = Self::new();
for i in 0..(points * 2) {
let angle = (i as f32) * PI / (points as f32) - PI / 2.0;
let radius = if i % 2 == 0 { outer_radius } else { inner_radius };
let x = center.x + radius * angle.cos();
let y = center.y + radius * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
path.closed = true;
path
}
pub fn ellipse(radii: Vec2, center: Vec2, segments: u32) -> Self {
use std::f32::consts::PI;
let mut path = Self::new();
for i in 0..segments {
let angle = (i as f32) * 2.0 * PI / (segments as f32);
let x = center.x + radii.x * angle.cos();
let y = center.y + radii.y * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
path.closed = true;
path
}
pub fn rounded_rect(size: Vec2, radius: f32, center: Vec2, segments_per_corner: u32) -> Self {
use std::f32::consts::PI;
let mut path = Self::new();
let half_w = size.x / 2.0;
let half_h = size.y / 2.0;
let r = radius.min(half_w).min(half_h);
for i in 0..segments_per_corner {
let angle = (i as f32) * PI / 2.0 / (segments_per_corner as f32);
let x = center.x + half_w - r + r * angle.cos();
let y = center.y - half_h + r - r * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
for i in 0..segments_per_corner {
let angle = PI / 2.0 + (i as f32) * PI / 2.0 / (segments_per_corner as f32);
let x = center.x + half_w - r + r * angle.cos();
let y = center.y + half_h - r - r * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
for i in 0..segments_per_corner {
let angle = PI + (i as f32) * PI / 2.0 / (segments_per_corner as f32);
let x = center.x - half_w + r + r * angle.cos();
let y = center.y + half_h - r - r * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
for i in 0..segments_per_corner {
let angle = 3.0 * PI / 2.0 + (i as f32) * PI / 2.0 / (segments_per_corner as f32);
let x = center.x - half_w + r + r * angle.cos();
let y = center.y - half_h + r - r * angle.sin();
path.vertices.push(Vec2::new(x, y));
}
path.closed = true;
path
}
pub fn tessellate(&self) -> Mesh {
let mut mesh = Mesh::new(
bevy::render::mesh::PrimitiveTopology::TriangleList,
bevy::render::render_asset::RenderAssetUsages::default(),
);
if self.vertices.len() < 3 {
return mesh;
}
let indices = self.triangulate_ear_clipping();
let positions: Vec<[f32; 3]> = self.vertices
.iter()
.map(|v| [v.x, v.y, 0.0])
.collect();
let normals: Vec<[f32; 3]> = vec![[0.0, 0.0, 1.0]; positions.len()];
let uvs: Vec<[f32; 2]> = self.vertices
.iter()
.map(|v| [v.x, v.y])
.collect();
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
mesh.insert_indices(bevy::render::mesh::Indices::U32(indices));
mesh
}
fn triangulate_ear_clipping(&self) -> Vec<u32> {
let mut indices = Vec::new();
if self.vertices.len() < 3 {
return indices;
}
let n = self.vertices.len();
for i in 1..(n - 1) {
indices.push(0);
indices.push(i as u32);
indices.push((i + 1) as u32);
}
indices
}
}
impl Default for ShapePath {
fn default() -> Self {
Self::new()
}
}
fn update_ui_shape_positions(
ui_nodes: Query<(&Node, &GlobalTransform), Without<UiShapeMarker>>,
mut ui_shapes: Query<(&UiShapeMarker, &ChildOf, &mut Transform)>,
cameras: Query<(&Camera, &GlobalTransform)>,
) {
let Ok((camera, camera_transform)) = cameras.get_single() else {
return;
};
for (shape_marker, parent, mut transform) in ui_shapes.iter_mut() {
if let Ok((node, global_transform)) = ui_nodes.get(parent.get()) {
let ui_pos = global_transform.translation();
if let Some(viewport_size) = camera.logical_viewport_size() {
let world_x = ui_pos.x - viewport_size.x / 2.0 + shape_marker.offset.x;
let world_y = -ui_pos.y + viewport_size.y / 2.0 + shape_marker.offset.y;
transform.translation.x = world_x;
transform.translation.y = world_y;
transform.translation.z = ui_pos.z + 1.0; }
}
}
}
pub struct UiShapeBuilder {
path: ShapePath,
color: Color,
offset: Vec2,
}
impl UiShapeBuilder {
pub fn new(path: ShapePath) -> Self {
Self {
path,
color: Color::WHITE,
offset: Vec2::ZERO,
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_offset(mut self, offset: Vec2) -> Self {
self.offset = offset;
self
}
pub fn build(
self,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<ColorMaterial>,
) -> impl Bundle {
let mesh = self.path.tessellate();
let mesh_handle = meshes.add(mesh);
let material_handle = materials.add(ColorMaterial::from(self.color));
(
UiShapeMarker {
offset: self.offset,
},
mesh_handle.clone(),
material_handle.clone(),
Transform::from_xyz(0.0, 0.0, 1.0),
Visibility::default(),
)
}
}