use std::collections::HashMap;
use bevy::prelude::*;
use noesis_runtime::transforms::Composite3DFields;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform3DSpec {
pub center: [f32; 3],
pub rotation: [f32; 3],
pub scale: [f32; 3],
pub translate: [f32; 3],
}
impl Default for Transform3DSpec {
fn default() -> Self {
Self {
center: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
translate: [0.0, 0.0, 0.0],
}
}
}
impl Transform3DSpec {
#[must_use]
pub(crate) fn to_fields(self) -> Composite3DFields {
Composite3DFields {
center_x: self.center[0],
center_y: self.center[1],
center_z: self.center[2],
rotation_x: self.rotation[0],
rotation_y: self.rotation[1],
rotation_z: self.rotation[2],
scale_x: self.scale[0],
scale_y: self.scale[1],
scale_z: self.scale[2],
translate_x: self.translate[0],
translate_y: self.translate[1],
translate_z: self.translate[2],
}
}
#[must_use]
pub(crate) fn from_fields(f: Composite3DFields) -> Self {
Self {
center: [f.center_x, f.center_y, f.center_z],
rotation: [f.rotation_x, f.rotation_y, f.rotation_z],
scale: [f.scale_x, f.scale_y, f.scale_z],
translate: [f.translate_x, f.translate_y, f.translate_z],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Matrix3DSpec {
pub rows: [f32; 12],
}
impl Default for Matrix3DSpec {
fn default() -> Self {
Self {
#[rustfmt::skip]
rows: [
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
0.0, 0.0, 0.0,
],
}
}
}
impl Matrix3DSpec {
#[must_use]
pub fn from_rows(rows: [f32; 12]) -> Self {
Self { rows }
}
#[must_use]
pub fn from_mat4(m: [[f32; 4]; 4]) -> Self {
#[rustfmt::skip]
let rows = [
m[0][0], m[0][1], m[0][2],
m[1][0], m[1][1], m[1][2],
m[2][0], m[2][1], m[2][2],
m[3][0], m[3][1], m[3][2],
];
Self { rows }
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisTransform3D {
pub transforms: HashMap<String, Transform3DSpec>,
pub matrices: HashMap<String, Matrix3DSpec>,
}
impl NoesisTransform3D {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(mut self, name: impl Into<String>, spec: Transform3DSpec) -> Self {
self.transforms.insert(name.into(), spec);
self
}
#[must_use]
pub fn translate(mut self, name: impl Into<String>, x: f32, y: f32, z: f32) -> Self {
self.entry(name).translate = [x, y, z];
self
}
#[must_use]
pub fn scale(mut self, name: impl Into<String>, x: f32, y: f32, z: f32) -> Self {
self.entry(name).scale = [x, y, z];
self
}
#[must_use]
pub fn center(mut self, name: impl Into<String>, x: f32, y: f32, z: f32) -> Self {
self.entry(name).center = [x, y, z];
self
}
#[must_use]
pub fn rotate(mut self, name: impl Into<String>, x: f32, y: f32, z: f32) -> Self {
self.entry(name).rotation = [x, y, z];
self
}
#[must_use]
pub fn rotate_x(mut self, name: impl Into<String>, degrees: f32) -> Self {
self.entry(name).rotation[0] = degrees;
self
}
#[must_use]
pub fn rotate_y(mut self, name: impl Into<String>, degrees: f32) -> Self {
self.entry(name).rotation[1] = degrees;
self
}
#[must_use]
pub fn rotate_z(mut self, name: impl Into<String>, degrees: f32) -> Self {
self.entry(name).rotation[2] = degrees;
self
}
fn entry(&mut self, name: impl Into<String>) -> &mut Transform3DSpec {
self.transforms.entry(name.into()).or_default()
}
#[must_use]
pub fn matrix(mut self, name: impl Into<String>, spec: Matrix3DSpec) -> Self {
self.matrices.insert(name.into(), spec);
self
}
#[must_use]
pub fn matrix_rows(self, name: impl Into<String>, rows: [f32; 12]) -> Self {
self.matrix(name, Matrix3DSpec::from_rows(rows))
}
pub fn write(&mut self, name: impl Into<String>, spec: Transform3DSpec) {
self.transforms.insert(name.into(), spec);
}
pub fn set_translate(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
self.entry(name).translate = [x, y, z];
}
pub fn set_scale(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
self.entry(name).scale = [x, y, z];
}
pub fn set_center(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
self.entry(name).center = [x, y, z];
}
pub fn set_rotation(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
self.entry(name).rotation = [x, y, z];
}
pub fn set_rotation_x(&mut self, name: impl Into<String>, degrees: f32) {
self.entry(name).rotation[0] = degrees;
}
pub fn set_rotation_y(&mut self, name: impl Into<String>, degrees: f32) {
self.entry(name).rotation[1] = degrees;
}
pub fn set_rotation_z(&mut self, name: impl Into<String>, degrees: f32) {
self.entry(name).rotation[2] = degrees;
}
pub fn write_matrix(&mut self, name: impl Into<String>, spec: Matrix3DSpec) {
self.matrices.insert(name.into(), spec);
}
pub fn write_matrix_rows(&mut self, name: impl Into<String>, rows: [f32; 12]) {
self.matrices
.insert(name.into(), Matrix3DSpec::from_rows(rows));
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisTransform3DChanged {
pub view: Entity,
pub name: String,
pub spec: Transform3DSpec,
}
#[derive(Message, Debug, Clone)]
pub struct NoesisMatrixTransform3DChanged {
pub view: Entity,
pub name: String,
pub matrix: [f32; 12],
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_transform3d_bridge(
views: Query<(Entity, Ref<NoesisTransform3D>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisTransform3DChanged>,
mut matrix_changed: MessageWriter<NoesisMatrixTransform3DChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, transform) in &views {
if transform.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_transforms3d_for(entity, &transform.transforms);
state.apply_matrix_transforms3d_for(entity, &transform.matrices);
}
let names: Vec<&str> = transform.transforms.keys().map(String::as_str).collect();
for (name, spec) in state.poll_transforms3d_for(entity, &names) {
changed.write(NoesisTransform3DChanged {
view: entity,
name,
spec,
});
}
let matrix_names: Vec<&str> = transform.matrices.keys().map(String::as_str).collect();
for (name, matrix) in state.poll_matrix_transforms3d_for(entity, &matrix_names) {
matrix_changed.write(NoesisMatrixTransform3DChanged {
view: entity,
name,
matrix,
});
}
}
}
pub struct NoesisTransform3DPlugin;
impl Plugin for NoesisTransform3DPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisTransform3DChanged>()
.add_message::<NoesisMatrixTransform3DChanged>()
.add_systems(PostUpdate, sync_transform3d_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_merges_fields_per_name() {
let t = NoesisTransform3D::new()
.translate("A", 10.0, 20.0, 30.0)
.scale("A", 2.0, 3.0, 4.0)
.rotate_y("A", 45.0)
.rotate_x("A", 15.0)
.translate("B", 1.0, 2.0, 3.0);
let a = t.transforms.get("A").copied().unwrap();
assert_eq!(a.translate, [10.0, 20.0, 30.0]);
assert_eq!(a.scale, [2.0, 3.0, 4.0]);
assert_eq!(a.rotation, [15.0, 45.0, 0.0]);
assert_eq!(a.center, [0.0, 0.0, 0.0]);
let b = t.transforms.get("B").copied().unwrap();
assert_eq!(b.translate, [1.0, 2.0, 3.0]);
assert_eq!(b.scale, [1.0, 1.0, 1.0]);
assert_eq!(b.rotation, [0.0, 0.0, 0.0]);
}
#[test]
fn rotate_sets_all_three_axes() {
let t = NoesisTransform3D::new().rotate("C", 10.0, 20.0, 30.0);
let c = t.transforms.get("C").copied().unwrap();
assert_eq!(c.rotation, [10.0, 20.0, 30.0]);
}
#[test]
fn matrix_builder_queues_per_name() {
#[rustfmt::skip]
let rows = [
2.0, 0.0, 0.0,
0.0, 3.0, 0.0,
0.0, 0.0, 4.0,
5.0, 6.0, 7.0,
];
let t = NoesisTransform3D::new()
.matrix_rows("A", rows)
.matrix("B", Matrix3DSpec::default());
assert_eq!(t.matrices.get("A").unwrap().rows, rows);
assert_eq!(t.matrices.get("B").copied(), Some(Matrix3DSpec::default()));
assert!(t.transforms.is_empty());
}
#[test]
fn matrix_from_mat4_drops_projective_column() {
let m = [
[2.0, 0.0, 0.0, 0.0],
[0.0, 3.0, 0.0, 0.0],
[0.0, 0.0, 4.0, 0.0],
[5.0, 6.0, 7.0, 1.0],
];
#[rustfmt::skip]
let expected = [
2.0, 0.0, 0.0,
0.0, 3.0, 0.0,
0.0, 0.0, 4.0,
5.0, 6.0, 7.0,
];
assert_eq!(Matrix3DSpec::from_mat4(m).rows, expected);
}
#[test]
fn fields_round_trip() {
let spec = Transform3DSpec {
center: [7.0, 8.0, 9.0],
rotation: [30.0, -15.0, 5.0],
scale: [2.0, 0.5, 1.5],
translate: [5.0, 6.0, -7.0],
};
assert_eq!(Transform3DSpec::from_fields(spec.to_fields()), spec);
}
}