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:
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 ---