//! Concrete editor commands that mutate the [`EditorState`](crate::state::EditorState). //! //! Routed through the [`CommandStack`](crate::command::CommandStack) so every //! one is undoable through the Edit menu, `Ctrl+Z`/`Ctrl+Y`, and the same path //! that future tools (transform gizmos, sculpt, paint) will use. //! //! Piece 6 ships the **first** commands so the undo plumbing is exercised //! end-to-end: //! //! - [`SetTransformCmd`] — change an entity's local [`Transform`]. Consecutive //! edits to the same entity coalesce via [`Command::merge`] so a slider drag //! or a (future) gizmo drag becomes one undo entry. //! - [`RenameCmd`] — rename an entity. //! //! Spawn/despawn aren't wired yet: round-tripping a despawn would need stable //! entity ids across re-spawn (the scene reuses ids), which is a Stage-7 //! design step. The hierarchy panel still offers Add/Delete; they bypass the //! stack today and are clearly labeled as "not undoable" in the shell. use std::any::Any; use oxide_engine::prelude::*; use crate::command::Command; use crate::state::EditorState; /// Replaces the open UI document's panel wholesale (widget tree + sizes). /// /// The UI canvas snapshots the panel before an edit and again after, so any /// structural change (add / remove / move a widget) or property change goes /// through one undoable command without per-operation bookkeeping. A panel is a /// small data tree, so cloning it for the snapshots is cheap. pub struct SetUiPanelCmd { /// The panel before the edit. pub before: UiPanel, /// The panel after the edit. pub after: UiPanel, /// Human-readable description for the Edit menu. pub label: String, } impl Command for SetUiPanelCmd { fn apply(&mut self, state: &mut EditorState) { if let Some(doc) = &mut state.ui_doc { doc.panel = self.after.clone(); doc.dirty = true; } } fn undo(&mut self, state: &mut EditorState) { if let Some(doc) = &mut state.ui_doc { doc.panel = self.before.clone(); doc.dirty = true; } } fn label(&self) -> String { self.label.clone() } fn as_any_mut(&mut self) -> &mut dyn Any { self } } /// Replaces an entity's local [`Transform`]. Coalesces consecutive edits to /// the same entity so an interactive drag is one undo entry. pub struct SetTransformCmd { pub entity: Entity, /// The transform before the first apply — preserved through merges so /// undo reverses the whole drag at once. pub before: Transform, /// The transform after the most recent apply. pub after: Transform, } impl SetTransformCmd { /// Builds the command, snapshotting the entity's current transform as the /// pre-edit state. Returns `None` if the entity has no transform (e.g. it /// was just despawned). pub fn new(state: &EditorState, entity: Entity, after: Transform) -> Option { let before = state.scene.local_transform(entity)?; Some(Self { entity, before, after, }) } } impl Command for SetTransformCmd { fn apply(&mut self, state: &mut EditorState) { state.scene.set_local_transform(self.entity, self.after); } fn undo(&mut self, state: &mut EditorState) { state.scene.set_local_transform(self.entity, self.before); } fn label(&self) -> String { "Edit Transform".to_owned() } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn merge(&mut self, next: &mut dyn Command) -> bool { let Some(next) = next.as_any_mut().downcast_mut::() else { return false; }; if next.entity != self.entity { return false; } // Absorb `next` by extending our `after` while preserving `before`, // so a long drag remains a single undo step. self.after = next.after; true } } /// Sets a single **reflected field** of a component on an entity, addressed by /// type name + field name and carried as RON. /// /// This is the generic counterpart to [`SetTransformCmd`]: the /// reflection-driven inspector emits one of these for *any* registered /// component's field, so a new component type becomes undoably editable with no /// new command type. Consecutive edits to the same `(entity, type, field)` /// coalesce via [`Command::merge`], so dragging a value slider is one undo /// entry. pub struct SetFieldCmd { pub entity: Entity, /// The registered type name (e.g. `"Transform"`). pub type_name: &'static str, /// The reflected field name (e.g. `"translation"`). pub field: &'static str, /// The field's RON before the first apply — preserved through merges. pub before: String, /// The field's RON after the most recent apply. pub after: String, } impl SetFieldCmd { /// Builds the command, snapshotting the field's current RON as the /// pre-edit state. Returns `None` if the field can't be read (unknown /// type/field, or the entity lacks the component). pub fn new( state: &EditorState, entity: Entity, type_name: &'static str, field: &'static str, after: String, ) -> Option { let before = state .registry .get_field(state.scene.world(), entity, type_name, field) .ok()?; Some(Self { entity, type_name, field, before, after, }) } } impl Command for SetFieldCmd { fn apply(&mut self, state: &mut EditorState) { // Disjoint borrows of EditorState: ®istry (receiver) + &mut scene // (the world). A write only fails if the entity/component vanished // between snapshot and apply, in which case there's nothing to do. let _ = state.registry.set_field( state.scene.world_mut(), self.entity, self.type_name, self.field, &self.after, ); } fn undo(&mut self, state: &mut EditorState) { let _ = state.registry.set_field( state.scene.world_mut(), self.entity, self.type_name, self.field, &self.before, ); } fn label(&self) -> String { format!("Edit {}.{}", self.type_name, self.field) } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn merge(&mut self, next: &mut dyn Command) -> bool { let Some(next) = next.as_any_mut().downcast_mut::() else { return false; }; // Only coalesce edits to the *same* field of the same component on the // same entity; preserve `before` so undo reverses the whole drag. if next.entity != self.entity || next.type_name != self.type_name || next.field != self.field { return false; } self.after = std::mem::take(&mut next.after); true } } /// Renames an entity. pub struct RenameCmd { pub entity: Entity, pub before: String, pub after: String, } impl RenameCmd { /// Snapshots the entity's current name as the pre-edit state. pub fn new(state: &EditorState, entity: Entity, after: String) -> Self { let before = state.scene.name(entity).unwrap_or_default(); Self { entity, before, after, } } } impl Command for RenameCmd { fn apply(&mut self, state: &mut EditorState) { state.scene.set_name(self.entity, self.after.clone()); } fn undo(&mut self, state: &mut EditorState) { state.scene.set_name(self.entity, self.before.clone()); } fn label(&self) -> String { format!("Rename to '{}'", self.after) } fn as_any_mut(&mut self) -> &mut dyn Any { self } } #[cfg(test)] mod tests { use super::*; use crate::command::CommandStack; fn state_with_entity() -> (EditorState, Entity) { let mut state = EditorState::new(); let e = state .scene .spawn("alpha", Transform::from_translation(Vec3::ZERO)); (state, e) } #[test] fn transform_undo_redo_round_trips() { let (mut state, e) = state_with_entity(); let target = Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)); let cmd = SetTransformCmd::new(&state, e, target).expect("transform present"); let mut stack: CommandStack = CommandStack::with_capacity(32); stack.push(cmd, &mut state); assert_eq!( state.scene.local_transform(e).unwrap().translation, target.translation ); assert!(stack.undo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::ZERO ); assert!(stack.redo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, target.translation ); } #[test] fn consecutive_transform_edits_coalesce_into_one_undo() { // Mirrors the "interactive drag" case: dozens of per-frame edits, one // undo step that returns to the pre-drag state. let (mut state, e) = state_with_entity(); let mut stack: CommandStack = CommandStack::with_capacity(32); for step in 1..=5 { let target = Transform::from_translation(Vec3::splat(step as f32)); let cmd = SetTransformCmd::new(&state, e, target).unwrap(); stack.push(cmd, &mut state); } assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::splat(5.0) ); // A single undo wipes the whole drag — that's the merge contract. assert!(stack.undo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::ZERO ); } #[test] fn set_field_undo_redo_round_trips() { let (mut state, e) = state_with_entity(); let cmd = SetFieldCmd::new( &state, e, "Transform", "translation", "(1.0,2.0,3.0)".into(), ) .expect("transform field readable"); let mut stack: CommandStack = CommandStack::with_capacity(32); stack.push(cmd, &mut state); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::new(1.0, 2.0, 3.0) ); assert!(stack.undo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::ZERO ); assert!(stack.redo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::new(1.0, 2.0, 3.0) ); } #[test] fn consecutive_field_edits_to_same_field_coalesce() { // A value-slider drag: many per-frame edits, one undo back to start. let (mut state, e) = state_with_entity(); let mut stack: CommandStack = CommandStack::with_capacity(32); for step in 1..=5 { let ron = format!("({0}.0,{0}.0,{0}.0)", step); let cmd = SetFieldCmd::new(&state, e, "Transform", "translation", ron).unwrap(); stack.push(cmd, &mut state); } assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::splat(5.0) ); assert!(stack.undo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::ZERO ); } #[test] fn set_field_edits_to_different_fields_do_not_coalesce() { // Editing translation then scale must be two undo steps, not one. let (mut state, e) = state_with_entity(); let mut stack: CommandStack = CommandStack::with_capacity(32); stack.push( SetFieldCmd::new( &state, e, "Transform", "translation", "(1.0,0.0,0.0)".into(), ) .unwrap(), &mut state, ); stack.push( SetFieldCmd::new(&state, e, "Transform", "scale", "(2.0,2.0,2.0)".into()).unwrap(), &mut state, ); // Undo reverses scale only. assert!(stack.undo(&mut state).is_some()); let t = state.scene.local_transform(e).unwrap(); assert_eq!(t.scale, Vec3::ONE); assert_eq!(t.translation, Vec3::new(1.0, 0.0, 0.0)); // A second undo reverses translation. assert!(stack.undo(&mut state).is_some()); assert_eq!( state.scene.local_transform(e).unwrap().translation, Vec3::ZERO ); } #[test] fn rename_undo_restores_previous_name() { let (mut state, e) = state_with_entity(); let cmd = RenameCmd::new(&state, e, "beta".into()); let mut stack: CommandStack = CommandStack::with_capacity(32); stack.push(cmd, &mut state); assert_eq!(state.scene.name(e).as_deref(), Some("beta")); assert!(stack.undo(&mut state).is_some()); assert_eq!(state.scene.name(e).as_deref(), Some("alpha")); } }