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 }