from core.components import Transform, SpriteRenderer
from core.components import TilemapComponent
[docs]
class Command:
[docs]
def execute(self):
raise NotImplementedError
[docs]
def undo(self):
raise NotImplementedError
[docs]
def redo(self):
self.execute()
[docs]
class UndoManager:
def __init__(self, main_window=None):
self.undo_stack = []
self.redo_stack = []
self.max_stack = 50
self.main_window = main_window
[docs]
def push(self, command):
self.undo_stack.append(command)
self.redo_stack.clear()
if len(self.undo_stack) > self.max_stack:
self.undo_stack.pop(0)
# print(f"Pushed command: {type(command).__name__}")
[docs]
def undo(self):
if not self.undo_stack:
return
command = self.undo_stack.pop()
try:
command.undo()
self.redo_stack.append(command)
# print(f"Undid {type(command).__name__}")
if self.main_window:
self.main_window.hierarchy_dock.refresh()
# Update inspector if selection matches
if self.main_window.inspector_dock.current_entities:
# Re-set entities to refresh UI values
self.main_window.inspector_dock.set_entities(self.main_window.inspector_dock.current_entities)
except Exception as e:
print(f"Undo failed: {e}")
[docs]
def redo(self):
if not self.redo_stack:
return
command = self.redo_stack.pop()
try:
command.redo()
self.undo_stack.append(command)
# print(f"Redid {type(command).__name__}")
if self.main_window:
self.main_window.hierarchy_dock.refresh()
if self.main_window.inspector_dock.current_entities:
self.main_window.inspector_dock.set_entities(self.main_window.inspector_dock.current_entities)
except Exception as e:
print(f"Redo failed: {e}")
[docs]
class DeleteEntitiesCommand(Command):
def __init__(self, world, entities):
self.world = world
self.entities = list(entities) # Copy list
self.indices = [] # List of (entity, parent, index_in_parent, index_in_world)
# Capture state
for entity in self.entities:
parent = entity.parent
idx_in_parent = -1
if parent:
if entity in parent.children:
idx_in_parent = parent.children.index(entity)
idx_in_world = -1
if entity in getattr(self.world, '_entity_set', self.world.entities):
idx_map = getattr(self.world, '_entity_index', None)
if idx_map is not None and entity in idx_map:
idx_in_world = idx_map[entity]
else:
idx_in_world = self.world.entities.index(entity)
self.indices.append({
'entity': entity,
'parent': parent,
'idx_parent': idx_in_parent,
'idx_world': idx_in_world
})
[docs]
def execute(self):
# The actual deletion logic.
# Note: If called from outside (e.g. key press), the deletion might happen there.
# But for Redo, we need this.
# And usually we construct command, execute it, then push.
for item in self.indices:
entity = item['entity']
if entity in getattr(self.world, '_entity_set', self.world.entities):
self.world.destroy_entity(entity)
[docs]
def undo(self):
# Restore in reverse order of deletion to maintain indices if possible
# But indices were captured at start.
# We should restore based on captured indices.
# Sort indices by world index to restore order?
sorted_items = sorted(self.indices, key=lambda x: x['idx_world'])
for item in sorted_items:
entity = item['entity']
parent = item['parent']
idx_parent = item['idx_parent']
idx_world = item['idx_world']
# Add back to world
if idx_world >= 0 and idx_world <= len(self.world.entities):
self.world.entities.insert(idx_world, entity)
else:
self.world.entities.append(entity)
self.world._register_entity(entity)
self.world._sync_entity_indices()
# Add back to parent
if parent:
entity.parent = parent
if idx_parent >= 0 and idx_parent <= len(parent.children):
parent.children.insert(idx_parent, entity)
else:
parent.children.append(entity)
else:
entity.parent = None
[docs]
def redo(self):
self.execute()
[docs]
class DuplicateEntitiesCommand(Command):
def __init__(self, world, new_entities):
self.world = world
self.new_entities = list(new_entities)
# Capture parents to restore hierarchy on redo
self.parents = {e: e.parent for e in new_entities}
[docs]
def execute(self):
for entity in self.new_entities:
# Add to world if not already there
if entity not in getattr(self.world, '_entity_set', self.world.entities):
# To properly register it, we use create_entity logic or append
# But since it's already instantiated, we append and add to index
self.world.entities.append(entity)
self.world._register_entity(entity)
self.world._sync_entity_indices()
# Register components
for component in entity.components.values():
self.world.on_component_added(entity, component)
# Register groups
for group in entity.groups:
self.world.on_entity_group_changed(entity, group, added=True)
# Restore parent connection
parent = self.parents.get(entity)
if parent:
# add_child sets entity.parent = parent and adds to children list
if entity not in parent.children:
parent.add_child(entity)
else:
entity.parent = None
[docs]
def undo(self):
for entity in self.new_entities:
if entity in getattr(self.world, '_entity_set', self.world.entities):
self.world.destroy_entity(entity)
[docs]
def redo(self):
self.execute()
[docs]
class PropertyChangeCommand(Command):
def __init__(self, entities, component_type, attr_name, old_values, new_value):
self.entities = entities
self.component_type = component_type
self.attr_name = attr_name
self.old_values = old_values # List of values corresponding to entities
self.new_value = new_value # Single value applied to all
[docs]
def undo(self):
for i, entity in enumerate(self.entities):
comp = entity.get_component(self.component_type)
if comp:
setattr(comp, self.attr_name, self.old_values[i])
[docs]
def redo(self):
for entity in self.entities:
comp = entity.get_component(self.component_type)
if comp:
setattr(comp, self.attr_name, self.new_value)
[docs]
class MultiPropertyChangeCommand(Command):
def __init__(self, entities, component_type, attr_names, old_values_list, new_values):
self.entities = entities
self.component_type = component_type
self.attr_names = attr_names
self.old_values_list = old_values_list
self.new_values = new_values
[docs]
def undo(self):
for i, entity in enumerate(self.entities):
comp = entity.get_component(self.component_type)
if not comp:
continue
for attr_name, old_values in zip(self.attr_names, self.old_values_list):
setattr(comp, attr_name, old_values[i])
[docs]
def redo(self):
for entity in self.entities:
comp = entity.get_component(self.component_type)
if not comp:
continue
for attr_name, new_value in zip(self.attr_names, self.new_values):
setattr(comp, attr_name, new_value)
[docs]
class EntityPropertyChangeCommand(Command):
def __init__(self, entities, getter, setter, old_values, new_value):
self.entities = entities
self.getter = getter
self.setter = setter
self.old_values = old_values
self.new_value = new_value
[docs]
def undo(self):
for i, entity in enumerate(self.entities):
self.setter(entity, self.old_values[i])
[docs]
def redo(self):
for entity in self.entities:
self.setter(entity, self.new_value)
[docs]
class TilemapEditCommand(Command):
"""
Undoable set of tile edits for one tilemap layer.
changes: list of (x, y, old_value, new_value)
"""
def __init__(self, entity, layer_index: int, changes: list[tuple[int, int, int, int]]):
self.entity = entity
self.layer_index = int(layer_index)
self.changes = [(int(x), int(y), int(old), int(new)) for (x, y, old, new) in (changes or [])]
def _apply(self, use_new: bool):
if not self.entity:
return
tilemap = self.entity.get_component(TilemapComponent)
if not tilemap or not tilemap.layers:
return
idx = max(0, min(self.layer_index, len(tilemap.layers) - 1))
layer = tilemap.layers[idx]
for x, y, old_value, new_value in self.changes:
# Use world coordinates for infinite expansion
layer.set_world(x, y, new_value if use_new else old_value)
[docs]
def execute(self):
self._apply(True)
[docs]
def undo(self):
self._apply(False)
[docs]
def redo(self):
self._apply(True)