Store only sub-image for undo/redo

This commit is contained in:
Isaiah Odhner 2023-04-11 14:14:23 -04:00
parent 6d17f085f5
commit 15db3f4538

View File

@ -165,18 +165,23 @@ class AnsiArtDocument:
source_region = Region(0, 0, source.width, source.height) source_region = Region(0, 0, source.width, source.height)
if target_region is None: if target_region is None:
target_region = Region(0, 0, source_region.width, source_region.height) target_region = Region(0, 0, source_region.width, source_region.height)
offset = Offset(target_region.x - source_region.x, target_region.y - source_region.y) source_offset = source_region.offset
target_offset = target_region.offset
random_color = "rgb(" + str(randint(0, 255)) + "," + str(randint(0, 255)) + "," + str(randint(0, 255)) + ")" random_color = "rgb(" + str(randint(0, 255)) + "," + str(randint(0, 255)) + "," + str(randint(0, 255)) + ")"
for y in range(target_region.height): for y in range(target_region.height):
for x in range(target_region.width): for x in range(target_region.width):
# for attr in ["ch", "bg", "fg"]: if source_region.contains(x + source_offset.x, y + source_offset.y):
# self[attr][y + offset.y][x + offset.x] = source[attr][y - offset.y][x - offset.x] self.ch[y + target_offset.y][x + target_offset.x] = source.ch[y + source_offset.y][x + source_offset.x]
self.ch[y + offset.y][x + offset.x] = source.ch[y - offset.y][x - offset.x] self.bg[y + target_offset.y][x + target_offset.x] = source.bg[y + source_offset.y][x + source_offset.x]
self.bg[y + offset.y][x + offset.x] = source.bg[y - offset.y][x - offset.x] self.fg[y + target_offset.y][x + target_offset.x] = source.fg[y + source_offset.y][x + source_offset.x]
self.fg[y + offset.y][x + offset.x] = source.fg[y - offset.y][x - offset.x]
# debug # debug
# self.bg[y + offset.y][x + offset.x] = "rgb(" + str((x + offset.x) * 255 // self.width) + "," + str((y + offset.y) * 255 // self.height) + ",0)" # self.bg[y + target_offset.y][x + target_offset.x] = "rgb(" + str((x + source_offset.x) * 255 // self.width) + "," + str((y + source_offset.y) * 255 // self.height) + ",0)"
self.bg[y + offset.y][x + offset.x] = random_color self.bg[y + target_offset.y][x + target_offset.x] = random_color
else:
# debug
self.ch[y + target_offset.y][x + target_offset.x] = "?"
self.bg[y + target_offset.y][x + target_offset.x] = "#ff00ff"
self.fg[y + target_offset.y][x + target_offset.x] = "#000000"
def get_ansi(self) -> str: def get_ansi(self) -> str:
"""Get the ANSI representation of the document. Untested. This is a freebie from the AI.""" """Get the ANSI representation of the document. Untested. This is a freebie from the AI."""
@ -193,18 +198,22 @@ class Action:
"""An action that can be undone efficiently using a region update.""" """An action that can be undone efficiently using a region update."""
def __init__(self, name, document: AnsiArtDocument, region: Region = None) -> None: def __init__(self, name, document: AnsiArtDocument, region: Region = None) -> None:
"""Initialize the action.""" """Initialize the action using the document state before modification."""
if region is None: if region is None:
region = Region(0, 0, document.width, document.height) region = Region(0, 0, document.width, document.height)
self.name = name self.name = name
self.document = document self.live_document = document # only for undoing; TODO: move to parameter of undo()
self.region = region self.region = region
self.sub_image_before = AnsiArtDocument(region.width, region.height) self.update(document)
self.sub_image_before.copy_region(document, region)
def update(self, document: AnsiArtDocument) -> None:
"""Grabs the image data from the current region of the document."""
self.sub_image_before = AnsiArtDocument(self.region.width, self.region.height)
self.sub_image_before.copy_region(document, self.region)
def undo(self) -> None: def undo(self) -> None:
"""Undo this action. Note that a canvas refresh is not performed here.""" """Undo this action. Note that a canvas refresh is not performed here."""
self.document.copy_region(self.sub_image_before, target_region=self.region) self.live_document.copy_region(self.sub_image_before, target_region=self.region)
def bresenham_walk(x0: int, y0: int, x1: int, y1: int) -> None: def bresenham_walk(x0: int, y0: int, x1: int, y1: int) -> None:
"""Bresenham's line algorithm""" """Bresenham's line algorithm"""
@ -337,7 +346,7 @@ class PaintApp(App):
"""Called when selected_color changes.""" """Called when selected_color changes."""
self.query_one("#selected_color").styles.background = selected_color self.query_one("#selected_color").styles.background = selected_color
def stamp_brush(self, x: int, y: int) -> None: def stamp_brush(self, x: int, y: int, affected_region: Region) -> Region:
brush_diameter = 1 brush_diameter = 1
if self.selected_tool == Tool.brush: if self.selected_tool == Tool.brush:
brush_diameter = 3 brush_diameter = 3
@ -349,6 +358,9 @@ class PaintApp(App):
for j in range(brush_diameter): for j in range(brush_diameter):
if (i - brush_diameter // 2) ** 2 + (j - brush_diameter // 2) ** 2 <= (brush_diameter // 2) ** 2: if (i - brush_diameter // 2) ** 2 + (j - brush_diameter // 2) ** 2 <= (brush_diameter // 2) ** 2:
self.stamp_char(x + i - brush_diameter // 2, y + j - brush_diameter // 2) self.stamp_char(x + i - brush_diameter // 2, y + j - brush_diameter // 2)
# expand the affected region to include the brush
brush_diameter += 2 # safety margin
return affected_region.union(Region(x - brush_diameter // 2, y - brush_diameter // 2, brush_diameter, brush_diameter))
def stamp_char(self, x: int, y: int) -> None: def stamp_char(self, x: int, y: int) -> None:
if x < self.image.width and y < self.image.height and x >= 0 and y >= 0: if x < self.image.width and y < self.image.height and x >= 0 and y >= 0:
@ -395,20 +407,32 @@ class PaintApp(App):
if self.selected_tool != Tool.pencil and self.selected_tool != Tool.brush: if self.selected_tool != Tool.pencil and self.selected_tool != Tool.brush:
self.selected_tool = Tool.pencil self.selected_tool = Tool.pencil
# TODO: support other tools # TODO: support other tools
# TODO: track region for undo state and only refresh same region self.image_at_start = AnsiArtDocument(self.image.width, self.image.height)
self.image_at_start.copy_region(self.image)
region = Region(event.mouse_down_event.x, event.mouse_down_event.y, 1, 1)
if len(self.redos) > 0: if len(self.redos) > 0:
self.redos = [] self.redos = []
self.undos.append(Action(self.selected_tool.get_name(), self.image)) action = Action(self.selected_tool.get_name(), self.image)
self.stamp_brush(event.mouse_down_event.x, event.mouse_down_event.y) self.undos.append(action)
self.canvas.refresh() region = self.stamp_brush(event.mouse_down_event.x, event.mouse_down_event.y, region)
action.region = region
action.update(self.image_at_start)
self.canvas.refresh(region)
event.stop() event.stop()
def on_canvas_tool_update(self, event: Canvas.ToolUpdate) -> None: def on_canvas_tool_update(self, event: Canvas.ToolUpdate) -> None:
"""Called when the user is drawing on the canvas.""" """Called when the user is drawing on the canvas."""
mm = event.mouse_move_event mm = event.mouse_move_event
action = self.undos[-1]
affected_region = Region(mm.x, mm.y, 1, 1)
for x, y in bresenham_walk(mm.x - mm.delta_x, mm.y - mm.delta_y, mm.x, mm.y): for x, y in bresenham_walk(mm.x - mm.delta_x, mm.y - mm.delta_y, mm.x, mm.y):
self.stamp_brush(x, y) affected_region = self.stamp_brush(x, y, affected_region)
self.canvas.refresh()
# Update action region and image data
action.region = action.region.union(affected_region)
action.update(self.image_at_start)
self.canvas.refresh(affected_region)
event.stop() event.stop()
def on_key(self, event: events.Key) -> None: def on_key(self, event: events.Key) -> None: