From f761e0b51e722022f84c41c37d823216c8de9885 Mon Sep 17 00:00:00 2001 From: Nomagno Date: Sat, 30 May 2026 16:59:14 +0200 Subject: [PATCH 01/21] Make xy plane offset coordinates in 2D mode work like in 3D mode --- src/systems.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems.rs b/src/systems.rs index 05f773c..6136810 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -848,8 +848,8 @@ pub(crate) fn sync_rgp_objects(mut params: RgpSyncParams) { match presentation.mode { TerminalPresentationMode::Flat2d => { transform.translation = Vec3::new( - layout.center_x + anchor.style.offset.x, - layout.center_y + bob + anchor.style.offset.y, + layout.center_x + anchor.style.offset.x*(terminal.pixmap_dimensions().x as f32), + layout.center_y + bob + anchor.style.offset.y*(terminal.pixmap_dimensions().y as f32), CURSOR_DEPTH + anchor.style.depth * 4.0 + anchor.style.offset.z, ); transform.rotation = object_rotation; From e0df361400e8bb71237387625d022243b1cfc58b Mon Sep 17 00:00:00 2001 From: Nomagno Date: Sun, 31 May 2026 18:18:02 +0200 Subject: [PATCH 02/21] Orthographic -> Perspective (breaks 2D mode, RGP commands to control later) --- src/scene/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c27cb90..1f7fc34 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4,6 +4,8 @@ mod mobius; pub use mobius::{MobiusTransition, MobiusTransitionDirection}; +use std::f32::consts::PI; + use bevy::asset::RenderAssetUsages; use bevy::camera::ClearColorConfig; use bevy::ecs::query::With; @@ -227,10 +229,10 @@ pub fn setup_scene( clear_color: ClearColorConfig::None, ..default() }, - Projection::Orthographic(OrthographicProjection { + Projection::Perspective(PerspectiveProjection { near: -2000.0, far: 2000.0, - ..OrthographicProjection::default_3d() + ..PerspectiveProjection::default() }), Transform::from_xyz(0.0, 0.0, 800.0).looking_at(Vec3::ZERO, Vec3::Y), Msaa::Off, @@ -462,13 +464,13 @@ pub(crate) fn apply_terminal_presentation( } for (mut projection, mut transform) in &mut plane_transforms.p2() { - if let Projection::Orthographic(ortho) = projection.as_mut() { + if let Projection::Perspective(persp) = projection.as_mut() { let zoom = if is_mobius && mobius_transition.active { mobius_transition.current_zoom() } else { plane_view.zoom }; - ortho.scale = if is_3d { zoom } else { 1.0 }; + persp.fov = if is_3d { zoom } else { 1.0 }; } let offset = if is_3d { From 05aa113ae2f29bdeca060450ee9a8f56c8ff1f06 Mon Sep 17 00:00:00 2001 From: Nomagno Date: Mon, 1 Jun 2026 00:27:39 +0200 Subject: [PATCH 03/21] Make it possible to switch between orthographic and perspective projections --- src/config.rs | 9 ++-- src/keyboard.rs | 22 ++++++-- src/mouse.rs | 30 +++-------- src/scene/mod.rs | 67 +++++++++++++++++++++---- src/systems.rs | 127 ++++++++++++++++++----------------------------- 5 files changed, 135 insertions(+), 120 deletions(-) diff --git a/src/config.rs b/src/config.rs index befc282..6168aa2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -202,9 +202,12 @@ pub enum BindingAction { /// Disables a binding. #[serde(rename = "none")] None, - /// Toggles between the flat and warped terminal views. - #[serde(rename = "Toggle3DMode")] - Toggle3DMode, + /// Toggles between the flat and orthographic terminal views. + #[serde(rename = "ToggleOrtho3DMode")] + ToggleOrtho3DMode, + /// Toggles the perspective terminal view. + #[serde(rename = "TogglePersp3DMode")] + TogglePersp3DMode, /// Toggles the Mobius-strip terminal view. #[serde(rename = "ToggleMobiusMode")] ToggleMobiusMode, diff --git a/src/keyboard.rs b/src/keyboard.rs index 4bd4d56..a6631c9 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -59,13 +59,22 @@ impl FromWorld for TerminalKeyBindings { let app_config = world.resource::(); let mut bindings = vec![ KeyBinding::new( - KeyCode::Enter, + KeyCode::KeyO, BindingModifiers { control: true, alt: true, ..default() }, - BindingAction::Toggle3DMode, + BindingAction::ToggleOrtho3DMode, + ), + KeyBinding::new( + KeyCode::KeyP, + BindingModifiers { + control: true, + alt: true, + ..default() + }, + BindingAction::TogglePersp3DMode, ), KeyBinding::new( KeyCode::KeyM, @@ -353,12 +362,19 @@ pub fn handle_keyboard_input( match action { BindingAction::None => {} - BindingAction::Toggle3DMode => { + BindingAction::ToggleOrtho3DMode => { params.presentation.toggle_plane_mode(); params.mobius_transition.stop(); params.selection.clear(); params.redraw.request(); continue; + }, + BindingAction::TogglePersp3DMode => { + params.presentation.toggle_persp_mode(); + params.mobius_transition.stop(); + params.selection.clear(); + params.redraw.request(); + continue; } BindingAction::ToggleMobiusMode => { if params.presentation.mode == TerminalPresentationMode::Mobius3d { diff --git a/src/mouse.rs b/src/mouse.rs index a0c23f7..035c62a 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -236,10 +236,7 @@ pub(crate) fn handle_mouse_input( continue; } - if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) { + if presentation.mode.is_3d() { if plane_view.rotating { if let Some(last) = plane_view.last_rotate_cursor { let delta = event.position - last; @@ -316,10 +313,7 @@ pub(crate) fn handle_mouse_input( runtime.write_input(&encode_mouse_event(cell, 0, false, mouse_encoding)); forwarded_mouse.last_cell = Some(cell); } - } else if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) { + } else if presentation.mode.is_3d() { plane_view.rotating = true; plane_view.last_rotate_cursor = selection.cursor_position(); } else if let Some(pos) = selection.cursor_position() @@ -340,10 +334,7 @@ pub(crate) fn handle_mouse_input( runtime.write_input(&encode_mouse_event(cell, 0, true, mouse_encoding)); forwarded_mouse.last_cell = Some(cell); } - } else if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) { + } else if presentation.mode.is_3d() { plane_view.rotating = false; plane_view.last_rotate_cursor = selection.cursor_position(); } else { @@ -395,19 +386,13 @@ pub(crate) fn handle_mouse_input( } } (MouseButton::Right, ButtonState::Pressed) - if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) => + if presentation.mode.is_3d() => { plane_view.panning = true; plane_view.last_pan_cursor = selection.cursor_position(); } (MouseButton::Right, ButtonState::Released) - if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) => + if presentation.mode.is_3d() => { plane_view.panning = false; plane_view.last_pan_cursor = selection.cursor_position(); @@ -461,10 +446,7 @@ pub(crate) fn handle_mouse_input( selection.clear(); redraw.request(); } - } else if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) && delta != 0.0 + } else if presentation.mode.is_3d() && delta != 0.0 { plane_view.zoom = (plane_view.zoom - delta).clamp(0.1, 4.0); redraw.request(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1f7fc34..a67555e 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4,8 +4,6 @@ mod mobius; pub use mobius::{MobiusTransition, MobiusTransitionDirection}; -use std::f32::consts::PI; - use bevy::asset::RenderAssetUsages; use bevy::camera::ClearColorConfig; use bevy::ecs::query::With; @@ -73,6 +71,8 @@ pub enum TerminalPresentationMode { Flat2d, /// Warped 3D presentation. Plane3d, + /// Perspective 3D presentation. + Persp3d, /// Mobius-strip 3D presentation. Mobius3d, } @@ -101,17 +101,27 @@ impl TerminalPresentation { pub fn toggle_plane_mode(&mut self) { self.mode = match self.mode { TerminalPresentationMode::Flat2d => TerminalPresentationMode::Plane3d, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { + TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d | TerminalPresentationMode::Persp3d => { TerminalPresentationMode::Flat2d } }; } + /// Toggles the Perspective projection terminal view. + pub fn toggle_persp_mode(&mut self) { + self.mode = match self.mode { + TerminalPresentationMode::Persp3d => TerminalPresentationMode::Flat2d, + TerminalPresentationMode::Flat2d | TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { + TerminalPresentationMode::Persp3d + } + }; + } + /// Toggles the Mobius-strip terminal view. pub fn toggle_mobius_mode(&mut self) { self.mode = match self.mode { TerminalPresentationMode::Mobius3d => TerminalPresentationMode::Flat2d, - TerminalPresentationMode::Flat2d | TerminalPresentationMode::Plane3d => { + TerminalPresentationMode::Flat2d | TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => { TerminalPresentationMode::Mobius3d } }; @@ -173,7 +183,7 @@ type PlaneTransformQuery<'w, 's> = Query<'w, 's, &'static mut Transform, With = Query<'w, 's, &'static mut Transform, With>; type PlaneCameraQuery<'w, 's> = - Query<'w, 's, (&'static mut Projection, &'static mut Transform), With>; + Query<'w, 's, (&'static mut Projection, &'static mut Transform, &'static mut Camera), With>; #[derive(SystemParam)] pub(crate) struct PresentationParams<'w, 's> { @@ -237,6 +247,23 @@ pub fn setup_scene( Transform::from_xyz(0.0, 0.0, 800.0).looking_at(Vec3::ZERO, Vec3::Y), Msaa::Off, )); + commands.spawn(( + TerminalPlaneCamera, + Camera3d::default(), + Camera { + order: 2, + clear_color: ClearColorConfig::None, + ..default() + }, + Projection::Orthographic(OrthographicProjection { + near: -2000.0, + far: 2000.0, + ..OrthographicProjection::default_3d() + }), + Transform::from_xyz(0.0, 0.0, 800.0).looking_at(Vec3::ZERO, Vec3::Y), + Msaa::Off, + )); + let pixmap = terminal.pixmap_dimensions(); let pixmap_width = pixmap.x; @@ -463,14 +490,32 @@ pub(crate) fn apply_terminal_presentation( } } - for (mut projection, mut transform) in &mut plane_transforms.p2() { + for (mut projection, mut transform, mut camera) in &mut plane_transforms.p2() { + let active = &mut camera.is_active; if let Projection::Perspective(persp) = projection.as_mut() { - let zoom = if is_mobius && mobius_transition.active { - mobius_transition.current_zoom() + if presentation.mode == TerminalPresentationMode::Persp3d { + *active = true; + let zoom = if is_mobius && mobius_transition.active { + mobius_transition.current_zoom() + } else { + plane_view.zoom + }; + persp.fov = if is_3d { zoom } else { 1.0 }; } else { - plane_view.zoom - }; - persp.fov = if is_3d { zoom } else { 1.0 }; + *active = false; + } + } else if let Projection::Orthographic(ortho) = projection.as_mut() { + if presentation.mode != TerminalPresentationMode::Persp3d { + *active = true; + let zoom = if is_mobius && mobius_transition.active { + mobius_transition.current_zoom() + } else { + plane_view.zoom + }; + ortho.scale = if is_3d { zoom } else { 1.0 }; + } else { + *active = false; + } } let offset = if is_3d { diff --git a/src/systems.rs b/src/systems.rs index 05f773c..b50cf43 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -273,18 +273,8 @@ pub fn apply_inline_objects( ), >, ) { - let sprite_visibility = match presentation.mode { - TerminalPresentationMode::Flat2d => Visibility::Visible, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { - Visibility::Hidden - } - }; - let plane_visibility = match presentation.mode { - TerminalPresentationMode::Flat2d => Visibility::Hidden, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { - Visibility::Visible - } - }; + let sprite_visibility = if presentation.mode.is_3d() { Visibility::Visible } else { Visibility::Hidden }; + let plane_visibility = if presentation.mode.is_3d() { Visibility::Hidden } else { Visibility::Visible }; for mut visibility in &mut sprite_query { *visibility = sprite_visibility; @@ -342,10 +332,7 @@ pub(crate) fn redraw_soft_terminal(mut params: RedrawParams) { asset_server, } = &mut params; let needs_redraw = redraw.take(); - let force_live_redraw = matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) && !app_config.cursor.model.visible; + let force_live_redraw = presentation.mode.is_3d() && !app_config.cursor.model.visible; if !needs_redraw && !force_live_redraw && model_load_state.loaded { return; } @@ -369,18 +356,12 @@ pub(crate) fn redraw_soft_terminal(mut params: RedrawParams) { }); let _ = terminal.sync_image(images, time.elapsed_secs()); - if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) { + if presentation.mode.is_3d() { sync_terminal_debug_image(terminal, images, screen); } sync_plane_texture(terminal.image_handle.as_ref(), plane_materials, materials); - if matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) { + if presentation.mode.is_3d() { sync_plane_texture( terminal.back_image_handle.as_ref(), plane_back_materials, @@ -448,10 +429,7 @@ pub(crate) fn sync_inline_objects(mut params: SyncInlineParams) { images, meshes, } = &mut params; - let force_warp_sync = matches!( - presentation.mode, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d - ) && plane_warp.amount > 0.0 + let force_warp_sync = presentation.mode.is_3d() && plane_warp.amount > 0.0 && !inline_objects.anchors.is_empty(); if !force_warp_sync && !inline_objects.needs_sync(viewport.size, terminal.cols, terminal.rows) { return; @@ -590,12 +568,7 @@ fn sync_kitty_inline_image( TerminalInlineObjectSprite, sprite, Transform::from_translation(Vec3::new(layout.center_x, layout.center_y, 5.0)), - match ctx.mode { - TerminalPresentationMode::Flat2d => Visibility::Visible, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { - Visibility::Hidden - } - }, + if ctx.mode.is_3d() { Visibility::Visible } else { Visibility::Hidden }, )); let x_segments = layout.columns.clamp(2, 24); @@ -845,36 +818,33 @@ pub(crate) fn sync_rgp_objects(mut params: RgpSyncParams) { let object_rotation = base_oblique * explicit_rotation * animated_rotation; let object_scale = Vec3::splat(scale) * scale3; - match presentation.mode { - TerminalPresentationMode::Flat2d => { - transform.translation = Vec3::new( - layout.center_x + anchor.style.offset.x, - layout.center_y + bob + anchor.style.offset.y, - CURSOR_DEPTH + anchor.style.depth * 4.0 + anchor.style.offset.z, - ); - transform.rotation = object_rotation; - transform.scale = object_scale; - *visibility = Visibility::Visible; - } - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { - let Ok(plane_transform) = plane_query.single() else { - *visibility = Visibility::Hidden; - continue; - }; - let local_position = plane_surface_point( - presentation.mode, - layout.local_x, - layout.local_y, - plane_warp.amount, - elapsed_secs, - 8.0 + anchor.style.depth * 1.5, - mobius_progress, - ) + anchor.style.offset; - transform.translation = plane_transform.transform_point(local_position); - transform.rotation = plane_transform.rotation * object_rotation; - transform.scale = object_scale; - *visibility = Visibility::Visible; - } + if presentation.mode.is_3d() { + let Ok(plane_transform) = plane_query.single() else { + *visibility = Visibility::Hidden; + continue; + }; + let local_position = plane_surface_point( + presentation.mode, + layout.local_x, + layout.local_y, + plane_warp.amount, + elapsed_secs, + 8.0 + anchor.style.depth * 1.5, + mobius_progress, + ) + anchor.style.offset; + transform.translation = plane_transform.transform_point(local_position); + transform.rotation = plane_transform.rotation * object_rotation; + transform.scale = object_scale; + *visibility = Visibility::Visible; + } else { + transform.translation = Vec3::new( + layout.center_x + anchor.style.offset.x, + layout.center_y + bob + anchor.style.offset.y, + CURSOR_DEPTH + anchor.style.depth * 4.0 + anchor.style.offset.z, + ); + transform.rotation = object_rotation; + transform.scale = object_scale; + *visibility = Visibility::Visible; } } } @@ -1109,7 +1079,7 @@ pub fn animate_terminal_plane_warp( let needs_update = match presentation.mode { TerminalPresentationMode::Flat2d => false, - TerminalPresentationMode::Plane3d => { + TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => { presentation.is_changed() || warp.is_changed() || warp.amount > 0.0 } // Reapply the strip every frame so mode switches and time-based motion are visible. @@ -1216,7 +1186,7 @@ fn apply_plane_warp( position[1] = point.y; position[2] = match mode { TerminalPresentationMode::Plane3d => point.z * direction, - TerminalPresentationMode::Flat2d | TerminalPresentationMode::Mobius3d => point.z, + TerminalPresentationMode::Flat2d | TerminalPresentationMode::Mobius3d | TerminalPresentationMode::Persp3d => point.z, }; } } @@ -1309,17 +1279,17 @@ fn cursor_pose( 0.0 }; - let (translation, rotation, visibility) = match ctx.mode { - TerminalPresentationMode::Flat2d => ( - Vec3::new(local_x, local_y + bob, CURSOR_DEPTH), - Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25), - if !app_config.cursor.model.visible || screen.hide_cursor() { - Visibility::Hidden - } else { - Visibility::Visible - }, - ), - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { + let (translation, rotation, visibility) = if ctx.mode.is_3d() { + ( + Vec3::new(local_x, local_y + bob, CURSOR_DEPTH), + Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25), + if !app_config.cursor.model.visible || screen.hide_cursor() { + Visibility::Hidden + } else { + Visibility::Visible + }, + ) + } else { let Ok(plane_transform) = ctx.plane_query.single() else { return (Vec3::ZERO, Quat::IDENTITY, scale, Visibility::Hidden); }; @@ -1344,7 +1314,6 @@ fn cursor_pose( Visibility::Hidden }, ) - } }; (translation, rotation, scale, visibility) @@ -1373,7 +1342,7 @@ fn plane_surface_point( ) -> Vec3 { match mode { TerminalPresentationMode::Flat2d => Vec3::new(local_x, local_y, depth_offset), - TerminalPresentationMode::Plane3d => Vec3::new( + TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => Vec3::new( local_x, local_y, plane_surface_z(local_x, local_y, warp_amount, elapsed_secs) + depth_offset, From 5652d631c94faa6feb04a193b579b95c11f69947 Mon Sep 17 00:00:00 2001 From: Nomagno Date: Mon, 1 Jun 2026 15:17:32 +0200 Subject: [PATCH 04/21] Camera controls extension to RGP (WIP, parsing only) Add an RGP extension to support controlling the camera Remove unused parameters --- protocols/graphics.md | 19 ++++++++++++++- src/inline.rs | 56 ++++++++++++++++++++++++++++++++++++++++--- src/keyboard.rs | 5 ++++ src/plugin.rs | 6 ++--- src/rgp.rs | 53 ++++++++++++++++++++++++++++++++++++++-- src/scene/mod.rs | 10 ++++---- src/systems.rs | 17 +++++++++++-- 7 files changed, 151 insertions(+), 15 deletions(-) diff --git a/protocols/graphics.md b/protocols/graphics.md index 2cce059..d320bda 100644 --- a/protocols/graphics.md +++ b/protocols/graphics.md @@ -65,7 +65,7 @@ ESC _ ratty;g;s ESC \ Ratty replies: ```text -ESC _ ratty;g;s;v=1;fmt=obj|glb;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;transform=1;update=1 ESC \ +ESC _ ratty;g;s;v=1;fmt=obj|glb;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;camera=1;transform=1;update=1 ESC \ ``` Fields: @@ -79,6 +79,7 @@ Fields: - `depth=1`: `depth=` placement is supported - `color=1`: `color=` placement is supported - `brightness=1`: `brightness=` placement is supported +- `camera=1`: `c` camera updates are supported - `transform=1`: transform fields such as rotation and offsets are supported - `update=1`: `u` object updates are supported @@ -210,6 +211,22 @@ Delete all Ratty graphics objects: ESC _ ratty;g;d ESC \ ``` +### 6. Camera controls + +Updates the rotation, transform or mode of a camera setting slot (there are 10 camera setting slots 0-9, they're the `id` field). + +```text +ESC _ ratty;g;c;id=0;set=0;type=Ortho;px=0.25;scale=1.0 ESC \ +``` + +- `set`: if 1, changes the current camera settings to those of the slot referred to by `id`. Defaults to 0. + +Optional fields (these camera settings will not be updated if the fields are not specified): +- `type`: one of `Flat`, `Ortho`, `Persp`, `Mobius`. +- `scale`: zoom/fov depending on the camera type, does nothing for `Flat` mode +- `px`, `py`, `pz`: camera offset from terminal plane origin +- `rx`, `ry`, `rz`: camera roll, pitch, yaw + ## Example Session Register an embedded object path: diff --git a/src/inline.rs b/src/inline.rs index f58d75b..434c4f5 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -13,6 +13,8 @@ use crate::rgp::{ RgpOperation, RgpPlacementStyle, RgpPlacementUpdate, RgpRegisterSource, consume_sequence as consume_rgp_sequence, support_reply, }; +use crate::scene::{ TerminalPlaneView, TerminalPresentation }; + const APC_START: &[u8] = b"\x1b_"; const ST: &[u8] = b"\x1b\\"; const C1_ST: u8 = 0x9c; @@ -32,6 +34,15 @@ pub struct TerminalRgpObject { pub object_id: u32, } +/// Holds the slots for the camera presets +#[derive(Resource, Default)] +pub struct TerminalCameraViewSlots { + /// The current slot in use + pub current_slot: usize, + /// The list of all slots, accessed with ctrl + alt + [0-9] or set through the Ratty Graphics Protocol + pub slots: [(TerminalPlaneView, TerminalPresentation); 10], +} + /// Inline object registry and anchor state. #[derive(Resource, Default)] pub struct TerminalInlineObjects { @@ -52,6 +63,7 @@ impl TerminalInlineObjects { &mut self, chunk: &[u8], parser: &mut vt100::Parser, + camera_slots: &mut TerminalCameraViewSlots, ) -> Vec> { self.pending_bytes.extend_from_slice(chunk); let mut replies = Vec::new(); @@ -80,7 +92,7 @@ impl TerminalInlineObjects { }; let sequence = self.pending_bytes[start..end].to_vec(); let (handled, reply) = - self.handle_apc_sequence(&sequence, parser.screen().cursor_position()); + self.handle_apc_sequence(&sequence, parser.screen().cursor_position(), camera_slots); if let Some(reply) = reply { replies.push(reply); } @@ -170,8 +182,9 @@ impl TerminalInlineObjects { &mut self, sequence: &[u8], cursor_position: (u16, u16), + camera_slots: &mut TerminalCameraViewSlots, ) -> (bool, Option>) { - if let Some(reply) = self.handle_rgp_sequence(sequence) { + if let Some(reply) = self.handle_rgp_sequence(sequence, camera_slots) { return (true, reply); } @@ -239,10 +252,47 @@ impl TerminalInlineObjects { } } - fn handle_rgp_sequence(&mut self, sequence: &[u8]) -> Option>> { + fn handle_rgp_sequence(&mut self, + sequence: &[u8], + camera_slots: &mut TerminalCameraViewSlots) -> Option>> { let operation = consume_rgp_sequence(sequence)?; Some(match operation { RgpOperation::SupportQuery => Some(support_reply()), + RgpOperation::Camera { + camera_slot, switch_immediately, settings + } => { + //info!("camera settings for slot {}: set {}; rotation: ({},{},{})", camera_slot, switch_immediately, settings.rotation[0].unwrap_or(0.0), settings.rotation[1].unwrap_or(0.0), settings.rotation[2].unwrap_or(0.0)); + if (camera_slot as usize) < camera_slots.slots.len() { + if switch_immediately { + camera_slots.current_slot = camera_slot as usize; + } + if let Some(ctype) = settings.camera_type { + camera_slots.slots[camera_slot as usize].1 = TerminalPresentation { mode: ctype }; + } + if let Some(px) = settings.offset[0] { + camera_slots.slots[camera_slot as usize].0.camera_offset[0] = px; + } + if let Some(py) = settings.offset[1] { + camera_slots.slots[camera_slot as usize].0.camera_offset[1] = py; + } + if let Some(pz) = settings.offset[2] { + camera_slots.slots[camera_slot as usize].0.camera_offset[2] = pz; + } + if let Some(rx) = settings.rotation[0] { + camera_slots.slots[camera_slot as usize].0.yaw = rx; + } + if let Some(_ry) = settings.rotation[1] { + /*camera_slots.slots[camera_slot as usize].0.roll = ry;*/ + } + if let Some(rz) = settings.rotation[2] { + camera_slots.slots[camera_slot as usize].0.pitch = rz; + } + if let Some(scale) = settings.scale { + camera_slots.slots[camera_slot as usize].0.zoom = scale; + } + } + None + }, RgpOperation::Register { object_id, format, diff --git a/src/keyboard.rs b/src/keyboard.rs index a6631c9..88e4484 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -16,6 +16,7 @@ use crate::scene::{ TerminalPresentationMode, TerminalViewport, }; use crate::terminal::{TerminalRedrawState, TerminalSurface}; +use crate::inline::{TerminalCameraViewSlots}; /// Clipboard bridge for terminal copy and paste. pub struct TerminalClipboard { @@ -320,6 +321,7 @@ pub struct KeyboardSystemParams<'w, 's> { selection: ResMut<'w, TerminalSelection>, plane_warp: ResMut<'w, TerminalPlaneWarp>, plane_view: ResMut<'w, TerminalPlaneView>, + camera_slots: Res<'w, TerminalCameraViewSlots>, presentation: ResMut<'w, TerminalPresentation>, mobius_transition: ResMut<'w, MobiusTransition>, clipboard: NonSendMut<'w, TerminalClipboard>, @@ -337,6 +339,9 @@ pub fn handle_keyboard_input( mut keyboard: Local, mut params: KeyboardSystemParams, ) { + params.presentation.mode = params.camera_slots.slots[params.camera_slots.current_slot].1.mode; + let view: &mut TerminalPlaneView = params.plane_view.as_mut(); + *view = params.camera_slots.slots[params.camera_slots.current_slot].0; for event in keyboard_events.read() { let binding_key_code = navigation_key_code(&event.logical_key).unwrap_or(event.key_code); let modifiers = current_modifiers(¶ms.keys).union(keyboard.modifiers()); diff --git a/src/plugin.rs b/src/plugin.rs index 5d480fa..9784653 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -25,8 +25,8 @@ impl Plugin for TerminalPlugin { .init_non_send_resource::() .add_systems(Startup, setup_scene) .add_systems(Update, pump_pty_output) - .add_systems(Update, handle_keyboard_input) - .add_systems(Update, handle_mouse_input) + .add_systems(Update, handle_keyboard_input.after(pump_pty_output)) + .add_systems(Update, handle_mouse_input.after(handle_keyboard_input)) .add_systems(Update, handle_window_resize) .add_systems( Update, @@ -42,7 +42,7 @@ impl Plugin for TerminalPlugin { Update, redraw_soft_terminal .after(handle_mouse_input) - .after(pump_pty_output), + .after(pump_pty_output).after(apply_inline_objects), ) .add_systems(Update, sync_inline_objects.after(redraw_soft_terminal)) .add_systems(Update, sync_rgp_objects.after(sync_inline_objects)) diff --git a/src/rgp.rs b/src/rgp.rs index 5cc489b..9ae1b06 100644 --- a/src/rgp.rs +++ b/src/rgp.rs @@ -1,6 +1,9 @@ //! Ratty Graphics Protocol parsing. use base64::Engine as _; +use bevy::prelude::*; + +use crate::scene::TerminalPresentationMode; /// Ratty Graphics Protocol APC prefix. pub const RGP_APC_START: &[u8] = b"\x1b_ratty;g;"; @@ -28,8 +31,21 @@ pub struct RgpPlacementStyle { pub scale3: [f32; 3], } -/// Partial update for an RGP object placement. +/// Settings for an RGP Camera. #[derive(Clone, Copy, Default)] +pub struct RgpCameraSettings { + /// Camera type, currently of "Flat","Proj","Pers","Mobs" + pub camera_type: Option, + /// Scale multiplier (FOV/Zoom depending on if orthogonal/perspective) + pub scale: Option, + /// Translation offset relative to the terminal plane. + pub offset: [Option; 3], + /// Rotation in degrees. + pub rotation: [Option; 3], +} + +/// Partial update for an RGP object placement. +#[derive(Clone, Copy)] pub struct RgpPlacementUpdate { /// Updates the default animation flag. pub animate: Option, @@ -86,9 +102,11 @@ pub fn consume_sequence(sequence: &[u8]) -> Option { let mut id = None; let mut format = None; let mut path = None; + let mut ctype = None; let mut source = None; let mut more = None; let mut name = None; + let mut set: Option = None; let mut row = None; let mut col = None; let mut width = None; @@ -120,9 +138,11 @@ pub fn consume_sequence(sequence: &[u8]) -> Option { "id" => id = value.parse().ok(), "fmt" => format = Some(value.to_string()), "path" => path = Some(value.to_string()), + "type" => ctype = Some(value.to_string()), "source" => source = Some(value.to_string()), "more" => more = parse_bool(value), "name" => name = Some(value.to_string()), + "set" => set = value.parse().ok(), "row" => row = value.parse().ok(), "col" => col = value.parse().ok(), "w" => width = value.parse().ok(), @@ -151,6 +171,26 @@ pub fn consume_sequence(sequence: &[u8]) -> Option { match verb { "s" => Some(RgpOperation::SupportQuery), + "c" => Some(RgpOperation::Camera { + camera_slot: id?, + switch_immediately: set.unwrap_or(0) != 0, + settings: RgpCameraSettings { + camera_type: if let Some(my_camera_type) = ctype { + match my_camera_type.as_str() { + "Flat" => Some(TerminalPresentationMode::Flat2d), + "Ortho" => Some(TerminalPresentationMode::Plane3d), + "Persp" => Some(TerminalPresentationMode::Persp3d), + "Mobius" => Some(TerminalPresentationMode::Mobius3d), + _ => None + } + } else { + None + }, + scale, + offset: [px, py, pz], + rotation: [rx, ry, rz], + } + }), "r" => Some(RgpOperation::Register { object_id: id?, format: format?, @@ -235,6 +275,15 @@ pub enum RgpOperation { /// Register source. source: RgpRegisterSource, }, + /// Camera manipulation + Camera { + /// Camera slot, 0 to just modify current camera + camera_slot: u32, + /// Defaults to 0, switches to the camera upon setting or not + switch_immediately: bool, + /// The settings: zoom/FOV, rotation, offset + settings: RgpCameraSettings + }, /// Object placement. Place { /// Object identifier. @@ -260,7 +309,7 @@ pub enum RgpOperation { /// Returns the RGP support reply sequence. pub fn support_reply() -> Vec { - b"\x1b_ratty;g;s;v=1;fmt=obj|glb;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;transform=1;update=1\x1b\\".to_vec() + b"\x1b_ratty;g;s;v=1;fmt=obj|glb;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;transform=1;update=1;camera=1;\x1b\\".to_vec() } fn parse_color(value: &str) -> Option<[u8; 3]> { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a67555e..b83d2b1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -15,6 +15,7 @@ use bevy::render::render_resource::{Extent3d, Face, TextureDimension, TextureFor use crate::config::AppConfig; use crate::terminal::TerminalSurface; +use crate::inline::TerminalCameraViewSlots; /// Marker for the 2D terminal sprite. #[derive(Component)] @@ -65,10 +66,10 @@ pub struct TerminalViewport { } /// Terminal presentation mode. -#[derive(Resource, Clone, Copy, PartialEq, Eq)] +#[derive(Resource, Clone, Copy, PartialEq, Eq, Default)] pub enum TerminalPresentationMode { /// Flat 2D presentation. - Flat2d, + #[default] Flat2d , /// Warped 3D presentation. Plane3d, /// Perspective 3D presentation. @@ -90,7 +91,7 @@ impl TerminalPresentationMode { } /// Active terminal presentation. -#[derive(Resource)] +#[derive(Resource, Default)] pub struct TerminalPresentation { /// Current presentation mode. pub mode: TerminalPresentationMode, @@ -129,7 +130,7 @@ impl TerminalPresentation { } /// Camera state for 3D presentation. -#[derive(Resource)] +#[derive(Resource, Copy, Clone)] pub struct TerminalPlaneView { /// Camera yaw. pub yaw: f32, @@ -379,6 +380,7 @@ pub fn setup_scene( mode: TerminalPresentationMode::Flat2d, }); commands.insert_resource(TerminalPlaneView::default()); + commands.insert_resource(TerminalCameraViewSlots::default()); commands.insert_resource(MobiusTransition::default()); commands.insert_resource(ModelLoadState { loaded: false, diff --git a/src/systems.rs b/src/systems.rs index b750a74..e8af6df 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -24,7 +24,7 @@ use std::sync::mpsc::TryRecvError; use crate::config::{AppConfig, CURSOR_DEPTH}; use crate::inline::{ InlineObject, TerminalInlineObjectPlane, TerminalInlineObjectSprite, TerminalInlineObjects, - TerminalRgpObject, + TerminalRgpObject, TerminalCameraViewSlots, }; use crate::model::CursorModel; use crate::model::spawn_cursor_model; @@ -116,6 +116,7 @@ type PlaneBackResizeQuery<'w, 's> = Query< pub fn pump_pty_output( mut runtime: NonSendMut, mut inline_objects: ResMut, + mut camera_slots: ResMut, mut app_exit: MessageWriter, mut redraw: ResMut, ) { @@ -135,7 +136,7 @@ pub fn pump_pty_output( } else { None }; - let mut replies = inline_objects.consume_pty_output(&chunk, &mut runtime.parser); + let mut replies = inline_objects.consume_pty_output(&chunk, &mut runtime.parser, camera_slots.as_mut()); replies.extend(runtime.parser.callbacks_mut().take_replies()); for reply in replies { runtime.write_input(&reply); @@ -857,6 +858,9 @@ pub(crate) struct BrightnessParams<'w, 's> { rgp_roots: Query<'w, 's, (Entity, &'static TerminalRgpObject)>, cursor_roots: Query<'w, 's, Entity, With>, parent_query: Query<'w, 's, &'static ChildOf>, + camera_slots: ResMut<'w, TerminalCameraViewSlots>, + plane_view: Res<'w, TerminalPlaneView>, + presentation: Res<'w, TerminalPresentation>, material_query: Query< 'w, 's, @@ -887,10 +891,19 @@ pub(crate) fn apply_instance_brightness(mut params: BrightnessParams) { rgp_roots, cursor_roots, parent_query, + camera_slots, + plane_view, + presentation, material_query, materials, commands, } = &mut params; + + let slot = camera_slots.current_slot; + let plane = plane_view.as_ref(); + camera_slots.slots[slot].1.mode = presentation.mode; + camera_slots.slots[slot].0 = *plane; + if material_query.is_empty() { return; } From fa48c185f0425a883773ea83058834f325c65886 Mon Sep 17 00:00:00 2001 From: Nomagno Date: Thu, 4 Jun 2026 21:37:25 +0200 Subject: [PATCH 05/21] Fix: report correct window size back to Portable-PTY at all times --- src/keyboard.rs | 2 +- src/runtime.rs | 7 +++---- src/systems.rs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 88e4484..e9cc832 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -504,7 +504,7 @@ pub fn handle_keyboard_input( ((params.viewport.size.x / char_dims.x as f32).floor() as u16).max(1); let rows = ((params.viewport.size.y / char_dims.y as f32).floor() as u16).max(1); - params.runtime.resize(cols, rows); + params.runtime.resize(cols, rows, params.viewport.size.x as u16, params.viewport.size.y as u16); params.terminal.resize(cols, rows); params.redraw.request(); } diff --git a/src/runtime.rs b/src/runtime.rs index 09ed959..d7e4f00 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -362,7 +362,7 @@ impl TerminalRuntime { } /// Resizes the PTY and parser screen. - pub fn resize(&mut self, cols: u16, rows: u16) { + pub fn resize(&mut self, cols: u16, rows: u16, pw: u16, ph: u16) { if cols == 0 || rows == 0 { return; } @@ -370,9 +370,8 @@ impl TerminalRuntime { let _ = self._master.resize(PtySize { rows, cols, - pixel_width: 0, - pixel_height: 0, - }); + pixel_width: pw, + pixel_height: ph, }); self.parser.screen_mut().set_size(rows, cols); } diff --git a/src/systems.rs b/src/systems.rs index e8af6df..102767d 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -240,7 +240,7 @@ pub(crate) fn handle_window_resize( let cols = ((viewport_size.x / char_dims.x as f32).floor() as u16).max(1); let rows = ((viewport_size.y / char_dims.y as f32).floor() as u16).max(1); - runtime.resize(cols, rows); + runtime.resize(cols, rows, viewport_size.x as u16, viewport_size.y as u16); terminal.resize(cols, rows); let _ = terminal.sync_image(images, 0.0); redraw.request(); From 69a34522bfcf483ec561ddb3318948e3aa9d31cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Tue, 30 Jun 2026 22:21:08 +0200 Subject: [PATCH 06/21] refactor: clean up implementation --- src/inline.rs | 26 +++++++---- src/keyboard.rs | 10 +++-- src/mouse.rs | 11 ++--- src/plugin.rs | 3 +- src/rgp.rs | 10 ++--- src/scene/mod.rs | 46 ++++++++++++-------- src/systems.rs | 110 +++++++++++++++++++++++++++-------------------- 7 files changed, 123 insertions(+), 93 deletions(-) diff --git a/src/inline.rs b/src/inline.rs index e52f74a..845d0f7 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -16,7 +16,7 @@ use crate::rgp::{ RgpOperation, RgpPlacementStyle, RgpPlacementUpdate, RgpRegisterSource, consume_sequence as consume_rgp_sequence, support_reply, }; -use crate::scene::{ TerminalPlaneView, TerminalPresentation }; +use crate::scene::{TerminalPlaneView, TerminalPresentation}; const APC_START: &[u8] = b"\x1b_"; const ST: &[u8] = b"\x1b\\"; @@ -94,8 +94,11 @@ impl TerminalInlineObjects { return replies; }; let sequence = self.pending_bytes[start..end].to_vec(); - let (handled, reply) = - self.handle_apc_sequence(&sequence, parser.screen().cursor_position(), camera_slots); + let (handled, reply) = self.handle_apc_sequence( + &sequence, + parser.screen().cursor_position(), + camera_slots, + ); if let Some(reply) = reply { replies.push(reply); } @@ -255,14 +258,18 @@ impl TerminalInlineObjects { } } - fn handle_rgp_sequence(&mut self, - sequence: &[u8], - camera_slots: &mut TerminalCameraViewSlots) -> Option>> { + fn handle_rgp_sequence( + &mut self, + sequence: &[u8], + camera_slots: &mut TerminalCameraViewSlots, + ) -> Option>> { let operation = consume_rgp_sequence(sequence)?; Some(match operation { RgpOperation::SupportQuery => Some(support_reply()), RgpOperation::Camera { - camera_slot, switch_immediately, settings + camera_slot, + switch_immediately, + settings, } => { //info!("camera settings for slot {}: set {}; rotation: ({},{},{})", camera_slot, switch_immediately, settings.rotation[0].unwrap_or(0.0), settings.rotation[1].unwrap_or(0.0), settings.rotation[2].unwrap_or(0.0)); if (camera_slot as usize) < camera_slots.slots.len() { @@ -270,7 +277,8 @@ impl TerminalInlineObjects { camera_slots.current_slot = camera_slot as usize; } if let Some(ctype) = settings.camera_type { - camera_slots.slots[camera_slot as usize].1 = TerminalPresentation { mode: ctype }; + camera_slots.slots[camera_slot as usize].1 = + TerminalPresentation { mode: ctype }; } if let Some(px) = settings.offset[0] { camera_slots.slots[camera_slot as usize].0.camera_offset[0] = px; @@ -295,7 +303,7 @@ impl TerminalInlineObjects { } } None - }, + } RgpOperation::Register { object_id, format, diff --git a/src/keyboard.rs b/src/keyboard.rs index 2d5126b..5ca982d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -9,6 +9,7 @@ use bevy::prelude::*; use arboard::Clipboard; use crate::config::{AppConfig, BindingAction, FontConfig, KeyBindingConfig}; +use crate::inline::TerminalCameraViewSlots; use crate::mouse::{TerminalSelection, encode_mouse_wheel}; use crate::runtime::TerminalRuntime; use crate::scene::{ @@ -16,7 +17,6 @@ use crate::scene::{ TerminalPresentationMode, TerminalViewport, }; use crate::terminal::{TerminalRedrawState, TerminalSurface}; -use crate::inline::{TerminalCameraViewSlots}; /// Clipboard bridge for terminal copy and paste. pub struct TerminalClipboard { @@ -339,7 +339,9 @@ pub fn handle_keyboard_input( mut keyboard: Local, mut params: KeyboardSystemParams, ) { - params.presentation.mode = params.camera_slots.slots[params.camera_slots.current_slot].1.mode; + params.presentation.mode = params.camera_slots.slots[params.camera_slots.current_slot] + .1 + .mode; let view: &mut TerminalPlaneView = params.plane_view.as_mut(); *view = params.camera_slots.slots[params.camera_slots.current_slot].0; for event in keyboard_events.read() { @@ -373,9 +375,9 @@ pub fn handle_keyboard_input( params.selection.clear(); params.redraw.request(); continue; - }, + } BindingAction::TogglePersp3DMode => { - params.presentation.toggle_persp_mode(); + params.presentation.toggle_perspective_mode(); params.mobius_transition.stop(); params.selection.clear(); params.redraw.request(); diff --git a/src/mouse.rs b/src/mouse.rs index cf9a48e..c62ac7f 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -436,15 +436,11 @@ pub(crate) fn handle_mouse_input( forwarded_mouse.last_cell = Some(cell); } } - (MouseButton::Right, ButtonState::Pressed) - if presentation.mode.is_3d() => - { + (MouseButton::Right, ButtonState::Pressed) if presentation.mode.is_3d() => { plane_view.panning = true; plane_view.last_pan_cursor = selection.cursor_position(); } - (MouseButton::Right, ButtonState::Released) - if presentation.mode.is_3d() => - { + (MouseButton::Right, ButtonState::Released) if presentation.mode.is_3d() => { plane_view.panning = false; plane_view.last_pan_cursor = selection.cursor_position(); } @@ -497,8 +493,7 @@ pub(crate) fn handle_mouse_input( selection.clear(); redraw.request(); } - } else if presentation.mode.is_3d() && delta != 0.0 - { + } else if presentation.mode.is_3d() && delta != 0.0 { plane_view.zoom = (plane_view.zoom - delta).clamp(0.1, 4.0); redraw.request(); } diff --git a/src/plugin.rs b/src/plugin.rs index dc65af2..87319a5 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -44,7 +44,8 @@ impl Plugin for TerminalPlugin { Update, redraw_soft_terminal .after(handle_mouse_input) - .after(pump_pty_output).after(apply_inline_objects), + .after(pump_pty_output) + .after(apply_inline_objects), ) .add_systems(Update, sync_inline_objects.after(redraw_soft_terminal)) .add_systems(Update, sync_rgp_objects.after(sync_inline_objects)) diff --git a/src/rgp.rs b/src/rgp.rs index 1d10388..b95e794 100644 --- a/src/rgp.rs +++ b/src/rgp.rs @@ -194,9 +194,9 @@ pub fn consume_sequence(sequence: &[u8]) -> Option { match my_camera_type.as_str() { "Flat" => Some(TerminalPresentationMode::Flat2d), "Ortho" => Some(TerminalPresentationMode::Plane3d), - "Persp" => Some(TerminalPresentationMode::Persp3d), + "Persp" => Some(TerminalPresentationMode::Perspective3d), "Mobius" => Some(TerminalPresentationMode::Mobius3d), - _ => None + _ => None, } } else { None @@ -204,7 +204,7 @@ pub fn consume_sequence(sequence: &[u8]) -> Option { scale, offset: [px, py, pz], rotation: [rx, ry, rz], - } + }, }), "r" => Some(RgpOperation::Register { object_id: id?, @@ -295,14 +295,14 @@ pub enum RgpOperation { /// Register source. source: RgpRegisterSource, }, - /// Camera manipulation + /// Camera manipulation. Camera { /// Camera slot, 0 to just modify current camera camera_slot: u32, /// Defaults to 0, switches to the camera upon setting or not switch_immediately: bool, /// The settings: zoom/FOV, rotation, offset - settings: RgpCameraSettings + settings: RgpCameraSettings, }, /// Object placement. Place { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b83d2b1..a399d75 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -14,8 +14,8 @@ use bevy::prelude::*; use bevy::render::render_resource::{Extent3d, Face, TextureDimension, TextureFormat}; use crate::config::AppConfig; -use crate::terminal::TerminalSurface; use crate::inline::TerminalCameraViewSlots; +use crate::terminal::TerminalSurface; /// Marker for the 2D terminal sprite. #[derive(Component)] @@ -69,11 +69,12 @@ pub struct TerminalViewport { #[derive(Resource, Clone, Copy, PartialEq, Eq, Default)] pub enum TerminalPresentationMode { /// Flat 2D presentation. - #[default] Flat2d , + #[default] + Flat2d, /// Warped 3D presentation. Plane3d, /// Perspective 3D presentation. - Persp3d, + Perspective3d, /// Mobius-strip 3D presentation. Mobius3d, } @@ -102,19 +103,19 @@ impl TerminalPresentation { pub fn toggle_plane_mode(&mut self) { self.mode = match self.mode { TerminalPresentationMode::Flat2d => TerminalPresentationMode::Plane3d, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d | TerminalPresentationMode::Persp3d => { - TerminalPresentationMode::Flat2d - } + TerminalPresentationMode::Plane3d + | TerminalPresentationMode::Mobius3d + | TerminalPresentationMode::Perspective3d => TerminalPresentationMode::Flat2d, }; } /// Toggles the Perspective projection terminal view. - pub fn toggle_persp_mode(&mut self) { + pub fn toggle_perspective_mode(&mut self) { self.mode = match self.mode { - TerminalPresentationMode::Persp3d => TerminalPresentationMode::Flat2d, - TerminalPresentationMode::Flat2d | TerminalPresentationMode::Plane3d | TerminalPresentationMode::Mobius3d => { - TerminalPresentationMode::Persp3d - } + TerminalPresentationMode::Perspective3d => TerminalPresentationMode::Flat2d, + TerminalPresentationMode::Flat2d + | TerminalPresentationMode::Plane3d + | TerminalPresentationMode::Mobius3d => TerminalPresentationMode::Perspective3d, }; } @@ -122,9 +123,9 @@ impl TerminalPresentation { pub fn toggle_mobius_mode(&mut self) { self.mode = match self.mode { TerminalPresentationMode::Mobius3d => TerminalPresentationMode::Flat2d, - TerminalPresentationMode::Flat2d | TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => { - TerminalPresentationMode::Mobius3d - } + TerminalPresentationMode::Flat2d + | TerminalPresentationMode::Plane3d + | TerminalPresentationMode::Perspective3d => TerminalPresentationMode::Mobius3d, }; } } @@ -183,8 +184,16 @@ type PlaneMaterialQuery<'w, 's> = type PlaneTransformQuery<'w, 's> = Query<'w, 's, &'static mut Transform, With>; type PlaneBackTransformQuery<'w, 's> = Query<'w, 's, &'static mut Transform, With>; -type PlaneCameraQuery<'w, 's> = - Query<'w, 's, (&'static mut Projection, &'static mut Transform, &'static mut Camera), With>; +type PlaneCameraQuery<'w, 's> = Query< + 'w, + 's, + ( + &'static mut Projection, + &'static mut Transform, + &'static mut Camera, + ), + With, +>; #[derive(SystemParam)] pub(crate) struct PresentationParams<'w, 's> { @@ -265,7 +274,6 @@ pub fn setup_scene( Msaa::Off, )); - let pixmap = terminal.pixmap_dimensions(); let pixmap_width = pixmap.x; let pixmap_height = pixmap.y; @@ -495,7 +503,7 @@ pub(crate) fn apply_terminal_presentation( for (mut projection, mut transform, mut camera) in &mut plane_transforms.p2() { let active = &mut camera.is_active; if let Projection::Perspective(persp) = projection.as_mut() { - if presentation.mode == TerminalPresentationMode::Persp3d { + if presentation.mode == TerminalPresentationMode::Perspective3d { *active = true; let zoom = if is_mobius && mobius_transition.active { mobius_transition.current_zoom() @@ -507,7 +515,7 @@ pub(crate) fn apply_terminal_presentation( *active = false; } } else if let Projection::Orthographic(ortho) = projection.as_mut() { - if presentation.mode != TerminalPresentationMode::Persp3d { + if presentation.mode != TerminalPresentationMode::Perspective3d { *active = true; let zoom = if is_mobius && mobius_transition.active { mobius_transition.current_zoom() diff --git a/src/systems.rs b/src/systems.rs index 7142bd3..5e74ad8 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -23,8 +23,8 @@ use std::sync::mpsc::TryRecvError; use crate::config::{AppConfig, CURSOR_DEPTH}; use crate::inline::{ - InlineObject, TerminalInlineObjectPlane, TerminalInlineObjectSprite, TerminalInlineObjects, - TerminalRgpObject, TerminalCameraViewSlots, + InlineObject, TerminalCameraViewSlots, TerminalInlineObjectPlane, TerminalInlineObjectSprite, + TerminalInlineObjects, TerminalRgpObject, }; use crate::model::CursorModel; use crate::model::spawn_cursor_model; @@ -178,7 +178,11 @@ pub fn pump_pty_output( } else { None }; - let mut replies = inline_objects.consume_pty_output(&chunk, &mut runtime.parser, camera_slots.as_mut()); + let mut replies = inline_objects.consume_pty_output( + &chunk, + &mut runtime.parser, + camera_slots.as_mut(), + ); replies.extend(runtime.parser.callbacks_mut().take_replies()); for reply in replies { runtime.write_input(&reply); @@ -316,8 +320,16 @@ pub fn apply_inline_objects( ), >, ) { - let sprite_visibility = if presentation.mode.is_3d() { Visibility::Visible } else { Visibility::Hidden }; - let plane_visibility = if presentation.mode.is_3d() { Visibility::Hidden } else { Visibility::Visible }; + let sprite_visibility = if presentation.mode.is_3d() { + Visibility::Visible + } else { + Visibility::Hidden + }; + let plane_visibility = if presentation.mode.is_3d() { + Visibility::Hidden + } else { + Visibility::Visible + }; for mut visibility in &mut sprite_query { *visibility = sprite_visibility; @@ -472,8 +484,8 @@ pub(crate) fn sync_inline_objects(mut params: SyncInlineParams) { images, meshes, } = &mut params; - let force_warp_sync = presentation.mode.is_3d() && plane_warp.amount > 0.0 - && !inline_objects.anchors.is_empty(); + let force_warp_sync = + presentation.mode.is_3d() && plane_warp.amount > 0.0 && !inline_objects.anchors.is_empty(); if !force_warp_sync && !inline_objects.needs_sync(viewport.size, terminal.cols, terminal.rows) { return; } @@ -611,7 +623,11 @@ fn sync_kitty_inline_image( TerminalInlineObjectSprite, sprite, Transform::from_translation(Vec3::new(layout.center_x, layout.center_y, 5.0)), - if ctx.mode.is_3d() { Visibility::Visible } else { Visibility::Hidden }, + if ctx.mode.is_3d() { + Visibility::Visible + } else { + Visibility::Hidden + }, )); let x_segments = layout.columns.clamp(2, 24); @@ -881,8 +897,7 @@ pub(crate) fn sync_rgp_objects(mut params: RgpSyncParams) { *visibility = Visibility::Visible; } else { transform.translation = Vec3::new( - layout.center_x - + anchor.style.offset.x * (terminal.pixmap_dimensions().x as f32), + layout.center_x + anchor.style.offset.x * (terminal.pixmap_dimensions().x as f32), layout.center_y + bob + anchor.style.offset.y * (terminal.pixmap_dimensions().y as f32), @@ -1137,7 +1152,7 @@ pub fn animate_terminal_plane_warp( let needs_update = match presentation.mode { TerminalPresentationMode::Flat2d => false, - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => { + TerminalPresentationMode::Plane3d | TerminalPresentationMode::Perspective3d => { presentation.is_changed() || warp.is_changed() || warp.amount > 0.0 } // Reapply the strip every frame so mode switches and time-based motion are visible. @@ -1244,7 +1259,9 @@ fn apply_plane_warp( position[1] = point.y; position[2] = match mode { TerminalPresentationMode::Plane3d => point.z * direction, - TerminalPresentationMode::Flat2d | TerminalPresentationMode::Mobius3d | TerminalPresentationMode::Persp3d => point.z, + TerminalPresentationMode::Flat2d + | TerminalPresentationMode::Mobius3d + | TerminalPresentationMode::Perspective3d => point.z, }; } } @@ -1338,40 +1355,39 @@ fn cursor_pose( }; let (translation, rotation, visibility) = if ctx.mode.is_3d() { - ( - Vec3::new(local_x, local_y + bob, CURSOR_DEPTH), - Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25), - if !app_config.cursor.model.visible || screen.hide_cursor() { - Visibility::Hidden - } else { - Visibility::Visible - }, - ) - } else { - let Ok(plane_transform) = ctx.plane_query.single() else { - return (Vec3::ZERO, Quat::IDENTITY, scale, Visibility::Hidden); - }; - let plane_local_x = cursor_x / cols - 0.5; - let plane_local_y = 0.5 - (cursor_row + 0.5) / rows + plane_bob; - let local_position = plane_surface_point( - ctx.mode, - plane_local_x, - plane_local_y, - ctx.plane_warp_amount, - ctx.elapsed_secs, - app_config.cursor.model.plane_offset, - ctx.mobius_progress, - ); - ( - plane_transform.transform_point(local_position), - plane_transform.rotation - * (Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25)), - if app_config.cursor.model.visible { - Visibility::Visible - } else { - Visibility::Hidden - }, - ) + ( + Vec3::new(local_x, local_y + bob, CURSOR_DEPTH), + Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25), + if !app_config.cursor.model.visible || screen.hide_cursor() { + Visibility::Hidden + } else { + Visibility::Visible + }, + ) + } else { + let Ok(plane_transform) = ctx.plane_query.single() else { + return (Vec3::ZERO, Quat::IDENTITY, scale, Visibility::Hidden); + }; + let plane_local_x = cursor_x / cols - 0.5; + let plane_local_y = 0.5 - (cursor_row + 0.5) / rows + plane_bob; + let local_position = plane_surface_point( + ctx.mode, + plane_local_x, + plane_local_y, + ctx.plane_warp_amount, + ctx.elapsed_secs, + app_config.cursor.model.plane_offset, + ctx.mobius_progress, + ); + ( + plane_transform.transform_point(local_position), + plane_transform.rotation * (Quat::from_rotation_y(spin) * Quat::from_rotation_x(-0.25)), + if app_config.cursor.model.visible { + Visibility::Visible + } else { + Visibility::Hidden + }, + ) }; (translation, rotation, scale, visibility) @@ -1400,7 +1416,7 @@ fn plane_surface_point( ) -> Vec3 { match mode { TerminalPresentationMode::Flat2d => Vec3::new(local_x, local_y, depth_offset), - TerminalPresentationMode::Plane3d | TerminalPresentationMode::Persp3d => Vec3::new( + TerminalPresentationMode::Plane3d | TerminalPresentationMode::Perspective3d => Vec3::new( local_x, local_y, plane_surface_z(local_x, local_y, warp_amount, elapsed_secs) + depth_offset, From 9f3666093e4d52a55ad04d0c7525e8de284c6d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Sun, 5 Jul 2026 18:18:57 +0200 Subject: [PATCH 07/21] style: fix formatting --- src/scene/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e65824f..b555a33 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -17,8 +17,8 @@ use bevy::window::PrimaryWindow; use bevy::camera::visibility::NoFrustumCulling; use crate::config::AppConfig; -use crate::inline::TerminalCameraViewSlots; use crate::direct_render::{new_terminal_image, new_terminal_render_image}; +use crate::inline::TerminalCameraViewSlots; use crate::present::{TerminalPresentMaterial, fullscreen_quad}; use crate::runtime::TerminalRuntime; use crate::terminal::{TerminalLayout, TerminalSurface, render_scale_for_window}; From 41bfe1b95aba8060a427eebbcad38399cd644fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Sun, 5 Jul 2026 18:19:50 +0200 Subject: [PATCH 08/21] chore: address review comments --- src/inline.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/inline.rs b/src/inline.rs index ab0c1fd..b9ce36b 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -278,7 +278,6 @@ impl TerminalInlineObjects { switch_immediately, settings, } => { - //info!("camera settings for slot {}: set {}; rotation: ({},{},{})", camera_slot, switch_immediately, settings.rotation[0].unwrap_or(0.0), settings.rotation[1].unwrap_or(0.0), settings.rotation[2].unwrap_or(0.0)); if (camera_slot as usize) < camera_slots.slots.len() { if switch_immediately { camera_slots.current_slot = camera_slot as usize; @@ -293,15 +292,13 @@ impl TerminalInlineObjects { if let Some(py) = settings.offset[1] { camera_slots.slots[camera_slot as usize].0.camera_offset[1] = py; } - if let Some(pz) = settings.offset[2] { - camera_slots.slots[camera_slot as usize].0.camera_offset[2] = pz; - } + // The interactive camera currently exposes 2D pan plus yaw/pitch; ignore + // unsupported Z pan and roll values until those controls exist. + let _ = settings.offset[2]; if let Some(rx) = settings.rotation[0] { camera_slots.slots[camera_slot as usize].0.yaw = rx; } - if let Some(_ry) = settings.rotation[1] { - /*camera_slots.slots[camera_slot as usize].0.roll = ry;*/ - } + let _ = settings.rotation[1]; if let Some(rz) = settings.rotation[2] { camera_slots.slots[camera_slot as usize].0.pitch = rz; } From bf7fcc60043e6195c8ed8d8b9a49857194a645da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Sun, 5 Jul 2026 18:26:07 +0200 Subject: [PATCH 09/21] refactor: last touches --- README.md | 12 ++++- src/config.rs | 20 +++++++++ src/keyboard.rs | 92 +++++++++++++++++++++----------------- src/scene/mod.rs | 20 ++++----- widget/examples/big_rat.rs | 60 ++++++++++++++++++++++++- 5 files changed, 149 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 7ec24fd..0df8e72 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,8 @@ Other useful cursor fields are: | ----------------------------------------------- | -------------------- | | Ctrl+Alt+C | Copy selection | | Ctrl+Alt+V | Paste clipboard | -| Ctrl+Alt+Enter | Toggle 2D / 3D mode | +| Ctrl+Alt+Enter | Toggle 2D / ortho 3D | +| Ctrl+Alt+P | Toggle perspective | | Ctrl+Alt+M | Toggle Mobius mode | | Ctrl+Alt+Up | Increase warp | | Ctrl+Alt+Down | Decrease warp | @@ -136,6 +137,10 @@ Other useful cursor fields are: | Ctrl+- | Decrease font size | | Ctrl+Alt+0 | Reset font size | +In custom configs, `ToggleOrtho3DMode` is the current action name for the +orthographic 3D toggle. The old `Toggle3DMode` action remains supported as a +backward-compatible alias. + ## Inline 3D objects Ratty uses its own protocol, the [Ratty Graphics Protocol](protocols/graphics.md), @@ -146,6 +151,7 @@ RGP supports: - registering `.obj`, `.glb`, and `.stl` assets by path - placing them at terminal cell anchors - animation, scale, color, depth and other attributes +- camera control for flat, orthographic, perspective and Mobius views There is a Ratatui widget called `ratatui-rgp` available in [`widget/`](widget/) if you want to build your own terminal applications that involve inline 3D objects. @@ -154,7 +160,9 @@ There is a Ratatui widget called `ratatui-rgp` available in #### [Big rat](widget/examples/big_rat.rs) -Places a single oversized rat directly in your terminal: +Places a single oversized rat directly in your terminal. Press `v` in the demo +to cycle the Ratty camera protocol through flat, orthographic, perspective and +Mobius views.