Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ Other useful cursor fields are:
| ----------------------------------------------- | -------------------- |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>C</kbd> | Copy selection |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>V</kbd> | Paste clipboard |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Enter</kbd> | Toggle 2D / 3D mode |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Enter</kbd> | Toggle 2D / ortho 3D |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>P</kbd> | Toggle perspective |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>M</kbd> | Toggle Mobius mode |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Up</kbd> | Increase warp |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Down</kbd> | Decrease warp |
Expand All @@ -136,6 +137,10 @@ Other useful cursor fields are:
| <kbd>Ctrl</kbd>+<kbd>-</kbd> | Decrease font size |
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>0</kbd> | 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),
Expand All @@ -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.
Expand All @@ -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.

<div>
<video width="80%" src="https://github.com/user-attachments/assets/e955d09a-d0eb-4bad-b3b2-fc1331f49646"/>
Expand Down
20 changes: 19 additions & 1 deletion protocols/graphics.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Each object has:
- `p` [place object](#3-place-object)
- `u` [update object](#4-update-object)
- `d` [delete object](#5-delete-object)
- `c` [camera control](#5-camera-control)

### 1. Support Query

Expand All @@ -65,7 +66,7 @@ ESC _ ratty;g;s ESC \
Ratty replies:

```text
ESC _ ratty;g;s;v=1;fmt=obj|glb|stl;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;transform=1;update=1;normalize=1 ESC \
ESC _ ratty;g;s;v=1;fmt=obj|glb|stl;path=1;payload=1;chunk=1;anim=1;depth=1;color=1;brightness=1;transform=1;update=1;normalize=1;camera=1 ESC \
```

Fields:
Expand All @@ -79,6 +80,7 @@ Fields:
- `depth=1`: `depth=<f32>` placement is supported
- `color=1`: `color=<RRGGBB>` placement is supported
- `brightness=1`: `brightness=<f32>` 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
- `normalize=1`: `normalize=<0|1>` registration is supported for OBJ assets
Expand Down Expand Up @@ -219,6 +221,22 @@ Delete all Ratty graphics objects:
ESC _ ratty;g;d ESC \
```

### 6. Camera control

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:
Expand Down
29 changes: 26 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,13 @@ 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")]
#[serde(alias = "Toggle3DMode")]
ToggleOrtho3DMode,
/// Toggles the perspective terminal view.
#[serde(rename = "TogglePersp3DMode")]
TogglePersp3DMode,
/// Toggles the Mobius-strip terminal view.
#[serde(rename = "ToggleMobiusMode")]
ToggleMobiusMode,
Expand Down Expand Up @@ -490,3 +494,22 @@ fn parse_hex_color(value: &str) -> anyhow::Result<[u8; 3]> {
.with_context(|| format!("invalid blue component in {value}"))?;
Ok([r, g, b])
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn accepts_legacy_toggle_3d_mode_binding_action() {
let binding: KeyBindingConfig = toml::from_str(
r#"
key = "Enter"
with = "Control | alt"
action = "Toggle3DMode"
"#,
)
.expect("legacy Toggle3DMode action should deserialize");

assert_eq!(binding.action, BindingAction::ToggleOrtho3DMode);
}
}
63 changes: 59 additions & 4 deletions src/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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;
Expand All @@ -34,6 +36,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 {
Expand All @@ -54,6 +65,7 @@ impl TerminalInlineObjects {
&mut self,
chunk: &[u8],
parser: &mut vt100::Parser<CB>,
camera_slots: &mut TerminalCameraViewSlots,
) -> Vec<Vec<u8>> {
self.pending_bytes.extend_from_slice(chunk);
let mut replies = Vec::new();
Expand Down Expand Up @@ -89,8 +101,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());
let (handled, reply) = self.handle_apc_sequence(
&sequence,
parser.screen().cursor_position(),
camera_slots,
);
if let Some(reply) = reply {
replies.push(reply);
}
Expand Down Expand Up @@ -180,8 +195,9 @@ impl TerminalInlineObjects {
&mut self,
sequence: &[u8],
cursor_position: (u16, u16),
camera_slots: &mut TerminalCameraViewSlots,
) -> (bool, Option<Vec<u8>>) {
if let Some(reply) = self.handle_rgp_sequence(sequence) {
if let Some(reply) = self.handle_rgp_sequence(sequence, camera_slots) {
return (true, reply);
}

Expand Down Expand Up @@ -249,10 +265,49 @@ impl TerminalInlineObjects {
}
}

fn handle_rgp_sequence(&mut self, sequence: &[u8]) -> Option<Option<Vec<u8>>> {
fn handle_rgp_sequence(
&mut self,
sequence: &[u8],
camera_slots: &mut TerminalCameraViewSlots,
) -> Option<Option<Vec<u8>>> {
let operation = consume_rgp_sequence(sequence)?;
Some(match operation {
RgpOperation::SupportQuery => Some(support_reply()),
RgpOperation::Camera {
camera_slot,
switch_immediately,
settings,
} => {
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;
}
// 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;
}
let _ = settings.rotation[1];
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,
Expand Down
Loading
Loading