diff --git a/Cargo.toml b/Cargo.toml index 0331c5f..ee3480f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,10 @@ path = "examples/background_image.rs" name = "update_pixels" path = "examples/update_pixels.rs" +[[example]] +name = "transforms" +path = "examples/transforms.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 977cdc5..75803c4 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -211,6 +211,110 @@ pub extern "C" fn processing_no_stroke(window_id: u64) { error::check(|| graphics_record_command(window_entity, DrawCommand::NoStroke)); } +/// Push the current transformation matrix onto the stack. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_push_matrix(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::PushMatrix)); +} + +/// Pop the transformation matrix from the stack. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_pop_matrix(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::PopMatrix)); +} + +/// Reset the transformation matrix to identity. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_reset_matrix(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::ResetMatrix)); +} + +/// Translate the coordinate system. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_translate(window_id: u64, x: f32, y: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::Translate { x, y })); +} + +/// Rotate the coordinate system. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_rotate(window_id: u64, angle: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::Rotate { angle })); +} + +/// Scale the coordinate system. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_scale(window_id: u64, x: f32, y: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::Scale { x, y })); +} + +/// Shear along the X axis. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_shear_x(window_id: u64, angle: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::ShearX { angle })); +} + +/// Shear along the Y axis. +/// +/// SAFETY: +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_shear_y(window_id: u64, angle: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| graphics_record_command(window_entity, DrawCommand::ShearY { angle })); +} + /// Draw a rectangle. /// /// SAFETY: diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 55e0479..9e43ae0 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -123,6 +123,46 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn push_matrix(&self) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::PushMatrix) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn pop_matrix(&self) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::PopMatrix) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn reset_matrix(&self) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::ResetMatrix) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn translate(&self, x: f32, y: f32) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::Translate { x, y }) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn rotate(&self, angle: f32) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::Rotate { angle }) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn scale(&self, x: f32, y: f32) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::Scale { x, y }) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn shear_x(&self, angle: f32) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::ShearX { angle }) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn shear_y(&self, angle: f32) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::ShearY { angle }) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn begin_draw(&self) -> PyResult<()> { graphics_begin_draw(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index cae9f23..7e54931 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -29,7 +29,10 @@ use crate::{ Flush, error::{ProcessingError, Result}, image::{Image, bytes_to_pixels, create_readback_buffer, pixel_size, pixels_to_bytes}, - render::command::{CommandBuffer, DrawCommand}, + render::{ + RenderState, + command::{CommandBuffer, DrawCommand}, + }, surface::Surface, }; @@ -246,6 +249,7 @@ pub fn create( Transform::from_xyz(0.0, 0.0, 999.9), render_layer, CommandBuffer::new(), + RenderState::default(), SurfaceSize(width, height), Graphics { readback_buffer, @@ -307,9 +311,21 @@ pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { world.run_system_cached_with(destroy_inner, entity).unwrap() } -pub fn begin_draw(_app: &mut App, _entity: Entity) -> Result<()> { - // nothing to do here for now - Ok(()) +pub fn begin_draw(world: &mut World, entity: Entity) -> Result<()> { + fn begin_draw_inner( + In(entity): In, + mut state_query: Query<&mut RenderState>, + ) -> Result<()> { + let mut state = state_query + .get_mut(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + state.reset(); + Ok(()) + } + + world + .run_system_cached_with(begin_draw_inner, entity) + .unwrap() } pub fn flush(app: &mut App, entity: Entity) -> Result<()> { diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 330e8be..9ee5a04 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -228,8 +228,8 @@ pub fn graphics_create(surface_entity: Entity, width: u32, height: u32) -> error } /// Begin a new draw pass for the graphics surface. -pub fn graphics_begin_draw(_graphics_entity: Entity) -> error::Result<()> { - app_mut(|app| graphics::begin_draw(app, _graphics_entity)) +pub fn graphics_begin_draw(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| graphics::begin_draw(app.world_mut(), graphics_entity)) } /// Flush current pending draw commands to the graphics surface. diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index c7ee2b2..fe85d81 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -16,6 +16,26 @@ pub enum DrawCommand { h: f32, radii: [f32; 4], // [tl, tr, br, bl] }, + PushMatrix, + PopMatrix, + ResetMatrix, + Translate { + x: f32, + y: f32, + }, + Rotate { + angle: f32, + }, + Scale { + x: f32, + y: f32, + }, + ShearX { + angle: f32, + }, + ShearY { + angle: f32, + }, } #[derive(Debug, Default, Component)] diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 4687b14..a5f84ef 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -2,11 +2,15 @@ pub mod command; pub mod material; pub mod mesh_builder; pub mod primitive; +pub mod transform; -use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*}; +use bevy::{ + camera::visibility::RenderLayers, ecs::system::SystemParam, math::Affine3A, prelude::*, +}; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; +use transform::TransformStack; use crate::{Flush, graphics::SurfaceSize, image::Image, render::primitive::rect}; @@ -19,29 +23,40 @@ pub struct BelongsToGraphics(pub Entity); pub struct TransientMeshes(Vec); #[derive(SystemParam)] -pub struct RenderContext<'w, 's> { +pub struct RenderResources<'w, 's> { commands: Commands<'w, 's>, meshes: ResMut<'w, Assets>, materials: ResMut<'w, Assets>, - batch: Local<'s, BatchState>, - state: Local<'s, RenderState>, } -#[derive(Default)] struct BatchState { current_mesh: Option, material_key: Option, + transform: Affine3A, draw_index: u32, render_layers: RenderLayers, - graphics_entity: Option, + graphics_entity: Entity, } -#[derive(Debug)] +impl BatchState { + fn new(graphics_entity: Entity, render_layers: RenderLayers) -> Self { + Self { + current_mesh: None, + material_key: None, + transform: Affine3A::IDENTITY, + draw_index: 0, + render_layers, + graphics_entity, + } + } +} + +#[derive(Debug, Component)] pub struct RenderState { - // drawing state pub fill_color: Option, pub stroke_color: Option, pub stroke_weight: f32, + pub transform: TransformStack, } impl Default for RenderState { @@ -50,21 +65,14 @@ impl Default for RenderState { fill_color: Some(Color::WHITE), stroke_color: Some(Color::BLACK), stroke_weight: 1.0, + transform: TransformStack::new(), } } } impl RenderState { - pub fn new() -> Self { - Self::default() - } - - pub fn has_fill(&self) -> bool { - self.fill_color.is_some() - } - - pub fn has_stroke(&self) -> bool { - self.stroke_color.is_some() + pub fn reset(&mut self) { + *self = Self::default(); } pub fn fill_is_transparent(&self) -> bool { @@ -77,41 +85,48 @@ impl RenderState { } pub fn flush_draw_commands( - mut ctx: RenderContext, - mut graphics: Query<(Entity, &mut CommandBuffer, &RenderLayers, &SurfaceSize), With>, + mut res: RenderResources, + mut graphics: Query< + ( + Entity, + &mut CommandBuffer, + &mut RenderState, + &RenderLayers, + &SurfaceSize, + ), + With, + >, p_images: Query<&Image>, ) { - for (graphics_entity, mut cmd_buffer, render_layers, SurfaceSize(width, height)) in + for (graphics_entity, mut cmd_buffer, mut state, render_layers, SurfaceSize(width, height)) in graphics.iter_mut() { let draw_commands = std::mem::take(&mut cmd_buffer.commands); - ctx.batch.render_layers = render_layers.clone(); - ctx.batch.graphics_entity = Some(graphics_entity); - ctx.batch.draw_index = 0; // Reset draw index for each flush + let mut batch = BatchState::new(graphics_entity, render_layers.clone()); for cmd in draw_commands { match cmd { DrawCommand::Fill(color) => { - ctx.state.fill_color = Some(color); + state.fill_color = Some(color); } DrawCommand::NoFill => { - ctx.state.fill_color = None; + state.fill_color = None; } DrawCommand::StrokeColor(color) => { - ctx.state.stroke_color = Some(color); + state.stroke_color = Some(color); } DrawCommand::NoStroke => { - ctx.state.stroke_color = None; + state.stroke_color = None; } DrawCommand::StrokeWeight(weight) => { - ctx.state.stroke_weight = weight; + state.stroke_weight = weight; } DrawCommand::Rect { x, y, w, h, radii } => { - add_fill(&mut ctx, |mesh, color| { + add_fill(&mut res, &mut batch, &state, |mesh, color| { rect(mesh, x, y, w, h, radii, color, TessellationMode::Fill) }); - add_stroke(&mut ctx, |mesh, color, weight| { + add_stroke(&mut res, &mut batch, &state, |mesh, color, weight| { rect( mesh, x, @@ -125,7 +140,7 @@ pub fn flush_draw_commands( }); } DrawCommand::BackgroundColor(color) => { - add_fill(&mut ctx, |mesh, _| { + add_fill(&mut res, &mut batch, &state, |mesh, _| { rect( mesh, 0.0, @@ -144,20 +159,17 @@ pub fn flush_draw_commands( continue; }; - // force flush current batch before changing material - flush_batch(&mut ctx); + flush_batch(&mut res, &mut batch); let material_key = MaterialKey { transparent: false, background_image: Some(p_image.handle.clone()), }; - ctx.batch.material_key = Some(material_key); - ctx.batch.current_mesh = Some(empty_mesh()); + batch.material_key = Some(material_key); + batch.current_mesh = Some(empty_mesh()); - // we're reusing rect to draw the fullscreen quad but don't need to track - // a fill here and can just pass white manually - if let Some(ref mut mesh) = ctx.batch.current_mesh { + if let Some(ref mut mesh) = batch.current_mesh { rect( mesh, 0.0, @@ -170,12 +182,20 @@ pub fn flush_draw_commands( ) } - flush_batch(&mut ctx); + flush_batch(&mut res, &mut batch); } + DrawCommand::PushMatrix => state.transform.push(), + DrawCommand::PopMatrix => state.transform.pop(), + DrawCommand::ResetMatrix => state.transform.reset(), + DrawCommand::Translate { x, y } => state.transform.translate(x, y), + DrawCommand::Rotate { angle } => state.transform.rotate(angle), + DrawCommand::Scale { x, y } => state.transform.scale(x, y), + DrawCommand::ShearX { angle } => state.transform.shear_x(angle), + DrawCommand::ShearY { angle } => state.transform.shear_y(angle), } } - flush_batch(&mut ctx); + flush_batch(&mut res, &mut batch); } } @@ -196,77 +216,101 @@ pub fn clear_transient_meshes( } } -fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: f32) { - let Some(material_key) = &ctx.batch.material_key else { - return; - }; - let Some(surface_entity) = ctx.batch.graphics_entity else { +fn spawn_mesh(res: &mut RenderResources, batch: &mut BatchState, mesh: Mesh, z_offset: f32) { + let Some(material_key) = &batch.material_key else { return; }; - let mesh_handle = ctx.meshes.add(mesh); - let material_handle = ctx.materials.add(material_key.to_material()); + let mesh_handle = res.meshes.add(mesh); + let material_handle = res.materials.add(material_key.to_material()); + + let (scale, rotation, translation) = batch.transform.to_scale_rotation_translation(); + let transform = Transform { + translation: translation + Vec3::new(0.0, 0.0, z_offset), + rotation, + scale, + }; - ctx.commands.spawn(( + res.commands.spawn(( Mesh3d(mesh_handle), MeshMaterial3d(material_handle), - BelongsToGraphics(surface_entity), - Transform::from_xyz(0.0, 0.0, z_offset), - ctx.batch.render_layers.clone(), + BelongsToGraphics(batch.graphics_entity), + transform, + batch.render_layers.clone(), )); } -fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color)) { - let Some(color) = ctx.state.fill_color else { +fn needs_batch(batch: &BatchState, state: &RenderState, material_key: &MaterialKey) -> bool { + let current_transform = state.transform.current(); + let material_changed = batch.material_key.as_ref() != Some(material_key); + let transform_changed = batch.transform != current_transform; + material_changed || transform_changed +} + +fn start_batch( + res: &mut RenderResources, + batch: &mut BatchState, + state: &RenderState, + material_key: MaterialKey, +) { + flush_batch(res, batch); + batch.material_key = Some(material_key); + batch.transform = state.transform.current(); + batch.current_mesh = Some(empty_mesh()); +} + +fn add_fill( + res: &mut RenderResources, + batch: &mut BatchState, + state: &RenderState, + tessellate: impl FnOnce(&mut Mesh, Color), +) { + let Some(color) = state.fill_color else { return; }; let material_key = MaterialKey { - transparent: ctx.state.fill_is_transparent(), + transparent: state.fill_is_transparent(), background_image: None, }; - // when the material changes, flush the current batch - if ctx.batch.material_key.as_ref() != Some(&material_key) { - flush_batch(ctx); - ctx.batch.material_key = Some(material_key); - ctx.batch.current_mesh = Some(empty_mesh()); + if needs_batch(batch, state, &material_key) { + start_batch(res, batch, state, material_key); } - // accumulate geometry into the current mega mesh - if let Some(ref mut mesh) = ctx.batch.current_mesh { + if let Some(ref mut mesh) = batch.current_mesh { tessellate(mesh, color); } } -fn add_stroke(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color, f32)) { - let Some(color) = ctx.state.stroke_color else { +fn add_stroke( + res: &mut RenderResources, + batch: &mut BatchState, + state: &RenderState, + tessellate: impl FnOnce(&mut Mesh, Color, f32), +) { + let Some(color) = state.stroke_color else { return; }; - let stroke_weight = ctx.state.stroke_weight; + let stroke_weight = state.stroke_weight; let material_key = MaterialKey { - transparent: ctx.state.stroke_is_transparent(), + transparent: state.stroke_is_transparent(), background_image: None, }; - // when the material changes, flush the current batch - if ctx.batch.material_key.as_ref() != Some(&material_key) { - flush_batch(ctx); - ctx.batch.material_key = Some(material_key); - ctx.batch.current_mesh = Some(empty_mesh()); + if needs_batch(batch, state, &material_key) { + start_batch(res, batch, state, material_key); } - // accumulate geometry into the current mega mesh - if let Some(ref mut mesh) = ctx.batch.current_mesh { + if let Some(ref mut mesh) = batch.current_mesh { tessellate(mesh, color, stroke_weight); } } -fn flush_batch(ctx: &mut RenderContext) { - if let Some(mesh) = ctx.batch.current_mesh.take() { - // we defensively apply a small z-offset based on draw_index to preserve painter's algorithm - let z_offset = -(ctx.batch.draw_index as f32 * 0.001); - spawn_mesh(ctx, mesh, z_offset); - ctx.batch.draw_index += 1; +fn flush_batch(res: &mut RenderResources, batch: &mut BatchState) { + if let Some(mesh) = batch.current_mesh.take() { + let z_offset = -(batch.draw_index as f32 * 0.001); + spawn_mesh(res, batch, mesh, z_offset); + batch.draw_index += 1; } - ctx.batch.material_key = None; + batch.material_key = None; } diff --git a/crates/processing_render/src/render/transform.rs b/crates/processing_render/src/render/transform.rs new file mode 100644 index 0000000..2207553 --- /dev/null +++ b/crates/processing_render/src/render/transform.rs @@ -0,0 +1,198 @@ +use bevy::math::{Affine3A, Mat3, Quat, Vec3}; + +#[derive(Debug, Clone, Default)] +pub struct TransformStack { + current: Affine3A, + stack: Vec, +} + +impl TransformStack { + pub fn new() -> Self { + Self::default() + } + + pub fn current(&self) -> Affine3A { + self.current + } + + pub fn push(&mut self) { + self.stack.push(self.current); + } + + pub fn pop(&mut self) { + if let Some(t) = self.stack.pop() { + self.current = t; + } + } + + pub fn reset(&mut self) { + self.current = Affine3A::IDENTITY; + } + + pub fn clear(&mut self) { + self.current = Affine3A::IDENTITY; + self.stack.clear(); + } + + pub fn translate(&mut self, x: f32, y: f32) { + self.translate_3d(x, y, 0.0); + } + + pub fn rotate(&mut self, angle: f32) { + self.rotate_z(angle); + } + + pub fn scale_uniform(&mut self, s: f32) { + self.scale(s, s); + } + + pub fn scale(&mut self, sx: f32, sy: f32) { + self.scale_3d(sx, sy, 1.0); + } + + pub fn shear_x(&mut self, angle: f32) { + let shear = Affine3A::from_mat3(Mat3::from_cols( + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(angle.tan(), 1.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + )); + self.current *= shear; + } + + pub fn shear_y(&mut self, angle: f32) { + let shear = Affine3A::from_mat3(Mat3::from_cols( + Vec3::new(1.0, angle.tan(), 0.0), + Vec3::new(0.0, 1.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + )); + self.current *= shear; + } + + pub fn translate_3d(&mut self, x: f32, y: f32, z: f32) { + let t = Affine3A::from_translation(Vec3::new(x, y, z)); + self.current *= t; + } + + pub fn rotate_x(&mut self, angle: f32) { + let r = Affine3A::from_quat(Quat::from_rotation_x(angle)); + self.current *= r; + } + + pub fn rotate_y(&mut self, angle: f32) { + let r = Affine3A::from_quat(Quat::from_rotation_y(angle)); + self.current *= r; + } + + pub fn rotate_z(&mut self, angle: f32) { + let r = Affine3A::from_quat(Quat::from_rotation_z(angle)); + self.current *= r; + } + + pub fn rotate_axis(&mut self, angle: f32, axis: Vec3) { + let r = Affine3A::from_quat(Quat::from_axis_angle(axis.normalize(), angle)); + self.current *= r; + } + + pub fn scale_3d(&mut self, sx: f32, sy: f32, sz: f32) { + let s = Affine3A::from_scale(Vec3::new(sx, sy, sz)); + self.current *= s; + } + + pub fn apply(&mut self, transform: Affine3A) { + self.current *= transform; + } + + pub fn to_bevy_transform(&self) -> bevy::prelude::Transform { + let (scale, rotation, translation) = self.current.to_scale_rotation_translation(); + bevy::prelude::Transform { + translation, + rotation, + scale, + } + } + + pub fn transform_point(&self, point: Vec3) -> Vec3 { + self.current.transform_point3(point) + } + + pub fn transform_point_2d(&self, x: f32, y: f32) -> (f32, f32) { + let p = self.current.transform_point3(Vec3::new(x, y, 0.0)); + (p.x, p.y) + } +} + +#[cfg(test)] +mod tests { + use std::f32::consts::PI; + + use super::*; + + static EPSILON: f32 = 1e-5; + + fn approx_eq(a: f32, b: f32) -> bool { + (a - b).abs() < EPSILON + } + + #[test] + fn test_identity() { + let stack = TransformStack::new(); + let (x, y) = stack.transform_point_2d(10.0, 20.0); + assert!(approx_eq(x, 10.0)); + assert!(approx_eq(y, 20.0)); + } + + #[test] + fn test_translate() { + let mut stack = TransformStack::new(); + stack.translate(100.0, 50.0); + let (x, y) = stack.transform_point_2d(10.0, 20.0); + assert!(approx_eq(x, 110.0)); + assert!(approx_eq(y, 70.0)); + } + + #[test] + fn test_scale() { + let mut stack = TransformStack::new(); + stack.scale(2.0, 3.0); + let (x, y) = stack.transform_point_2d(10.0, 10.0); + assert!(approx_eq(x, 20.0)); + assert!(approx_eq(y, 30.0)); + } + + #[test] + fn test_rotate_90() { + let mut stack = TransformStack::new(); + stack.rotate(PI / 2.0); + let (x, y) = stack.transform_point_2d(10.0, 0.0); + assert!(approx_eq(x, 0.0)); + assert!(approx_eq(y, 10.0)); + } + + #[test] + fn test_push_pop() { + let mut stack = TransformStack::new(); + stack.translate(100.0, 100.0); + stack.push(); + stack.translate(50.0, 50.0); + + let (x, y) = stack.transform_point_2d(0.0, 0.0); + assert!(approx_eq(x, 150.0)); + assert!(approx_eq(y, 150.0)); + + stack.pop(); + + let (x, y) = stack.transform_point_2d(0.0, 0.0); + assert!(approx_eq(x, 100.0)); + assert!(approx_eq(y, 100.0)); + } + + #[test] + fn test_pop_empty_is_noop() { + let mut stack = TransformStack::new(); + stack.translate(50.0, 50.0); + stack.pop(); + let (x, y) = stack.transform_point_2d(0.0, 0.0); + assert!(approx_eq(x, 50.0)); + assert!(approx_eq(y, 50.0)); + } +} diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 3d7d4df..b74a1d0 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -140,6 +140,70 @@ pub fn js_rect( )) } +#[wasm_bindgen(js_name = "pushMatrix")] +pub fn js_push_matrix(surface_id: u64) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::PushMatrix, + )) +} + +#[wasm_bindgen(js_name = "popMatrix")] +pub fn js_pop_matrix(surface_id: u64) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::PopMatrix, + )) +} + +#[wasm_bindgen(js_name = "resetMatrix")] +pub fn js_reset_matrix(surface_id: u64) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::ResetMatrix, + )) +} + +#[wasm_bindgen(js_name = "translate")] +pub fn js_translate(surface_id: u64, x: f32, y: f32) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::Translate { x, y }, + )) +} + +#[wasm_bindgen(js_name = "rotate")] +pub fn js_rotate(surface_id: u64, angle: f32) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::Rotate { angle }, + )) +} + +#[wasm_bindgen(js_name = "scale")] +pub fn js_scale(surface_id: u64, x: f32, y: f32) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::Scale { x, y }, + )) +} + +#[wasm_bindgen(js_name = "shearX")] +pub fn js_shear_x(surface_id: u64, angle: f32) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::ShearX { angle }, + )) +} + +#[wasm_bindgen(js_name = "shearY")] +pub fn js_shear_y(surface_id: u64, angle: f32) -> Result<(), JsValue> { + check(graphics_record_command( + Entity::from_bits(surface_id), + DrawCommand::ShearY { angle }, + )) +} + #[wasm_bindgen(js_name = "createImage")] pub fn js_image_create(width: u32, height: u32, data: &[u8]) -> Result { use bevy::render::render_resource::{Extent3d, TextureFormat}; diff --git a/examples/transforms.rs b/examples/transforms.rs new file mode 100644 index 0000000..1e26a1c --- /dev/null +++ b/examples/transforms.rs @@ -0,0 +1,78 @@ +mod glfw; + +use std::f32::consts::PI; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + sketch().unwrap(); + exit(0).unwrap(); +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(400, 400)?; + init()?; + + let window_handle = glfw_ctx.get_window(); + let display_handle = glfw_ctx.get_display(); + let surface = surface_create(window_handle, display_handle, 400, 400, 1.0)?; + let graphics = graphics_create(surface, 400, 400)?; + + let mut t: f32 = 0.0; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.1, 0.1, 0.1)), + )?; + + for i in 0..4 { + for j in 0..4 { + graphics_record_command(graphics, DrawCommand::PushMatrix)?; + + graphics_record_command( + graphics, + DrawCommand::Translate { + x: 50.0 + j as f32 * 100.0, + y: 50.0 + i as f32 * 100.0, + }, + )?; + + let angle = t + (i + j) as f32 * PI / 8.0; + graphics_record_command(graphics, DrawCommand::Rotate { angle })?; + + let s = 0.8 + (t * 2.0 + (i * j) as f32).sin() * 0.2; + graphics_record_command(graphics, DrawCommand::Scale { x: s, y: s })?; + + let r = j as f32 / 3.0; + let g = i as f32 / 3.0; + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgb(r, g, 0.8)), + )?; + + graphics_record_command( + graphics, + DrawCommand::Rect { + x: -20.0, + y: -20.0, + w: 40.0, + h: 40.0, + radii: [0.0; 4], + }, + )?; + + graphics_record_command(graphics, DrawCommand::PopMatrix)?; + } + } + + graphics_end_draw(graphics)?; + t += 0.02; + } + + Ok(()) +}