Skip to content

Commit e74abf0

Browse files
committed
Transform stack
1 parent 982c4b2 commit e74abf0

File tree

8 files changed

+555
-15
lines changed

8 files changed

+555
-15
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ path = "examples/background_image.rs"
4646
name = "update_pixels"
4747
path = "examples/update_pixels.rs"
4848

49+
[[example]]
50+
name = "transforms"
51+
path = "examples/transforms.rs"
52+
4953
[profile.wasm-release]
5054
inherits = "release"
5155
opt-level = "z"

crates/processing_ffi/src/lib.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,110 @@ pub extern "C" fn processing_no_stroke(window_id: u64) {
211211
error::check(|| graphics_record_command(window_entity, DrawCommand::NoStroke));
212212
}
213213

214+
/// Push the current transformation matrix onto the stack.
215+
///
216+
/// SAFETY:
217+
/// - Init and surface_create have been called.
218+
/// - window_id is a valid ID returned from surface_create.
219+
/// - This is called from the same thread as init.
220+
#[unsafe(no_mangle)]
221+
pub extern "C" fn processing_push_matrix(window_id: u64) {
222+
error::clear_error();
223+
let window_entity = Entity::from_bits(window_id);
224+
error::check(|| graphics_record_command(window_entity, DrawCommand::PushMatrix));
225+
}
226+
227+
/// Pop the transformation matrix from the stack.
228+
///
229+
/// SAFETY:
230+
/// - Init and surface_create have been called.
231+
/// - window_id is a valid ID returned from surface_create.
232+
/// - This is called from the same thread as init.
233+
#[unsafe(no_mangle)]
234+
pub extern "C" fn processing_pop_matrix(window_id: u64) {
235+
error::clear_error();
236+
let window_entity = Entity::from_bits(window_id);
237+
error::check(|| graphics_record_command(window_entity, DrawCommand::PopMatrix));
238+
}
239+
240+
/// Reset the transformation matrix to identity.
241+
///
242+
/// SAFETY:
243+
/// - Init and surface_create have been called.
244+
/// - window_id is a valid ID returned from surface_create.
245+
/// - This is called from the same thread as init.
246+
#[unsafe(no_mangle)]
247+
pub extern "C" fn processing_reset_matrix(window_id: u64) {
248+
error::clear_error();
249+
let window_entity = Entity::from_bits(window_id);
250+
error::check(|| graphics_record_command(window_entity, DrawCommand::ResetMatrix));
251+
}
252+
253+
/// Translate the coordinate system.
254+
///
255+
/// SAFETY:
256+
/// - Init and surface_create have been called.
257+
/// - window_id is a valid ID returned from surface_create.
258+
/// - This is called from the same thread as init.
259+
#[unsafe(no_mangle)]
260+
pub extern "C" fn processing_translate(window_id: u64, x: f32, y: f32) {
261+
error::clear_error();
262+
let window_entity = Entity::from_bits(window_id);
263+
error::check(|| graphics_record_command(window_entity, DrawCommand::Translate { x, y }));
264+
}
265+
266+
/// Rotate the coordinate system.
267+
///
268+
/// SAFETY:
269+
/// - Init and surface_create have been called.
270+
/// - window_id is a valid ID returned from surface_create.
271+
/// - This is called from the same thread as init.
272+
#[unsafe(no_mangle)]
273+
pub extern "C" fn processing_rotate(window_id: u64, angle: f32) {
274+
error::clear_error();
275+
let window_entity = Entity::from_bits(window_id);
276+
error::check(|| graphics_record_command(window_entity, DrawCommand::Rotate { angle }));
277+
}
278+
279+
/// Scale the coordinate system.
280+
///
281+
/// SAFETY:
282+
/// - Init and surface_create have been called.
283+
/// - window_id is a valid ID returned from surface_create.
284+
/// - This is called from the same thread as init.
285+
#[unsafe(no_mangle)]
286+
pub extern "C" fn processing_scale(window_id: u64, x: f32, y: f32) {
287+
error::clear_error();
288+
let window_entity = Entity::from_bits(window_id);
289+
error::check(|| graphics_record_command(window_entity, DrawCommand::Scale { x, y }));
290+
}
291+
292+
/// Shear along the X axis.
293+
///
294+
/// SAFETY:
295+
/// - Init and surface_create have been called.
296+
/// - window_id is a valid ID returned from surface_create.
297+
/// - This is called from the same thread as init.
298+
#[unsafe(no_mangle)]
299+
pub extern "C" fn processing_shear_x(window_id: u64, angle: f32) {
300+
error::clear_error();
301+
let window_entity = Entity::from_bits(window_id);
302+
error::check(|| graphics_record_command(window_entity, DrawCommand::ShearX { angle }));
303+
}
304+
305+
/// Shear along the Y axis.
306+
///
307+
/// SAFETY:
308+
/// - Init and surface_create have been called.
309+
/// - window_id is a valid ID returned from surface_create.
310+
/// - This is called from the same thread as init.
311+
#[unsafe(no_mangle)]
312+
pub extern "C" fn processing_shear_y(window_id: u64, angle: f32) {
313+
error::clear_error();
314+
let window_entity = Entity::from_bits(window_id);
315+
error::check(|| graphics_record_command(window_entity, DrawCommand::ShearY { angle }));
316+
}
317+
214318
/// Draw a rectangle.
215319
///
216320
/// SAFETY:

crates/processing_pyo3/src/graphics.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,46 @@ impl Graphics {
123123
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
124124
}
125125

126+
pub fn push_matrix(&self) -> PyResult<()> {
127+
graphics_record_command(self.entity, DrawCommand::PushMatrix)
128+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
129+
}
130+
131+
pub fn pop_matrix(&self) -> PyResult<()> {
132+
graphics_record_command(self.entity, DrawCommand::PopMatrix)
133+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
134+
}
135+
136+
pub fn reset_matrix(&self) -> PyResult<()> {
137+
graphics_record_command(self.entity, DrawCommand::ResetMatrix)
138+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
139+
}
140+
141+
pub fn translate(&self, x: f32, y: f32) -> PyResult<()> {
142+
graphics_record_command(self.entity, DrawCommand::Translate { x, y })
143+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
144+
}
145+
146+
pub fn rotate(&self, angle: f32) -> PyResult<()> {
147+
graphics_record_command(self.entity, DrawCommand::Rotate { angle })
148+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
149+
}
150+
151+
pub fn scale(&self, x: f32, y: f32) -> PyResult<()> {
152+
graphics_record_command(self.entity, DrawCommand::Scale { x, y })
153+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
154+
}
155+
156+
pub fn shear_x(&self, angle: f32) -> PyResult<()> {
157+
graphics_record_command(self.entity, DrawCommand::ShearX { angle })
158+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
159+
}
160+
161+
pub fn shear_y(&self, angle: f32) -> PyResult<()> {
162+
graphics_record_command(self.entity, DrawCommand::ShearY { angle })
163+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
164+
}
165+
126166
pub fn begin_draw(&self) -> PyResult<()> {
127167
graphics_begin_draw(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
128168
}

crates/processing_render/src/render/command.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ pub enum DrawCommand {
1616
h: f32,
1717
radii: [f32; 4], // [tl, tr, br, bl]
1818
},
19+
PushMatrix,
20+
PopMatrix,
21+
ResetMatrix,
22+
Translate { x: f32, y: f32 },
23+
Rotate { angle: f32 },
24+
Scale { x: f32, y: f32 },
25+
ShearX { angle: f32 },
26+
ShearY { angle: f32 },
1927
}
2028

2129
#[derive(Debug, Default, Component)]

crates/processing_render/src/render/mod.rs

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ pub mod command;
22
pub mod material;
33
pub mod mesh_builder;
44
pub mod primitive;
5+
pub mod transform;
56

6-
use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*};
7+
use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, math::Affine3A, prelude::*};
78
use command::{CommandBuffer, DrawCommand};
89
use material::MaterialKey;
910
use primitive::{TessellationMode, empty_mesh};
11+
use transform::TransformStack;
1012

1113
use crate::{Flush, graphics::SurfaceSize, image::Image, render::primitive::rect};
1214

@@ -27,21 +29,36 @@ pub struct RenderContext<'w, 's> {
2729
state: Local<'s, RenderState>,
2830
}
2931

30-
#[derive(Default)]
3132
struct BatchState {
3233
current_mesh: Option<Mesh>,
3334
material_key: Option<MaterialKey>,
35+
transform: Affine3A,
3436
draw_index: u32,
3537
render_layers: RenderLayers,
3638
graphics_entity: Option<Entity>,
3739
}
3840

41+
impl Default for BatchState {
42+
fn default() -> Self {
43+
Self {
44+
current_mesh: None,
45+
material_key: None,
46+
transform: Affine3A::IDENTITY,
47+
draw_index: 0,
48+
render_layers: RenderLayers::default(),
49+
graphics_entity: None,
50+
}
51+
}
52+
}
53+
3954
#[derive(Debug)]
4055
pub struct RenderState {
4156
// drawing state
4257
pub fill_color: Option<Color>,
4358
pub stroke_color: Option<Color>,
4459
pub stroke_weight: f32,
60+
// transform state
61+
pub transform: TransformStack,
4562
}
4663

4764
impl Default for RenderState {
@@ -50,6 +67,7 @@ impl Default for RenderState {
5067
fill_color: Some(Color::WHITE),
5168
stroke_color: Some(Color::BLACK),
5269
stroke_weight: 1.0,
70+
transform: TransformStack::new(),
5371
}
5472
}
5573
}
@@ -172,6 +190,26 @@ pub fn flush_draw_commands(
172190

173191
flush_batch(&mut ctx);
174192
}
193+
DrawCommand::PushMatrix => ctx.state.transform.push(),
194+
DrawCommand::PopMatrix => ctx.state.transform.pop(),
195+
DrawCommand::ResetMatrix => {
196+
ctx.state.transform.reset();
197+
}
198+
DrawCommand::Translate { x, y } => {
199+
ctx.state.transform.translate(x, y);
200+
}
201+
DrawCommand::Rotate { angle } => {
202+
ctx.state.transform.rotate(angle);
203+
}
204+
DrawCommand::Scale { x, y } => {
205+
ctx.state.transform.scale(x, y);
206+
}
207+
DrawCommand::ShearX { angle } => {
208+
ctx.state.transform.shear_x(angle);
209+
}
210+
DrawCommand::ShearY { angle } => {
211+
ctx.state.transform.shear_y(angle);
212+
}
175213
}
176214
}
177215

@@ -207,15 +245,38 @@ fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: f32) {
207245
let mesh_handle = ctx.meshes.add(mesh);
208246
let material_handle = ctx.materials.add(material_key.to_material());
209247

248+
let (scale, rotation, translation) = ctx.batch.transform.to_scale_rotation_translation();
249+
let transform = Transform {
250+
translation: translation + Vec3::new(0.0, 0.0, z_offset),
251+
rotation,
252+
scale,
253+
};
254+
210255
ctx.commands.spawn((
211256
Mesh3d(mesh_handle),
212257
MeshMaterial3d(material_handle),
213258
BelongsToGraphics(surface_entity),
214-
Transform::from_xyz(0.0, 0.0, z_offset),
259+
transform,
215260
ctx.batch.render_layers.clone(),
216261
));
217262
}
218263

264+
fn maybe_start_batch(ctx: &mut RenderContext, material_key: MaterialKey) -> bool {
265+
let current_transform = ctx.state.transform.current();
266+
let material_changed = ctx.batch.material_key.as_ref() != Some(&material_key);
267+
let transform_changed = ctx.batch.transform != current_transform;
268+
269+
if material_changed || transform_changed {
270+
flush_batch(ctx);
271+
ctx.batch.material_key = Some(material_key);
272+
ctx.batch.transform = current_transform;
273+
ctx.batch.current_mesh = Some(empty_mesh());
274+
true
275+
} else {
276+
false
277+
}
278+
}
279+
219280
fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color)) {
220281
let Some(color) = ctx.state.fill_color else {
221282
return;
@@ -225,12 +286,7 @@ fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color))
225286
background_image: None,
226287
};
227288

228-
// when the material changes, flush the current batch
229-
if ctx.batch.material_key.as_ref() != Some(&material_key) {
230-
flush_batch(ctx);
231-
ctx.batch.material_key = Some(material_key);
232-
ctx.batch.current_mesh = Some(empty_mesh());
233-
}
289+
maybe_start_batch(ctx, material_key);
234290

235291
// accumulate geometry into the current mega mesh
236292
if let Some(ref mut mesh) = ctx.batch.current_mesh {
@@ -248,12 +304,7 @@ fn add_stroke(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color,
248304
background_image: None,
249305
};
250306

251-
// when the material changes, flush the current batch
252-
if ctx.batch.material_key.as_ref() != Some(&material_key) {
253-
flush_batch(ctx);
254-
ctx.batch.material_key = Some(material_key);
255-
ctx.batch.current_mesh = Some(empty_mesh());
256-
}
307+
maybe_start_batch(ctx, material_key);
257308

258309
// accumulate geometry into the current mega mesh
259310
if let Some(ref mut mesh) = ctx.batch.current_mesh {

0 commit comments

Comments
 (0)