notedeck

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

commit 778ae178dd4ed75de9f758c04a7180ad82faaad3
parent 24257fb5220e960e1c7b843c3cbfb677efef6c77
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 10:33:38 -0800

nostrverse: add object duplication (Ctrl+D)

Duplicate the selected object with Ctrl+D or the Duplicate button
in the inspector. Copies all properties (model, scale, rotation,
location) with an X offset of 0.5m. Reuses the existing model
handle since it's shared GPU data.

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

Diffstat:
Mcrates/notedeck_nostrverse/src/lib.rs | 16++++++++++++++++
Mcrates/notedeck_nostrverse/src/room_state.rs | 2++
Mcrates/notedeck_nostrverse/src/room_view.rs | 18+++++++++++++++---
3 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs @@ -688,6 +688,22 @@ impl NostrverseApp { } self.state.dirty = true; } + NostrverseAction::DuplicateObject(id) => { + let Some(src) = self.state.objects.iter().find(|o| o.id == id).cloned() else { + return; + }; + let new_id = format!("{}-copy-{}", src.id, self.state.objects.len()); + let mut dup = src; + dup.id = new_id.clone(); + dup.name = format!("{} (copy)", dup.name); + dup.position.x += 0.5; + // Clear scene node — sync_scene will create a new one. + // Keep model_handle: it's a shared ref to loaded GPU data. + dup.scene_object_id = None; + self.state.objects.push(dup); + self.state.dirty = true; + self.state.selected_object = Some(new_id); + } } } } diff --git a/crates/notedeck_nostrverse/src/room_state.rs b/crates/notedeck_nostrverse/src/room_state.rs @@ -17,6 +17,8 @@ pub enum NostrverseAction { AddObject(RoomObject), /// An object was removed RemoveObject(String), + /// Duplicate the selected object + DuplicateObject(String), } /// Reference to a nostrverse room diff --git a/crates/notedeck_nostrverse/src/room_view.rs b/crates/notedeck_nostrverse/src/room_view.rs @@ -286,6 +286,13 @@ pub fn show_room_view( state.grid_snap_enabled = !state.grid_snap_enabled; } + // Ctrl+D: duplicate selected object + if ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::D)) + && let Some(id) = state.selected_object.clone() + { + action = Some(NostrverseAction::DuplicateObject(id)); + } + // WASD + QE movement: always available let dt = ui.input(|i| i.stable_dt); let mut forward = 0.0_f32; @@ -567,9 +574,14 @@ pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option< } ui.add_space(8.0); - if ui.button("Delete Object").clicked() { - action = Some(NostrverseAction::RemoveObject(selected_id.to_owned())); - } + ui.horizontal(|ui| { + if ui.button("Duplicate").clicked() { + action = Some(NostrverseAction::DuplicateObject(selected_id.to_owned())); + } + if ui.button("Delete").clicked() { + action = Some(NostrverseAction::RemoveObject(selected_id.to_owned())); + } + }); } // --- Grid Snap ---