notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 566efef3c45414a100c684ab82e4e11d94847d65
parent 990f5d48134e4eda9a45640d5696e8ca67cdd54f
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 11:34:25 -0800

nostrverse: persist rotation as human-readable Euler degrees

Store rotation in protoverse format as (rotation x y z) in degrees
with YXZ Euler order, so (rotation 0 90 0) reads as "90° around Y".
Conversion to/from Quat happens in the convert layer. Identity
rotations are omitted using angle_between check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_nostrverse/src/convert.rs | 23++++++++++++++++++++++-
Mcrates/notedeck_nostrverse/src/room_view.rs | 1+
Mcrates/protoverse/src/ast.rs | 10++++++++++
Mcrates/protoverse/src/parser.rs | 9+++++++++
Mcrates/protoverse/src/serializer.rs | 9+++++++++
5 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/crates/notedeck_nostrverse/src/convert.rs b/crates/notedeck_nostrverse/src/convert.rs @@ -1,7 +1,7 @@ //! Convert protoverse Space AST to renderer room state. use crate::room_state::{ObjectLocation, Room, RoomObject, RoomObjectType, RoomShape}; -use glam::Vec3; +use glam::{Quat, Vec3}; use protoverse::{Attribute, Cell, CellId, CellType, Location, ObjectType, Shape, Space}; /// Convert a parsed protoverse Space into a Room and its objects. @@ -92,6 +92,17 @@ fn collect_objects(space: &Space, id: CellId, objects: &mut Vec<RoomObject>) { let model_url = space.model_url(id).map(|s| s.to_string()); let location = space.location(id).map(location_from_protoverse); + let rotation = space + .rotation(id) + .map(|(x, y, z)| { + Quat::from_euler( + glam::EulerRot::YXZ, + (y as f32).to_radians(), + (x as f32).to_radians(), + (z as f32).to_radians(), + ) + }) + .unwrap_or(Quat::IDENTITY); let mut obj = RoomObject::new(obj_id, name, position) .with_object_type(object_type_from_cell(obj_type)); @@ -101,6 +112,7 @@ fn collect_objects(space: &Space, id: CellId, objects: &mut Vec<RoomObject>) { if let Some(loc) = location { obj = obj.with_location(loc); } + obj.rotation = rotation; objects.push(obj); } @@ -180,6 +192,15 @@ pub fn build_space(room: &Room, objects: &[RoomObject]) -> Space { pos.y as f64, pos.z as f64, )); + // Only emit rotation when non-identity to keep output clean + if obj.rotation.angle_between(Quat::IDENTITY) > 1e-4 { + let (y, x, z) = obj.rotation.to_euler(glam::EulerRot::YXZ); + attributes.push(Attribute::Rotation( + x.to_degrees() as f64, + y.to_degrees() as f64, + z.to_degrees() as f64, + )); + } let obj_attr_count = (attributes.len() as u32 - obj_attr_start) as u16; let obj_type = CellType::Object(match &obj.object_type { diff --git a/crates/notedeck_nostrverse/src/room_view.rs b/crates/notedeck_nostrverse/src/room_view.rs @@ -914,6 +914,7 @@ fn is_sexp_keyword(word: &str) -> bool { | "height" | "depth" | "position" + | "rotation" | "location" | "model-url" | "material" diff --git a/crates/protoverse/src/ast.rs b/crates/protoverse/src/ast.rs @@ -65,6 +65,8 @@ pub enum Attribute { Location(Location), State(CellState), Position(f64, f64, f64), + /// Euler rotation in degrees (X, Y, Z), applied in YXZ order. + Rotation(f64, f64, f64), ModelUrl(String), } @@ -206,6 +208,14 @@ impl Space { }) } + /// Euler rotation in degrees (X, Y, Z). + pub fn rotation(&self, id: CellId) -> Option<(f64, f64, f64)> { + self.attrs(id).iter().find_map(|a| match a { + Attribute::Rotation(x, y, z) => Some((*x, *y, *z)), + _ => None, + }) + } + pub fn model_url(&self, id: CellId) -> Option<&str> { self.attrs(id).iter().find_map(|a| match a { Attribute::ModelUrl(s) => Some(s.as_str()), diff --git a/crates/protoverse/src/parser.rs b/crates/protoverse/src/parser.rs @@ -216,6 +216,15 @@ impl<'a> Parser<'a> { _ => None, } } + "rotation" => { + let x = self.eat_number(); + let y = self.eat_number(); + let z = self.eat_number(); + match (x, y, z) { + (Some(x), Some(y), Some(z)) => Some(Attribute::Rotation(x, y, z)), + _ => None, + } + } "model-url" => self .eat_string() .map(|s| Attribute::ModelUrl(s.to_string())), diff --git a/crates/protoverse/src/serializer.rs b/crates/protoverse/src/serializer.rs @@ -104,6 +104,15 @@ fn write_attr(attr: &Attribute, out: &mut String) { format_number(*z) ); } + Attribute::Rotation(x, y, z) => { + let _ = write!( + out, + "(rotation {} {} {})", + format_number(*x), + format_number(*y), + format_number(*z) + ); + } Attribute::ModelUrl(s) => { let _ = write!(out, "(model-url \"{}\")", s); }