Import Oxide engine (Stages 0–10) under MIT license

Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
+415
View File
@@ -0,0 +1,415 @@
//! 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<EditorState> 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<Self> {
let before = state.scene.local_transform(entity)?;
Some(Self {
entity,
before,
after,
})
}
}
impl Command<EditorState> 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<EditorState>) -> bool {
let Some(next) = next.as_any_mut().downcast_mut::<SetTransformCmd>() 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<Self> {
let before = state
.registry
.get_field(state.scene.world(), entity, type_name, field)
.ok()?;
Some(Self {
entity,
type_name,
field,
before,
after,
})
}
}
impl Command<EditorState> for SetFieldCmd {
fn apply(&mut self, state: &mut EditorState) {
// Disjoint borrows of EditorState: &registry (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<EditorState>) -> bool {
let Some(next) = next.as_any_mut().downcast_mut::<SetFieldCmd>() 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<EditorState> 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<EditorState> = 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<EditorState> = 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<EditorState> = 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<EditorState> = 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<EditorState> = 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<EditorState> = 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"));
}
}