notedeck

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

room_state.rs (11503B)


      1 //! Space state management for nostrverse views
      2 
      3 use enostr::Pubkey;
      4 use glam::{Quat, Vec3};
      5 use renderbud::{Aabb, Model, ObjectId};
      6 
      7 /// Actions that can be triggered from the nostrverse view
      8 #[derive(Clone, Debug)]
      9 pub enum NostrverseAction {
     10     /// Object was moved to a new position (id, new_pos)
     11     MoveObject { id: String, position: Vec3 },
     12     /// Object was selected
     13     SelectObject(Option<String>),
     14     /// Space or object was edited, needs re-ingest
     15     SaveSpace,
     16     /// A new object was added
     17     AddObject(RoomObject),
     18     /// An object was removed
     19     RemoveObject(String),
     20     /// Duplicate the selected object
     21     DuplicateObject(String),
     22     /// Object was rotated (id, new rotation)
     23     RotateObject { id: String, rotation: Quat },
     24     /// Reset scene to the default demo space
     25     ResetSpace,
     26 }
     27 
     28 /// Reference to a nostrverse space
     29 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
     30 pub struct SpaceRef {
     31     /// Space identifier (d-tag)
     32     pub id: String,
     33     /// Space owner pubkey
     34     pub pubkey: Pubkey,
     35 }
     36 
     37 impl SpaceRef {
     38     pub fn new(id: String, pubkey: Pubkey) -> Self {
     39         Self { id, pubkey }
     40     }
     41 
     42     /// Get the NIP-33 "a" tag format
     43     pub fn to_naddr(&self) -> String {
     44         format!("{}:{}:{}", super::kinds::ROOM, self.pubkey.hex(), self.id)
     45     }
     46 }
     47 
     48 /// Parsed space definition from event
     49 #[derive(Clone, Debug)]
     50 pub struct SpaceInfo {
     51     pub name: String,
     52     /// Tilemap ground plane (if present)
     53     pub tilemap: Option<TilemapData>,
     54 }
     55 
     56 impl Default for SpaceInfo {
     57     fn default() -> Self {
     58         Self {
     59             name: "Untitled Space".to_string(),
     60             tilemap: None,
     61         }
     62     }
     63 }
     64 
     65 /// Converted space data: space info + objects.
     66 /// Used as the return type from convert_space to avoid fragile tuples.
     67 pub struct SpaceData {
     68     pub info: SpaceInfo,
     69     pub objects: Vec<RoomObject>,
     70 }
     71 
     72 /// Spatial location relative to the room or another object.
     73 /// Mirrors protoverse::Location for decoupling.
     74 #[derive(Clone, Debug, PartialEq)]
     75 pub enum ObjectLocation {
     76     Center,
     77     Floor,
     78     Ceiling,
     79     /// On top of another object (by id)
     80     TopOf(String),
     81     /// Near another object (by id)
     82     Near(String),
     83     Custom(String),
     84 }
     85 
     86 /// Protoverse object type, preserved for round-trip serialization
     87 #[derive(Clone, Debug, Default)]
     88 pub enum RoomObjectType {
     89     Table,
     90     Chair,
     91     Door,
     92     Light,
     93     #[default]
     94     Prop,
     95     Custom(String),
     96 }
     97 
     98 /// Object in a room - references a 3D model
     99 #[derive(Clone, Debug)]
    100 pub struct RoomObject {
    101     pub id: String,
    102     pub name: String,
    103     /// Protoverse cell type (table, chair, prop, etc.)
    104     pub object_type: RoomObjectType,
    105     /// URL to a glTF model (None = use placeholder geometry)
    106     pub model_url: Option<String>,
    107     /// Semantic location (e.g. "top-of obj1"), resolved to position at load time
    108     pub location: Option<ObjectLocation>,
    109     /// 3D position in world space
    110     pub position: Vec3,
    111     /// Base position from resolved location (used to compute offset for saving)
    112     pub location_base: Option<Vec3>,
    113     /// 3D rotation
    114     pub rotation: Quat,
    115     /// 3D scale
    116     pub scale: Vec3,
    117     /// Runtime: renderbud scene object handle
    118     pub scene_object_id: Option<ObjectId>,
    119     /// Runtime: loaded model handle
    120     pub model_handle: Option<Model>,
    121 }
    122 
    123 impl RoomObject {
    124     pub fn new(id: String, name: String, position: Vec3) -> Self {
    125         Self {
    126             id,
    127             name,
    128             object_type: RoomObjectType::Prop,
    129             model_url: None,
    130             location: None,
    131             position,
    132             location_base: None,
    133             rotation: Quat::IDENTITY,
    134             scale: Vec3::ONE,
    135             scene_object_id: None,
    136             model_handle: None,
    137         }
    138     }
    139 
    140     pub fn with_object_type(mut self, object_type: RoomObjectType) -> Self {
    141         self.object_type = object_type;
    142         self
    143     }
    144 
    145     pub fn with_model_url(mut self, url: String) -> Self {
    146         self.model_url = Some(url);
    147         self
    148     }
    149 
    150     pub fn with_location(mut self, loc: ObjectLocation) -> Self {
    151         self.location = Some(loc);
    152         self
    153     }
    154 
    155     pub fn with_scale(mut self, scale: Vec3) -> Self {
    156         self.scale = scale;
    157         self
    158     }
    159 }
    160 
    161 /// Parsed tilemap data — compact tile grid representation.
    162 #[derive(Clone, Debug)]
    163 pub struct TilemapData {
    164     /// Grid width in tiles
    165     pub width: u32,
    166     /// Grid height in tiles
    167     pub height: u32,
    168     /// Tile type names (index 0 = first name, etc.)
    169     pub tileset: Vec<String>,
    170     /// Tile indices, row-major. Length == 1 means fill-all with that value.
    171     pub tiles: Vec<u8>,
    172     /// Runtime: renderbud scene object handle for the tilemap mesh
    173     pub scene_object_id: Option<ObjectId>,
    174     /// Runtime: loaded model handle for the tilemap mesh
    175     pub model_handle: Option<Model>,
    176 }
    177 
    178 impl TilemapData {
    179     /// Get the tile index at grid position (x, y).
    180     pub fn tile_at(&self, x: u32, y: u32) -> u8 {
    181         if self.tiles.len() == 1 {
    182             return self.tiles[0];
    183         }
    184         let idx = (y * self.width + x) as usize;
    185         self.tiles.get(idx).copied().unwrap_or(0)
    186     }
    187 
    188     /// Encode tiles back to the compact data string.
    189     /// If all tiles are the same value, returns just that value.
    190     pub fn encode_data(&self) -> String {
    191         if self.tiles.len() == 1 {
    192             return self.tiles[0].to_string();
    193         }
    194         if self.tiles.iter().all(|&t| t == self.tiles[0]) {
    195             return self.tiles[0].to_string();
    196         }
    197         self.tiles
    198             .iter()
    199             .map(|t| t.to_string())
    200             .collect::<Vec<_>>()
    201             .join(" ")
    202     }
    203 
    204     /// Parse the compact data string into tile indices.
    205     pub fn decode_data(data: &str) -> Vec<u8> {
    206         let parts: Vec<&str> = data.split_whitespace().collect();
    207         if parts.len() == 1 {
    208             // Fill-all mode: single value
    209             let val = parts[0].parse::<u8>().unwrap_or(0);
    210             vec![val]
    211         } else {
    212             parts.iter().map(|s| s.parse::<u8>().unwrap_or(0)).collect()
    213         }
    214     }
    215 }
    216 
    217 /// A user present in a room (for rendering)
    218 #[derive(Clone, Debug)]
    219 pub struct RoomUser {
    220     pub pubkey: Pubkey,
    221     pub display_name: String,
    222     /// Authoritative position from last presence event
    223     pub position: Vec3,
    224     /// Velocity from last presence event (units/second)
    225     pub velocity: Vec3,
    226     /// Smoothed display position (interpolated for remote users, direct for self)
    227     pub display_position: Vec3,
    228     /// Monotonic time when last presence update was received (extrapolation base)
    229     pub update_time: f64,
    230     /// Whether this is the current user
    231     pub is_self: bool,
    232     /// Monotonic timestamp (seconds) of last presence update
    233     pub last_seen: f64,
    234     /// Nostr event created_at of the latest accepted presence event.
    235     /// Used to ignore out-of-order replaceable events.
    236     pub event_created_at: u64,
    237     /// Runtime: renderbud scene object handle for avatar
    238     pub scene_object_id: Option<ObjectId>,
    239     /// Runtime: loaded model handle for avatar
    240     pub model_handle: Option<Model>,
    241 }
    242 
    243 impl RoomUser {
    244     pub fn new(pubkey: Pubkey, display_name: String, position: Vec3) -> Self {
    245         Self {
    246             pubkey,
    247             display_name,
    248             position,
    249             velocity: Vec3::ZERO,
    250             display_position: position,
    251             update_time: 0.0,
    252             is_self: false,
    253             last_seen: 0.0,
    254             event_created_at: 0,
    255             scene_object_id: None,
    256             model_handle: None,
    257         }
    258     }
    259 
    260     pub fn with_self(mut self, is_self: bool) -> Self {
    261         self.is_self = is_self;
    262         self
    263     }
    264 }
    265 
    266 /// How a drag interaction is constrained
    267 #[derive(Clone, Debug)]
    268 pub enum DragMode {
    269     /// Free object: drag on world-space Y plane
    270     Free,
    271     /// Parented object: slide on parent surface, may break away
    272     Parented {
    273         parent_id: String,
    274         parent_scene_id: ObjectId,
    275         parent_aabb: Aabb,
    276         /// Local Y where child sits (e.g. parent top + child half height)
    277         local_y: f32,
    278     },
    279 }
    280 
    281 /// State for an active object drag in the 3D viewport
    282 pub struct DragState {
    283     /// ID of the object being dragged
    284     pub object_id: String,
    285     /// Offset from object position to the initial grab point
    286     pub grab_offset: Vec3,
    287     /// Y height of the drag constraint plane
    288     pub plane_y: f32,
    289     /// Drag constraint mode
    290     pub mode: DragMode,
    291 }
    292 
    293 /// State for a nostrverse view
    294 pub struct NostrverseState {
    295     /// Reference to the space being viewed
    296     pub space_ref: SpaceRef,
    297     /// Parsed space data (if loaded)
    298     pub space: Option<SpaceInfo>,
    299     /// Objects in the space
    300     pub objects: Vec<RoomObject>,
    301     /// Users currently in the space
    302     pub users: Vec<RoomUser>,
    303     /// Currently selected object ID
    304     pub selected_object: Option<String>,
    305     /// Whether we're in edit mode
    306     pub edit_mode: bool,
    307     /// Smoothed avatar yaw for lerped rotation
    308     pub smooth_avatar_yaw: f32,
    309     /// Space has unsaved edits
    310     pub dirty: bool,
    311     /// Active drag state for viewport object manipulation
    312     pub drag_state: Option<DragState>,
    313     /// Grid snap size in meters
    314     pub grid_snap: f32,
    315     /// Whether grid snapping is enabled
    316     pub grid_snap_enabled: bool,
    317     /// Whether rotate mode is active (R key toggle)
    318     pub rotate_mode: bool,
    319     /// Whether the current drag is a rotation drag (started on an object in rotate mode)
    320     pub rotate_drag: bool,
    321     /// Rotation snap increment in degrees (used when grid snap is enabled)
    322     pub rotation_snap: f32,
    323     /// Cached serialized scene text (avoids re-serializing every frame)
    324     pub cached_scene_text: String,
    325 }
    326 
    327 impl NostrverseState {
    328     pub fn new(space_ref: SpaceRef) -> Self {
    329         Self {
    330             space_ref,
    331             space: None,
    332             objects: Vec::new(),
    333             users: Vec::new(),
    334             selected_object: None,
    335             edit_mode: true,
    336             smooth_avatar_yaw: 0.0,
    337             dirty: false,
    338             drag_state: None,
    339             grid_snap: 0.5,
    340             grid_snap_enabled: false,
    341             rotate_mode: false,
    342             rotate_drag: false,
    343             rotation_snap: 15.0,
    344             cached_scene_text: String::new(),
    345         }
    346     }
    347 
    348     /// Add or update a user in the room
    349     pub fn update_user(&mut self, user: RoomUser) {
    350         if let Some(existing) = self.users.iter_mut().find(|u| u.pubkey == user.pubkey) {
    351             *existing = user;
    352         } else {
    353             self.users.push(user);
    354         }
    355     }
    356 
    357     /// Remove a user from the room
    358     pub fn remove_user(&mut self, pubkey: &Pubkey) {
    359         self.users.retain(|u| &u.pubkey != pubkey);
    360     }
    361 
    362     /// Get a mutable reference to an object by ID
    363     pub fn get_object_mut(&mut self, id: &str) -> Option<&mut RoomObject> {
    364         self.objects.iter_mut().find(|o| o.id == id)
    365     }
    366 
    367     /// Get the tilemap (if present in the space info)
    368     pub fn tilemap(&self) -> Option<&TilemapData> {
    369         self.space.as_ref()?.tilemap.as_ref()
    370     }
    371 
    372     /// Get the tilemap mutably (if present in the space info)
    373     pub fn tilemap_mut(&mut self) -> Option<&mut TilemapData> {
    374         self.space.as_mut()?.tilemap.as_mut()
    375     }
    376 
    377     /// Get the local user
    378     pub fn self_user(&self) -> Option<&RoomUser> {
    379         self.users.iter().find(|u| u.is_self)
    380     }
    381 
    382     /// Get the local user mutably
    383     pub fn self_user_mut(&mut self) -> Option<&mut RoomUser> {
    384         self.users.iter_mut().find(|u| u.is_self)
    385     }
    386 }