diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 0b424cf..71b5004 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1,5 +1,5 @@ use bevy::{ - math::{Vec2, Vec3, Vec4}, + math::{Affine3A, Mat4, Vec2, Vec3, Vec4}, prelude::Entity, render::render_resource::{Extent3d, TextureFormat}, }; @@ -1887,6 +1887,202 @@ pub extern "C" fn processing_transform_reset(entity_id: u64) { error::check(|| transform_reset(entity)); } +/// Attach an orbit camera controller. +#[unsafe(no_mangle)] +pub extern "C" fn processing_orbit_camera(graphics_id: u64) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_orbit_camera(graphics_entity)); +} + +/// Attach a free-flight camera controller. +#[unsafe(no_mangle)] +pub extern "C" fn processing_free_camera(graphics_id: u64) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_free_camera(graphics_entity)); +} + +/// Attach a pan/zoom camera controller. +#[unsafe(no_mangle)] +pub extern "C" fn processing_pan_camera(graphics_id: u64) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_pan_camera(graphics_entity)); +} + +/// Remove the active camera controller. +#[unsafe(no_mangle)] +pub extern "C" fn processing_disable_camera_controller(graphics_id: u64) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_disable_camera_controller(graphics_entity)); +} + +/// Set the camera distance from its center (zoom). +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_set_distance(graphics_id: u64, distance: f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_set_distance(graphics_entity, distance)); +} + +/// Set the orbit camera's look-at center. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_set_center(graphics_id: u64, x: f32, y: f32, z: f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_set_center(graphics_entity, Vec3::new(x, y, z))); +} + +/// Set the minimum camera distance. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_set_min_distance(graphics_id: u64, min: f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_set_min_distance(graphics_entity, min)); +} + +/// Set the maximum camera distance. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_set_max_distance(graphics_id: u64, max: f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_set_max_distance(graphics_entity, max)); +} + +/// Set the camera controller's sensitivity. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_set_speed(graphics_id: u64, speed: f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_set_speed(graphics_entity, speed)); +} + +/// Reset the camera controller to its initial pose. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera_reset(graphics_id: u64) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| camera_reset(graphics_entity)); +} + +/// Position the camera at eye, looking at center with the given up. +/// +/// An active camera controller overrides this each frame. +#[unsafe(no_mangle)] +pub extern "C" fn processing_camera( + graphics_id: u64, + eye_x: f32, + eye_y: f32, + eye_z: f32, + center_x: f32, + center_y: f32, + center_z: f32, + up_x: f32, + up_y: f32, + up_z: f32, +) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| { + graphics_camera( + graphics_entity, + Vec3::new(eye_x, eye_y, eye_z), + Vec3::new(center_x, center_y, center_z), + Vec3::new(up_x, up_y, up_z), + ) + }); +} + +/// A column-major 4x4 matrix. +#[repr(C)] +pub struct Matrix { + pub m: [f32; 16], +} + +/// Right-multiply the model matrix by a column-major 4x4 matrix. +/// +/// # Safety +/// - matrix points to at least 16 f32. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_apply_matrix(graphics_id: u64, matrix: *const f32) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| { + // SAFETY: Caller guarantees matrix points to 16 valid f32 elements + let cols: [f32; 16] = unsafe { std::slice::from_raw_parts(matrix, 16) } + .try_into() + .unwrap(); + let affine = Affine3A::from_mat4(Mat4::from_cols_array(&cols)); + graphics_record_command(graphics_entity, DrawCommand::ApplyMatrix(affine)) + }); +} + +/// The current model matrix, column-major. Flushes pending draws; identity on error. +#[unsafe(no_mangle)] +pub extern "C" fn processing_get_matrix(graphics_id: u64) -> Matrix { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + let m = error::check(|| graphics_get_matrix(graphics_entity).map(|mat| mat.to_cols_array())) + .unwrap_or_else(|| Mat4::IDENTITY.to_cols_array()); + Matrix { m } +} + +/// Model-space point to world-space X (modelX). +#[unsafe(no_mangle)] +pub extern "C" fn processing_model_x(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_model_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.x)) + .unwrap_or(0.0) +} + +/// Model-space point to world-space Y (modelY). +#[unsafe(no_mangle)] +pub extern "C" fn processing_model_y(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_model_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.y)) + .unwrap_or(0.0) +} + +/// Model-space point to world-space Z (modelZ). +#[unsafe(no_mangle)] +pub extern "C" fn processing_model_z(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_model_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.z)) + .unwrap_or(0.0) +} + +/// Model-space point to screen X in pixels (screenX). +#[unsafe(no_mangle)] +pub extern "C" fn processing_screen_x(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_screen_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.x)) + .unwrap_or(0.0) +} + +/// Model-space point to screen Y in pixels (screenY). +#[unsafe(no_mangle)] +pub extern "C" fn processing_screen_y(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_screen_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.y)) + .unwrap_or(0.0) +} + +/// Model-space point to screen depth in [0,1] (screenZ). +#[unsafe(no_mangle)] +pub extern "C" fn processing_screen_z(graphics_id: u64, x: f32, y: f32, z: f32) -> f32 { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| graphics_screen_point(graphics_entity, Vec3::new(x, y, z)).map(|p| p.z)) + .unwrap_or(0.0) +} + pub const PROCESSING_ATTR_FORMAT_FLOAT: u8 = 1; pub const PROCESSING_ATTR_FORMAT_FLOAT2: u8 = 2; pub const PROCESSING_ATTR_FORMAT_FLOAT3: u8 = 3; diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 8f93b53..d8fa2b9 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -448,6 +448,46 @@ pub fn begin_draw(In(entity): In, mut state_query: Query<&mut RenderStat Ok(()) } +pub fn get_matrix(In(entity): In, states: Query<&RenderState>) -> Result { + let state = states + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + Ok(Mat4::from(state.transform.current())) +} + +pub fn model_point( + In((entity, point)): In<(Entity, Vec3)>, + states: Query<&RenderState>, +) -> Result { + let state = states + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + Ok(state.transform.transform_point(point)) +} + +pub fn screen_point( + In((entity, point)): In<(Entity, Vec3)>, + query: Query<(&RenderState, &Projection, &Transform, &SurfaceSize)>, +) -> Result { + let (state, projection, camera_transform, size) = query + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + let world = state.transform.transform_point(point); + let clip_from_view = projection.get_clip_from_view(); + let view_from_world = camera_transform.to_matrix().inverse(); + let clip = clip_from_view * view_from_world * world.extend(1.0); + if clip.w == 0.0 { + return Ok(Vec3::ZERO); + } + let ndc = clip.truncate() / clip.w; + let SurfaceSize(width, height) = *size; + Ok(Vec3::new( + (ndc.x + 1.0) * 0.5 * width as f32, + (1.0 - ndc.y) * 0.5 * height as f32, + ndc.z, + )) +} + pub fn flush(app: &mut App, entity: Entity) -> Result<()> { graphics_mut!(app, entity).insert(Flush); app.update(); diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 0718e5e..2024566 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -847,6 +847,50 @@ pub fn transform_look_at(entity: Entity, target: Vec3) -> error::Result<()> { }) } +/// Position the camera at `eye`, looking at `center` with the given `up`. +pub fn graphics_camera( + entity: Entity, + eye: Vec3, + center: Vec3, + up: Vec3, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(transform::camera, (entity, eye, center, up)) + .unwrap() + }) +} + +/// The current model matrix. Flushes pending draws first. +pub fn graphics_get_matrix(entity: Entity) -> error::Result { + app_mut(|app| { + graphics::flush(app, entity)?; + app.world_mut() + .run_system_cached_with(graphics::get_matrix, entity) + .unwrap() + }) +} + +/// Map a point from the current model space to world space (modelX/Y/Z). +pub fn graphics_model_point(entity: Entity, point: Vec3) -> error::Result { + app_mut(|app| { + graphics::flush(app, entity)?; + app.world_mut() + .run_system_cached_with(graphics::model_point, (entity, point)) + .unwrap() + }) +} + +/// Map a point from the current model space to screen space (screenX/Y/Z). +pub fn graphics_screen_point(entity: Entity, point: Vec3) -> error::Result { + app_mut(|app| { + graphics::flush(app, entity)?; + app.world_mut() + .run_system_cached_with(graphics::screen_point, (entity, point)) + .unwrap() + }) +} + pub fn transform_reset(entity: Entity) -> error::Result<()> { app_mut(|app| { app.world_mut() diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 97238cf..ea82485 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -1,3 +1,4 @@ +use bevy::math::Affine3A; use bevy::prelude::*; use bevy::render::render_resource::{BlendComponent, BlendFactor, BlendOperation, BlendState}; use processing_core::constants as consts; @@ -573,6 +574,7 @@ pub enum DrawCommand { PushMatrix, PopMatrix, ResetMatrix, + ApplyMatrix(Affine3A), PushStyle, PopStyle, Translate(Vec3), diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 23cb7ac..cc173ad 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -862,6 +862,7 @@ pub fn flush_draw_commands( DrawCommand::PushMatrix => state.transform.push(), DrawCommand::PopMatrix => state.transform.pop(), DrawCommand::ResetMatrix => state.transform.reset(), + DrawCommand::ApplyMatrix(m) => state.transform.apply(m), DrawCommand::PushStyle => state.style.push(), DrawCommand::PopStyle => state.style.pop(), DrawCommand::Translate(v) => state.transform.apply(Affine3A::from_translation(v)), diff --git a/crates/processing_render/src/transform.rs b/crates/processing_render/src/transform.rs index 1e8c2d9..9661763 100644 --- a/crates/processing_render/src/transform.rs +++ b/crates/processing_render/src/transform.rs @@ -112,6 +112,17 @@ pub fn look_at( Ok(()) } +pub fn camera( + In((entity, eye, center, up)): In<(Entity, Vec3, Vec3, Vec3)>, + mut transforms: Query<&mut Transform>, +) -> Result<()> { + let mut transform = transforms + .get_mut(entity) + .map_err(|_| ProcessingError::TransformNotFound)?; + *transform = Transform::from_translation(eye).looking_at(center, up); + Ok(()) +} + pub fn reset(In(entity): In, mut transforms: Query<&mut Transform>) -> Result<()> { let mut transform = transforms .get_mut(entity)