use std::collections::HashMap;
use bevy::prelude::*;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ShapeKind {
Rectangle {
width: f32,
height: f32,
radius_x: f32,
radius_y: f32,
},
Ellipse {
width: f32,
height: f32,
},
Line {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShapeSpec {
pub kind: ShapeKind,
pub fill: Option<[f32; 4]>,
pub stroke: Option<[f32; 4]>,
pub stroke_thickness: Option<f32>,
}
impl ShapeSpec {
#[must_use]
pub fn new(kind: ShapeKind) -> Self {
Self {
kind,
fill: None,
stroke: None,
stroke_thickness: None,
}
}
#[must_use]
pub fn with_fill(mut self, rgba: [f32; 4]) -> Self {
self.fill = Some(rgba);
self
}
#[must_use]
pub fn with_stroke(mut self, rgba: [f32; 4]) -> Self {
self.stroke = Some(rgba);
self
}
#[must_use]
pub fn with_stroke_thickness(mut self, thickness: f32) -> Self {
self.stroke_thickness = Some(thickness);
self
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisShapes {
pub shapes: HashMap<String, ShapeSpec>,
}
impl NoesisShapes {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn insert(mut self, name: impl Into<String>, spec: ShapeSpec) -> Self {
self.shapes.insert(name.into(), spec);
self
}
#[must_use]
pub fn rectangle(self, name: impl Into<String>, width: f32, height: f32) -> Self {
self.insert(
name,
ShapeSpec::new(ShapeKind::Rectangle {
width,
height,
radius_x: 0.0,
radius_y: 0.0,
}),
)
}
#[must_use]
pub fn rounded_rectangle(
self,
name: impl Into<String>,
width: f32,
height: f32,
radius_x: f32,
radius_y: f32,
) -> Self {
self.insert(
name,
ShapeSpec::new(ShapeKind::Rectangle {
width,
height,
radius_x,
radius_y,
}),
)
}
#[must_use]
pub fn ellipse(self, name: impl Into<String>, width: f32, height: f32) -> Self {
self.insert(name, ShapeSpec::new(ShapeKind::Ellipse { width, height }))
}
#[must_use]
pub fn line(self, name: impl Into<String>, x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
self.insert(name, ShapeSpec::new(ShapeKind::Line { x1, y1, x2, y2 }))
}
pub fn set(&mut self, name: impl Into<String>, spec: ShapeSpec) {
self.shapes.insert(name.into(), spec);
}
pub fn set_rectangle(&mut self, name: impl Into<String>, width: f32, height: f32) {
self.set(
name,
ShapeSpec::new(ShapeKind::Rectangle {
width,
height,
radius_x: 0.0,
radius_y: 0.0,
}),
);
}
pub fn set_rounded_rectangle(
&mut self,
name: impl Into<String>,
width: f32,
height: f32,
radius_x: f32,
radius_y: f32,
) {
self.set(
name,
ShapeSpec::new(ShapeKind::Rectangle {
width,
height,
radius_x,
radius_y,
}),
);
}
pub fn set_ellipse(&mut self, name: impl Into<String>, width: f32, height: f32) {
self.set(name, ShapeSpec::new(ShapeKind::Ellipse { width, height }));
}
pub fn set_line(&mut self, name: impl Into<String>, x1: f32, y1: f32, x2: f32, y2: f32) {
self.set(name, ShapeSpec::new(ShapeKind::Line { x1, y1, x2, y2 }));
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_shapes_bridge(
views: Query<(Entity, Ref<NoesisShapes>)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, shapes) in &views {
if shapes.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_shapes_for(entity, &shapes.shapes);
}
}
}
pub struct NoesisShapesPlugin;
impl Plugin for NoesisShapesPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, sync_shapes_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_shapes() {
let s = NoesisShapes::new()
.rectangle("Host", 40.0, 24.0)
.ellipse("Dot", 8.0, 8.0)
.line("Edge", 0.0, 0.0, 10.0, 5.0);
assert_eq!(
s.shapes.get("Host"),
Some(&ShapeSpec::new(ShapeKind::Rectangle {
width: 40.0,
height: 24.0,
radius_x: 0.0,
radius_y: 0.0,
})),
);
assert_eq!(
s.shapes.get("Dot"),
Some(&ShapeSpec::new(ShapeKind::Ellipse {
width: 8.0,
height: 8.0,
})),
);
assert_eq!(
s.shapes.get("Edge"),
Some(&ShapeSpec::new(ShapeKind::Line {
x1: 0.0,
y1: 0.0,
x2: 10.0,
y2: 5.0,
})),
);
}
#[test]
fn spec_builders_attach_paint() {
let spec = ShapeSpec::new(ShapeKind::Ellipse {
width: 4.0,
height: 4.0,
})
.with_fill([1.0, 0.0, 0.0, 1.0])
.with_stroke([0.0, 1.0, 0.0, 1.0])
.with_stroke_thickness(2.0);
assert_eq!(spec.fill, Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(spec.stroke, Some([0.0, 1.0, 0.0, 1.0]));
assert_eq!(spec.stroke_thickness, Some(2.0));
}
}