playscii/edit_command.py

299 lines
10 KiB
Python

class EditCommand:
"undo/redo-able representation of an art edit (eg paint, erase) operation"
def __init__(self, art):
self.art = art
self.start_time = art.app.get_elapsed_time()
self.finish_time = None
# nested dictionary with frame(layer(column(row))) structure -
# this prevents multiple commands operating on the same tile
# from stomping each other
self.tile_commands = {}
def get_number_of_commands(self):
commands = 0
for frame in self.tile_commands.values():
for layer in frame.values():
for column in layer.values():
for _tile in column.values():
commands += 1
return commands
def __str__(self):
# get unique-ish ID from memory address
addr = self.__repr__()
addr = addr[addr.find("0") : -1]
s = "EditCommand_{}: {} tiles, time {}".format(
addr,
self.get_number_of_commands(),
self.finish_time,
)
return s
def add_command_tiles(self, new_command_tiles):
for ct in new_command_tiles:
# create new tables for frames/layers/columns if not present
if ct.frame not in self.tile_commands:
self.tile_commands[ct.frame] = {}
if ct.layer not in self.tile_commands[ct.frame]:
self.tile_commands[ct.frame][ct.layer] = {}
if ct.y not in self.tile_commands[ct.frame][ct.layer]:
self.tile_commands[ct.frame][ct.layer][ct.y] = {}
# preserve "before" state of any command we overwrite
if ct.x in self.tile_commands[ct.frame][ct.layer][ct.y]:
old_ct = self.tile_commands[ct.frame][ct.layer][ct.y][ct.x]
ct.set_before(old_ct.b_char, old_ct.b_fg, old_ct.b_bg, old_ct.b_xform)
self.tile_commands[ct.frame][ct.layer][ct.y][ct.x] = ct
def undo_commands_for_tile(self, frame, layer, x, y):
# no commands at all yet, maybe
if len(self.tile_commands) == 0:
return
# tile might not have undo commands, eg text entry beyond start region
if (
y not in self.tile_commands[frame][layer]
or x not in self.tile_commands[frame][layer][y]
):
return
self.tile_commands[frame][layer][y][x].undo()
def undo(self):
for frame in self.tile_commands.values():
for layer in frame.values():
for column in layer.values():
for tile_command in column.values():
tile_command.undo()
def apply(self):
for frame in self.tile_commands.values():
for layer in frame.values():
for column in layer.values():
for tile_command in column.values():
tile_command.apply()
class EntireArtCommand:
"""
undo/redo-able representation of a whole-art operation, eg:
resize/crop, run art script, add/remove layer, etc
"""
# art arrays to grab
array_types = ["chars", "fg_colors", "bg_colors", "uv_mods"]
def __init__(self, art, origin_x=0, origin_y=0):
self.art = art
# remember origin of resize command
self.origin_x, self.origin_y = origin_x, origin_y
self.before_frame = art.active_frame
self.before_layer = art.active_layer
self.start_time = self.finish_time = art.app.get_elapsed_time()
def save_tiles(self, before=True):
# save copies of tile data lists
prefix = "b" if before else "a"
for atype in self.array_types:
# save list as eg "b_chars" for "character data before operation"
src_data = getattr(self.art, atype)
var_name = "{}_{}".format(prefix, atype)
# deep copy each frame's data, else before == after
new_data = []
for frame in src_data:
new_data.append(frame.copy())
setattr(self, var_name, new_data)
if before:
self.before_size = (self.art.width, self.art.height)
else:
self.after_size = (self.art.width, self.art.height)
def undo(self):
# undo might remove frames/layers that were added
self.art.set_active_frame(self.before_frame)
self.art.set_active_layer(self.before_layer)
if self.before_size != self.after_size:
x, y = self.before_size
self.art.resize(x, y, self.origin_x, self.origin_y)
for atype in self.array_types:
new_data = getattr(self, "b_" + atype)
setattr(self.art, atype, new_data[:])
if self.before_size != self.after_size:
# Art.resize will set geo_changed and mark all frames changed
self.art.app.ui.adjust_for_art_resize(self.art)
self.art.mark_all_frames_changed()
def apply(self):
if self.before_size != self.after_size:
x, y = self.after_size
self.art.resize(x, y, self.origin_x, self.origin_y)
for atype in self.array_types:
new_data = getattr(self, "a_" + atype)
setattr(self.art, atype, new_data[:])
if self.before_size != self.after_size:
self.art.app.ui.adjust_for_art_resize(self.art)
self.art.mark_all_frames_changed()
class EditCommandTile:
def __init__(self, art):
self.art = art
self.creation_time = self.art.app.get_elapsed_time()
# initialize everything
# previously did 'string list of serialized items' + setattr
# which made prettier code but was slower
self.frame = self.layer = self.x = self.y = None
self.b_char = self.b_fg = self.b_bg = self.b_xform = None
self.a_char = self.a_fg = self.a_bg = self.a_xform = None
def __str__(self):
s = "F{} L{} {},{} @ {:.2f}: ".format(
self.frame,
self.layer,
str(self.x).rjust(2, "0"),
str(self.y).rjust(2, "0"),
self.creation_time,
)
s += "c{} f{} b{} x{} -> ".format(
self.b_char, self.b_fg, self.b_bg, self.b_xform
)
s += "c{} f{} b{} x{}".format(self.a_char, self.a_fg, self.a_bg, self.a_xform)
return s
def __eq__(self, value):
return (
self.frame == value.frame
and self.layer == value.layer
and self.x == value.x
and self.y == value.y
and self.b_char == value.b_char
and self.b_fg == value.b_fg
and self.b_bg == value.b_bg
and self.b_xform == value.b_xform
and self.a_char == value.a_char
and self.a_fg == value.a_fg
and self.a_bg == value.a_bg
and self.a_xform == value.a_xform
)
def copy(self):
"returns a deep copy of this tile command"
new_ect = EditCommandTile(self.art)
# TODO: old or new timestamp? does it matter?
# new_ect.creation_time = self.art.app.get_elapsed_time()
new_ect.creation_time = self.creation_time
# copy all properties
new_ect.frame, new_ect.layer = self.frame, self.layer
new_ect.x, new_ect.y = self.x, self.y
new_ect.b_char, new_ect.b_xform = self.b_char, self.b_xform
new_ect.b_fg, new_ect.b_bg = self.b_fg, self.b_bg
new_ect.a_char, new_ect.a_xform = self.a_char, self.a_xform
new_ect.a_fg, new_ect.a_bg = self.a_fg, self.a_bg
return new_ect
def set_tile(self, frame, layer, x, y):
self.frame, self.layer = frame, layer
self.x, self.y = x, y
def set_before(self, char, fg, bg, xform):
self.b_char, self.b_xform = char, xform
self.b_fg, self.b_bg = fg, bg
def set_after(self, char, fg, bg, xform):
self.a_char, self.a_xform = char, xform
self.a_fg, self.a_bg = fg, bg
def is_null(self):
return (
self.a_char == self.b_char
and self.a_fg == self.b_fg
and self.a_bg == self.b_bg
and self.a_xform == self.b_xform
)
def undo(self):
# tile's frame or layer may have been deleted
if self.layer > self.art.layers - 1 or self.frame > self.art.frames - 1:
return
if self.x >= self.art.width or self.y >= self.art.height:
return
tool = self.art.app.ui.selected_tool
set_all = (
tool.affects_char
and tool.affects_fg_color
and tool.affects_fg_color
and tool.affects_xform
)
self.art.set_tile_at(
self.frame,
self.layer,
self.x,
self.y,
self.b_char,
self.b_fg,
self.b_bg,
self.b_xform,
set_all,
)
def apply(self):
tool = self.art.app.ui.selected_tool
set_all = (
tool.affects_char
and tool.affects_fg_color
and tool.affects_fg_color
and tool.affects_xform
)
self.art.set_tile_at(
self.frame,
self.layer,
self.x,
self.y,
self.a_char,
self.a_fg,
self.a_bg,
self.a_xform,
set_all,
)
class CommandStack:
def __init__(self, art):
self.art = art
self.undo_commands, self.redo_commands = [], []
def __str__(self):
s = "stack for {}:\n".format(self.art.filename)
s += "===\nundo:\n"
for cmd in self.undo_commands:
s += str(cmd) + "\n"
s += "\n===\nredo:\n"
for cmd in self.redo_commands:
s += str(cmd) + "\n"
return s
def commit_commands(self, new_commands):
self.undo_commands += new_commands[:]
self.clear_redo()
def undo(self):
if len(self.undo_commands) == 0:
return
command = self.undo_commands.pop()
self.art.app.cursor.undo_preview_edits()
command.undo()
self.redo_commands.append(command)
self.art.app.cursor.update_cursor_preview()
def redo(self):
if len(self.redo_commands) == 0:
return
command = self.redo_commands.pop()
# un-apply cursor preview before applying redo, else preview edits
# edits will "stick"
self.art.app.cursor.undo_preview_edits()
command.apply()
# add to end of undo stack
self.undo_commands.append(command)
self.art.app.cursor.update_cursor_preview()
def clear_redo(self):
self.redo_commands = []