commit 76ed91f7e0708e41b11a61d8b8bff832bfd9b2a1
parent 9ed92216cecd6fc7c2b066311b3eab11bfe07297
Author: William Casarin <jb55@jb55.com>
Date: Wed, 25 Feb 2026 14:24:29 -0800
nostrverse: break down show_room_view and render_editing_panel
Extract focused helpers from room_view.rs:
- handle_drag_start, compute_initial_drag: drag initiation logic
- apply_drag_update: drag state mutations
- handle_keyboard_input: shortcuts and WASD movement
- render_object_list: selectable object list
- render_object_inspector: transform/property editing
- render_grid_snap_controls: snap settings
- render_scene_preview: syntax-highlighted source view
Also fix clippy unnecessary_mut_passed in convert.rs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
2 files changed, 423 insertions(+), 338 deletions(-)
diff --git a/crates/notedeck_nostrverse/src/convert.rs b/crates/notedeck_nostrverse/src/convert.rs
@@ -146,7 +146,7 @@ pub fn build_space(info: &SpaceInfo, objects: &[RoomObject]) -> Space {
// Object cells (indices 2..)
for obj in objects {
- build_object_cell(obj, &mut cells, &mut attributes, &mut child_ids);
+ build_object_cell(obj, &mut cells, &mut attributes, &child_ids);
}
Space {
diff --git a/crates/notedeck_nostrverse/src/room_view.rs b/crates/notedeck_nostrverse/src/room_view.rs
@@ -169,6 +169,223 @@ fn compute_drag_update(
}
}
+/// Try to start an object drag. Returns the action (selection) if an object was picked.
+fn handle_drag_start(
+ state: &mut NostrverseState,
+ vp_x: f32,
+ vp_y: f32,
+ r: &mut renderbud::Renderer,
+) -> Option<NostrverseAction> {
+ let scene_id = r.pick(vp_x, vp_y)?;
+ let obj = state
+ .objects
+ .iter()
+ .find(|o| o.scene_object_id == Some(scene_id))?;
+
+ // Always select on drag start
+ r.set_selected(Some(scene_id));
+ state.selected_object = Some(obj.id.clone());
+
+ // In rotate mode, mark this as a rotation drag (don't start a position drag)
+ let drag_info = if state.rotate_mode {
+ state.rotate_drag = true;
+ None
+ } else {
+ compute_initial_drag(obj, state, vp_x, vp_y, r)
+ };
+
+ if let Some((mode, grab_offset, plane_y)) = drag_info {
+ state.drag_state = Some(DragState {
+ object_id: obj.id.clone(),
+ grab_offset,
+ plane_y,
+ mode,
+ });
+ }
+ None
+}
+
+/// Compute the initial drag mode and grab offset for an object.
+fn compute_initial_drag(
+ obj: &RoomObject,
+ state: &NostrverseState,
+ vp_x: f32,
+ vp_y: f32,
+ r: &renderbud::Renderer,
+) -> Option<(DragMode, Vec3, f32)> {
+ match &obj.location {
+ Some(ObjectLocation::TopOf(parent_id)) | Some(ObjectLocation::Near(parent_id)) => {
+ let parent = state.objects.iter().find(|o| o.id == *parent_id)?;
+ let parent_scene_id = parent.scene_object_id?;
+ let parent_aabb = r.model_bounds(parent.model_handle?)?;
+ let parent_world = r.world_matrix(parent_scene_id)?;
+
+ let child_half_h = obj
+ .model_handle
+ .and_then(|m| r.model_bounds(m))
+ .map(|b| (b.max.y - b.min.y) * 0.5)
+ .unwrap_or(0.0);
+ let local_y = if matches!(&obj.location, Some(ObjectLocation::TopOf(_))) {
+ parent_aabb.max.y + child_half_h
+ } else {
+ 0.0
+ };
+ let obj_world = parent_world.transform_point3(obj.position);
+ let plane_y = obj_world.y;
+ let hit = r
+ .unproject_to_plane(vp_x, vp_y, plane_y)
+ .unwrap_or(obj_world);
+ let local_hit = parent_world.inverse().transform_point3(hit);
+ let grab_offset = obj.position - local_hit;
+ Some((
+ DragMode::Parented {
+ parent_id: parent_id.clone(),
+ parent_scene_id,
+ parent_aabb,
+ local_y,
+ },
+ grab_offset,
+ plane_y,
+ ))
+ }
+ None | Some(ObjectLocation::Floor) => {
+ let plane_y = obj.position.y;
+ let hit = r
+ .unproject_to_plane(vp_x, vp_y, plane_y)
+ .unwrap_or(obj.position);
+ let grab_offset = obj.position - hit;
+ Some((DragMode::Free, grab_offset, plane_y))
+ }
+ _ => None, // Center/Ceiling/Custom: not draggable
+ }
+}
+
+/// Apply a computed drag update to state and renderer.
+fn apply_drag_update(
+ update: DragUpdate,
+ state: &mut NostrverseState,
+ r: &mut renderbud::Renderer,
+) -> Option<NostrverseAction> {
+ match update {
+ DragUpdate::Move { id, position } => Some(NostrverseAction::MoveObject { id, position }),
+ DragUpdate::Breakaway {
+ id,
+ world_pos,
+ new_grab_offset,
+ new_plane_y,
+ } => {
+ if let Some(obj) = state.objects.iter_mut().find(|o| o.id == id) {
+ if let Some(sid) = obj.scene_object_id {
+ r.set_parent(sid, None);
+ }
+ obj.position = world_pos;
+ obj.location = None;
+ obj.location_base = None;
+ state.dirty = true;
+ }
+ state.drag_state = Some(DragState {
+ object_id: id,
+ grab_offset: new_grab_offset,
+ plane_y: new_plane_y,
+ mode: DragMode::Free,
+ });
+ None
+ }
+ DragUpdate::SnapToParent {
+ id,
+ parent_id,
+ parent_scene_id,
+ parent_aabb,
+ local_pos,
+ local_y,
+ plane_y,
+ new_grab_offset,
+ } => {
+ if let Some(obj) = state.objects.iter_mut().find(|o| o.id == id) {
+ if let Some(sid) = obj.scene_object_id {
+ r.set_parent(sid, Some(parent_scene_id));
+ }
+ obj.position = local_pos;
+ obj.location = Some(ObjectLocation::TopOf(parent_id.clone()));
+ obj.location_base = Some(Vec3::new(0.0, local_y, 0.0));
+ state.dirty = true;
+ }
+ state.drag_state = Some(DragState {
+ object_id: id,
+ grab_offset: new_grab_offset,
+ plane_y,
+ mode: DragMode::Parented {
+ parent_id,
+ parent_scene_id,
+ parent_aabb,
+ local_y,
+ },
+ });
+ None
+ }
+ }
+}
+
+/// Handle keyboard shortcuts and WASD movement. Returns an action if triggered.
+fn handle_keyboard_input(
+ ui: &Ui,
+ state: &mut NostrverseState,
+ r: &mut renderbud::Renderer,
+) -> Option<NostrverseAction> {
+ let mut action = None;
+
+ // G key: toggle grid snap
+ if ui.input(|i| i.key_pressed(egui::Key::G)) {
+ state.grid_snap_enabled = !state.grid_snap_enabled;
+ }
+
+ // R key: toggle rotate mode
+ if ui.input(|i| i.key_pressed(egui::Key::R)) {
+ state.rotate_mode = !state.rotate_mode;
+ }
+
+ // 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;
+ let mut right = 0.0_f32;
+ let mut up = 0.0_f32;
+
+ ui.input(|i| {
+ if i.key_down(egui::Key::W) {
+ forward -= 1.0;
+ }
+ if i.key_down(egui::Key::S) {
+ forward += 1.0;
+ }
+ if i.key_down(egui::Key::D) {
+ right += 1.0;
+ }
+ if i.key_down(egui::Key::A) {
+ right -= 1.0;
+ }
+ if i.key_down(egui::Key::E) || i.key_down(egui::Key::Space) {
+ up += 1.0;
+ }
+ if i.key_down(egui::Key::Q) {
+ up -= 1.0;
+ }
+ });
+
+ if forward != 0.0 || right != 0.0 || up != 0.0 {
+ r.process_movement(forward, right, up, dt);
+ ui.ctx().request_repaint();
+ }
+
+ action
+}
+
/// Render the nostrverse room view with 3D scene
pub fn show_room_view(
ui: &mut Ui,
@@ -193,86 +410,8 @@ pub fn show_room_view(
&& let Some(pos) = response.interact_pointer_pos()
{
let vp = pos - rect.min.to_vec2();
- if let Some(scene_id) = r.pick(vp.x, vp.y)
- && let Some(obj) = state
- .objects
- .iter()
- .find(|o| o.scene_object_id == Some(scene_id))
- {
- // Always select on drag start
- r.set_selected(Some(scene_id));
- state.selected_object = Some(obj.id.clone());
-
- // In rotate mode, mark this as a rotation drag
- // (don't start a position drag)
- let drag_info = if state.rotate_mode {
- state.rotate_drag = true;
- None
- } else {
- match &obj.location {
- Some(ObjectLocation::TopOf(parent_id))
- | Some(ObjectLocation::Near(parent_id)) => {
- let parent_obj = state.objects.iter().find(|o| o.id == *parent_id);
- if let Some(parent) = parent_obj
- && let Some(parent_scene_id) = parent.scene_object_id
- && let Some(parent_model) = parent.model_handle
- && let Some(parent_aabb) = r.model_bounds(parent_model)
- && let Some(parent_world) = r.world_matrix(parent_scene_id)
- {
- let child_half_h = obj
- .model_handle
- .and_then(|m| r.model_bounds(m))
- .map(|b| (b.max.y - b.min.y) * 0.5)
- .unwrap_or(0.0);
- let local_y = if matches!(
- &obj.location,
- Some(ObjectLocation::TopOf(_))
- ) {
- parent_aabb.max.y + child_half_h
- } else {
- 0.0
- };
- let obj_world = parent_world.transform_point3(obj.position);
- let plane_y = obj_world.y;
- let hit = r
- .unproject_to_plane(vp.x, vp.y, plane_y)
- .unwrap_or(obj_world);
- let local_hit = parent_world.inverse().transform_point3(hit);
- let grab_offset = obj.position - local_hit;
- Some((
- DragMode::Parented {
- parent_id: parent_id.clone(),
- parent_scene_id,
- parent_aabb,
- local_y,
- },
- grab_offset,
- plane_y,
- ))
- } else {
- None
- }
- }
- None | Some(ObjectLocation::Floor) => {
- let plane_y = obj.position.y;
- let hit = r
- .unproject_to_plane(vp.x, vp.y, plane_y)
- .unwrap_or(obj.position);
- let grab_offset = obj.position - hit;
- Some((DragMode::Free, grab_offset, plane_y))
- }
- _ => None, // Center/Ceiling/Custom: not draggable
- }
- };
-
- if let Some((mode, grab_offset, plane_y)) = drag_info {
- state.drag_state = Some(DragState {
- object_id: obj.id.clone(),
- grab_offset,
- plane_y,
- mode,
- });
- }
+ if let Some(a) = handle_drag_start(state, vp.x, vp.y, &mut r) {
+ action = Some(a);
}
}
@@ -286,7 +425,6 @@ pub fn show_room_view(
let delta_x = response.drag_delta().x;
let angle = delta_x * ROTATE_SENSITIVITY;
let new_rotation = Quat::from_rotation_y(angle) * obj.rotation;
- // Snap to angle increments when grid snap is enabled
let new_rotation = if state.grid_snap_enabled {
let (_, y, _) = new_rotation.to_euler(glam::EulerRot::YXZ);
let snap_rad = state.rotation_snap.to_radians();
@@ -304,9 +442,7 @@ pub fn show_room_view(
if let Some(pos) = response.interact_pointer_pos() {
let vp = pos - rect.min.to_vec2();
let grid = state.grid_snap_enabled.then_some(state.grid_snap);
- // Borrow of state.drag_state is scoped to this call
let update = compute_drag_update(drag, vp.x, vp.y, grid, &r);
- // Borrow released — free to mutate state
// For free drags, check if we should snap to a parent
let update = if let Some(DragUpdate::Move {
ref id,
@@ -342,64 +478,10 @@ pub fn show_room_view(
update
};
- match update {
- Some(DragUpdate::Move { id, position }) => {
- action = Some(NostrverseAction::MoveObject { id, position });
- }
- Some(DragUpdate::Breakaway {
- id,
- world_pos,
- new_grab_offset,
- new_plane_y,
- }) => {
- if let Some(obj) = state.objects.iter_mut().find(|o| o.id == id) {
- if let Some(sid) = obj.scene_object_id {
- r.set_parent(sid, None);
- }
- obj.position = world_pos;
- obj.location = None;
- obj.location_base = None;
- state.dirty = true;
- }
- state.drag_state = Some(DragState {
- object_id: id,
- grab_offset: new_grab_offset,
- plane_y: new_plane_y,
- mode: DragMode::Free,
- });
+ if let Some(update) = update {
+ if let Some(a) = apply_drag_update(update, state, &mut r) {
+ action = Some(a);
}
- Some(DragUpdate::SnapToParent {
- id,
- parent_id,
- parent_scene_id,
- parent_aabb,
- local_pos,
- local_y,
- plane_y,
- new_grab_offset,
- }) => {
- if let Some(obj) = state.objects.iter_mut().find(|o| o.id == id) {
- if let Some(sid) = obj.scene_object_id {
- r.set_parent(sid, Some(parent_scene_id));
- }
- obj.position = local_pos;
- obj.location = Some(ObjectLocation::TopOf(parent_id.clone()));
- obj.location_base = Some(Vec3::new(0.0, local_y, 0.0));
- state.dirty = true;
- }
- state.drag_state = Some(DragState {
- object_id: id,
- grab_offset: new_grab_offset,
- plane_y,
- mode: DragMode::Parented {
- parent_id,
- parent_scene_id,
- parent_aabb,
- local_y,
- },
- });
- }
- None => {}
}
}
ui.ctx().request_repaint();
@@ -448,53 +530,8 @@ pub fn show_room_view(
}
}
- // G key: toggle grid snap
- if ui.input(|i| i.key_pressed(egui::Key::G)) {
- state.grid_snap_enabled = !state.grid_snap_enabled;
- }
-
- // R key: toggle rotate mode
- if ui.input(|i| i.key_pressed(egui::Key::R)) {
- state.rotate_mode = !state.rotate_mode;
- }
-
- // 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;
- let mut right = 0.0_f32;
- let mut up = 0.0_f32;
-
- ui.input(|i| {
- if i.key_down(egui::Key::W) {
- forward -= 1.0;
- }
- if i.key_down(egui::Key::S) {
- forward += 1.0;
- }
- if i.key_down(egui::Key::D) {
- right += 1.0;
- }
- if i.key_down(egui::Key::A) {
- right -= 1.0;
- }
- if i.key_down(egui::Key::E) || i.key_down(egui::Key::Space) {
- up += 1.0;
- }
- if i.key_down(egui::Key::Q) {
- up -= 1.0;
- }
- });
-
- if forward != 0.0 || right != 0.0 || up != 0.0 {
- r.process_movement(forward, right, up, dt);
- ui.ctx().request_repaint();
+ if let Some(a) = handle_keyboard_input(ui, state, &mut r) {
+ action = Some(a);
}
}
@@ -543,33 +580,12 @@ fn draw_info_overlay(painter: &egui::Painter, state: &NostrverseState, rect: Rec
painter.galley(text_pos, galley, Color32::PLACEHOLDER);
}
-/// Render the side panel with space editing, object list, and object inspector.
-pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option<NostrverseAction> {
- let mut action = None;
-
- // --- Space Properties ---
- if let Some(info) = &mut state.space {
- ui.strong("Space");
- ui.separator();
-
- let name_changed = ui
- .horizontal(|ui| {
- ui.label("Name:");
- ui.text_edit_singleline(&mut info.name).changed()
- })
- .inner;
-
- if name_changed {
- state.dirty = true;
- }
-
- ui.add_space(8.0);
- }
-
- // --- Object List ---
+/// Render the object list and add-object button. Returns an action if triggered.
+fn render_object_list(ui: &mut Ui, state: &NostrverseState) -> Option<NostrverseAction> {
ui.strong("Objects");
ui.separator();
+ let mut action = None;
let num_objects = state.objects.len();
for i in 0..num_objects {
let is_selected = state
@@ -597,130 +613,140 @@ pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option<
action = Some(NostrverseAction::AddObject(obj));
}
- ui.add_space(12.0);
-
- // --- Object Inspector ---
- if let Some(selected_id) = state.selected_object.as_ref()
- && let Some(obj) = state.objects.iter_mut().find(|o| &o.id == selected_id)
- {
- ui.strong("Inspector");
- ui.separator();
+ action
+}
- ui.small(format!("ID: {}", obj.id));
- ui.add_space(4.0);
+/// Render the object inspector panel for the selected object.
+/// Returns an action and whether any property changed.
+fn render_object_inspector(
+ ui: &mut Ui,
+ selected_id: &str,
+ obj: &mut RoomObject,
+ grid_snap_enabled: bool,
+ rotation_snap: f32,
+) -> (Option<NostrverseAction>, bool) {
+ let mut action = None;
- // Editable name
- let name_changed = ui
- .horizontal(|ui| {
- ui.label("Name:");
- ui.text_edit_singleline(&mut obj.name).changed()
- })
- .inner;
+ ui.strong("Inspector");
+ ui.separator();
- // Edit offset (relative to location base) or absolute position
- let base = obj.location_base.unwrap_or(Vec3::ZERO);
- let offset = obj.position - base;
- let mut ox = offset.x;
- let mut oy = offset.y;
- let mut oz = offset.z;
- let has_location = obj.location.is_some();
- let pos_label = if has_location { "Offset:" } else { "Pos:" };
- let pos_changed = ui
- .horizontal(|ui| {
- ui.label(pos_label);
- let x = ui
- .add(egui::DragValue::new(&mut ox).speed(0.1).prefix("x:"))
- .changed();
- let y = ui
- .add(egui::DragValue::new(&mut oy).speed(0.1).prefix("y:"))
- .changed();
- let z = ui
- .add(egui::DragValue::new(&mut oz).speed(0.1).prefix("z:"))
- .changed();
- x || y || z
- })
- .inner;
- obj.position = base + Vec3::new(ox, oy, oz);
+ ui.small(format!("ID: {}", obj.id));
+ ui.add_space(4.0);
- // Editable scale (uniform)
- let mut sx = obj.scale.x;
- let mut sy = obj.scale.y;
- let mut sz = obj.scale.z;
- let scale_changed = ui
- .horizontal(|ui| {
- ui.label("Scale:");
- let x = ui
- .add(
- egui::DragValue::new(&mut sx)
- .speed(0.05)
- .prefix("x:")
- .range(0.01..=100.0),
- )
- .changed();
- let y = ui
- .add(
- egui::DragValue::new(&mut sy)
- .speed(0.05)
- .prefix("y:")
- .range(0.01..=100.0),
- )
- .changed();
- let z = ui
- .add(
- egui::DragValue::new(&mut sz)
- .speed(0.05)
- .prefix("z:")
- .range(0.01..=100.0),
- )
- .changed();
- x || y || z
- })
- .inner;
- obj.scale = Vec3::new(sx, sy, sz);
-
- // Editable Y rotation (degrees)
- let (_, angle_y, _) = obj.rotation.to_euler(glam::EulerRot::YXZ);
- let mut deg = angle_y.to_degrees();
- let snap = state.grid_snap_enabled;
- let snap_deg = state.rotation_snap;
- let rot_changed = ui
- .horizontal(|ui| {
- ui.label("Rot Y:");
- let speed = if snap { snap_deg } else { 1.0 };
- ui.add(egui::DragValue::new(&mut deg).speed(speed).suffix("°"))
- .changed()
- })
- .inner;
- if rot_changed {
- if snap {
- deg = (deg / snap_deg).round() * snap_deg;
- }
- obj.rotation = Quat::from_rotation_y(deg.to_radians());
+ // Editable name
+ let name_changed = ui
+ .horizontal(|ui| {
+ ui.label("Name:");
+ ui.text_edit_singleline(&mut obj.name).changed()
+ })
+ .inner;
+
+ // Edit offset (relative to location base) or absolute position
+ let base = obj.location_base.unwrap_or(Vec3::ZERO);
+ let offset = obj.position - base;
+ let mut ox = offset.x;
+ let mut oy = offset.y;
+ let mut oz = offset.z;
+ let has_location = obj.location.is_some();
+ let pos_label = if has_location { "Offset:" } else { "Pos:" };
+ let pos_changed = ui
+ .horizontal(|ui| {
+ ui.label(pos_label);
+ let x = ui
+ .add(egui::DragValue::new(&mut ox).speed(0.1).prefix("x:"))
+ .changed();
+ let y = ui
+ .add(egui::DragValue::new(&mut oy).speed(0.1).prefix("y:"))
+ .changed();
+ let z = ui
+ .add(egui::DragValue::new(&mut oz).speed(0.1).prefix("z:"))
+ .changed();
+ x || y || z
+ })
+ .inner;
+ obj.position = base + Vec3::new(ox, oy, oz);
+
+ // Editable scale
+ let mut sx = obj.scale.x;
+ let mut sy = obj.scale.y;
+ let mut sz = obj.scale.z;
+ let scale_changed = ui
+ .horizontal(|ui| {
+ ui.label("Scale:");
+ let x = ui
+ .add(
+ egui::DragValue::new(&mut sx)
+ .speed(0.05)
+ .prefix("x:")
+ .range(0.01..=100.0),
+ )
+ .changed();
+ let y = ui
+ .add(
+ egui::DragValue::new(&mut sy)
+ .speed(0.05)
+ .prefix("y:")
+ .range(0.01..=100.0),
+ )
+ .changed();
+ let z = ui
+ .add(
+ egui::DragValue::new(&mut sz)
+ .speed(0.05)
+ .prefix("z:")
+ .range(0.01..=100.0),
+ )
+ .changed();
+ x || y || z
+ })
+ .inner;
+ obj.scale = Vec3::new(sx, sy, sz);
+
+ // Editable Y rotation (degrees)
+ let (_, angle_y, _) = obj.rotation.to_euler(glam::EulerRot::YXZ);
+ let mut deg = angle_y.to_degrees();
+ let rot_changed = ui
+ .horizontal(|ui| {
+ ui.label("Rot Y:");
+ let speed = if grid_snap_enabled {
+ rotation_snap
+ } else {
+ 1.0
+ };
+ ui.add(egui::DragValue::new(&mut deg).speed(speed).suffix("°"))
+ .changed()
+ })
+ .inner;
+ if rot_changed {
+ if grid_snap_enabled {
+ deg = (deg / rotation_snap).round() * rotation_snap;
}
+ obj.rotation = Quat::from_rotation_y(deg.to_radians());
+ }
- // Model URL (read-only for now)
- if let Some(url) = &obj.model_url {
- ui.add_space(4.0);
- ui.small(format!("Model: {}", url));
- }
+ // Model URL (read-only for now)
+ if let Some(url) = &obj.model_url {
+ ui.add_space(4.0);
+ ui.small(format!("Model: {}", url));
+ }
- if name_changed || pos_changed || scale_changed || rot_changed {
- state.dirty = true;
+ let changed = name_changed || pos_changed || scale_changed || rot_changed;
+
+ ui.add_space(8.0);
+ 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()));
+ }
+ });
- ui.add_space(8.0);
- 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()));
- }
- });
- }
+ (action, changed)
+}
- // --- Grid Snap ---
- ui.add_space(8.0);
+/// Render grid snap and rotation snap controls.
+fn render_grid_snap_controls(ui: &mut Ui, state: &mut NostrverseState) {
ui.horizontal(|ui| {
ui.checkbox(&mut state.grid_snap_enabled, "Grid Snap (G)");
if state.grid_snap_enabled {
@@ -743,19 +769,10 @@ pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option<
);
});
}
+}
- // --- Save button ---
- ui.add_space(12.0);
- ui.separator();
- let save_label = if state.dirty { "Save *" } else { "Save" };
- if ui
- .add_enabled(state.dirty, egui::Button::new(save_label))
- .clicked()
- {
- action = Some(NostrverseAction::SaveSpace);
- }
-
- // --- Scene body (syntax-highlighted, read-only) ---
+/// Render the syntax-highlighted scene source preview.
+fn render_scene_preview(ui: &mut Ui, state: &mut NostrverseState) {
// Only re-serialize when not actively dragging an object
if state.drag_state.is_none()
&& let Some(info) = &state.space
@@ -782,6 +799,74 @@ pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option<
ui.add(egui::Label::new(layout_job).wrap());
});
}
+}
+
+/// Render the side panel with space editing, object list, and object inspector.
+pub fn render_editing_panel(ui: &mut Ui, state: &mut NostrverseState) -> Option<NostrverseAction> {
+ let mut action = None;
+
+ // --- Space Properties ---
+ if let Some(info) = &mut state.space {
+ ui.strong("Space");
+ ui.separator();
+
+ let name_changed = ui
+ .horizontal(|ui| {
+ ui.label("Name:");
+ ui.text_edit_singleline(&mut info.name).changed()
+ })
+ .inner;
+
+ if name_changed {
+ state.dirty = true;
+ }
+
+ ui.add_space(8.0);
+ }
+
+ // --- Object List ---
+ if let Some(a) = render_object_list(ui, state) {
+ action = Some(a);
+ }
+
+ ui.add_space(12.0);
+
+ // --- Object Inspector ---
+ if let Some(selected_id) = state.selected_object.clone()
+ && let Some(obj) = state.objects.iter_mut().find(|o| o.id == selected_id)
+ {
+ let (inspector_action, changed) = render_object_inspector(
+ ui,
+ &selected_id,
+ obj,
+ state.grid_snap_enabled,
+ state.rotation_snap,
+ );
+ if let Some(a) = inspector_action {
+ action = Some(a);
+ }
+ if changed {
+ state.dirty = true;
+ }
+ }
+
+ // --- Grid Snap ---
+ ui.add_space(8.0);
+ render_grid_snap_controls(ui, state);
+
+ // --- Save button ---
+ ui.add_space(12.0);
+ ui.separator();
+ let save_label = if state.dirty { "Save *" } else { "Save" };
+ if ui
+ .add_enabled(state.dirty, egui::Button::new(save_label))
+ .clicked()
+ {
+ action = Some(NostrverseAction::SaveSpace);
+ }
+
+ // --- Scene body ---
+ render_scene_preview(ui, state);
action
}