mirror of
https://github.com/1j01/textual-paint.git
synced 2024-12-22 14:21:33 +03:00
Store only sub-image for undo/redo
This commit is contained in:
parent
6d17f085f5
commit
15db3f4538
64
paint.py
64
paint.py
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user