use std::collections::HashMap;
use bevy::prelude::*;
use noesis_runtime::transforms::CompositeFields;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TransformSpec {
pub translate: [f32; 2],
pub scale: [f32; 2],
pub rotation: f32,
pub center: [f32; 2],
pub skew: [f32; 2],
}
impl Default for TransformSpec {
fn default() -> Self {
Self {
translate: [0.0, 0.0],
scale: [1.0, 1.0],
rotation: 0.0,
center: [0.0, 0.0],
skew: [0.0, 0.0],
}
}
}
impl TransformSpec {
#[must_use]
pub(crate) fn to_fields(self) -> CompositeFields {
CompositeFields {
center_x: self.center[0],
center_y: self.center[1],
scale_x: self.scale[0],
scale_y: self.scale[1],
skew_x: self.skew[0],
skew_y: self.skew[1],
rotation: self.rotation,
translate_x: self.translate[0],
translate_y: self.translate[1],
}
}
#[must_use]
pub(crate) fn from_fields(f: CompositeFields) -> Self {
Self {
translate: [f.translate_x, f.translate_y],
scale: [f.scale_x, f.scale_y],
rotation: f.rotation,
center: [f.center_x, f.center_y],
skew: [f.skew_x, f.skew_y],
}
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisTransform {
pub transforms: HashMap<String, TransformSpec>,
}
impl NoesisTransform {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(mut self, name: impl Into<String>, spec: TransformSpec) -> Self {
self.transforms.insert(name.into(), spec);
self
}
#[must_use]
pub fn translate(mut self, name: impl Into<String>, x: f32, y: f32) -> Self {
self.entry(name).translate = [x, y];
self
}
#[must_use]
pub fn scale(mut self, name: impl Into<String>, x: f32, y: f32) -> Self {
self.entry(name).scale = [x, y];
self
}
#[must_use]
pub fn rotate(mut self, name: impl Into<String>, degrees: f32) -> Self {
self.entry(name).rotation = degrees;
self
}
#[must_use]
pub fn center(mut self, name: impl Into<String>, x: f32, y: f32) -> Self {
self.entry(name).center = [x, y];
self
}
#[must_use]
pub fn skew(mut self, name: impl Into<String>, x: f32, y: f32) -> Self {
self.entry(name).skew = [x, y];
self
}
pub fn write(&mut self, name: impl Into<String>, spec: TransformSpec) {
self.transforms.insert(name.into(), spec);
}
pub fn set_translate(&mut self, name: impl Into<String>, x: f32, y: f32) {
self.entry(name).translate = [x, y];
}
pub fn set_scale(&mut self, name: impl Into<String>, x: f32, y: f32) {
self.entry(name).scale = [x, y];
}
pub fn set_rotation(&mut self, name: impl Into<String>, degrees: f32) {
self.entry(name).rotation = degrees;
}
pub fn set_center(&mut self, name: impl Into<String>, x: f32, y: f32) {
self.entry(name).center = [x, y];
}
pub fn set_skew(&mut self, name: impl Into<String>, x: f32, y: f32) {
self.entry(name).skew = [x, y];
}
fn entry(&mut self, name: impl Into<String>) -> &mut TransformSpec {
self.transforms.entry(name.into()).or_default()
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisTransformChanged {
pub view: Entity,
pub name: String,
pub spec: TransformSpec,
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_transform_bridge(
views: Query<(Entity, Ref<NoesisTransform>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisTransformChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, transform) in &views {
if transform.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_transforms_for(entity, &transform.transforms);
}
let names: Vec<&str> = transform.transforms.keys().map(String::as_str).collect();
for (name, spec) in state.poll_transforms_for(entity, &names) {
changed.write(NoesisTransformChanged {
view: entity,
name,
spec,
});
}
}
}
pub struct NoesisTransformPlugin;
impl Plugin for NoesisTransformPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisTransformChanged>()
.add_systems(PostUpdate, sync_transform_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_merges_fields_per_name() {
let t = NoesisTransform::new()
.translate("A", 10.0, 20.0)
.scale("A", 2.0, 3.0)
.rotate("A", 45.0)
.translate("B", 1.0, 2.0);
let a = t.transforms.get("A").copied().unwrap();
assert_eq!(a.translate, [10.0, 20.0]);
assert_eq!(a.scale, [2.0, 3.0]);
assert_eq!(a.rotation, 45.0);
assert_eq!(a.center, [0.0, 0.0]);
assert_eq!(a.skew, [0.0, 0.0]);
let b = t.transforms.get("B").copied().unwrap();
assert_eq!(b.translate, [1.0, 2.0]);
assert_eq!(b.scale, [1.0, 1.0]);
}
#[test]
fn fields_round_trip() {
let spec = TransformSpec {
translate: [5.0, 6.0],
scale: [2.0, 0.5],
rotation: 30.0,
center: [7.0, 8.0],
skew: [1.0, -1.0],
};
assert_eq!(TransformSpec::from_fields(spec.to_fields()), spec);
}
}