From dc52b856dace6964306f212ec2200c9a5b289670 Mon Sep 17 00:00:00 2001 From: kts of kettek Date: Fri, 16 Feb 2024 23:23:00 -0800 Subject: [PATCH] Move selection interaction to the undo system --- frontend/src/App.svelte | 7 ++- frontend/src/types/file.ts | 85 +++++++++++++++++++++++++++++++++ frontend/src/types/selection.ts | 21 +++++++- frontend/src/types/tools.ts | 21 ++++++-- 4 files changed, 127 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 1d1982e..06a8079 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -6,7 +6,7 @@ import FloatingPanel from './components/FloatingPanel.svelte' import { Palette, PaletteEntry, defaultPalette } from './types/palette' - import { LoadedFile } from './types/file' + import { LoadedFile, SelectionClearUndoable } from './types/file' import "carbon-components-svelte/css/all.css" import { Tabs, Tab, TabContent, Theme, Button, Modal, Truncate, ButtonSet, NumberInput } from "carbon-components-svelte" @@ -118,8 +118,13 @@ + focusedFile?.push(new SelectionClearUndoable())} /> swapTool(toolSelection)} /> swapTool(toolMove)} /> + currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: -1, y: 0, id: 0}):null} /> + currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 1, y: 0, id: 0}):null} /> + currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 0, y: -1, id: 0}):null} /> + currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 0, y: 1, id: 0}):null} /> swapTool(toolBrush)} /> currentTool===toolBrush?swapTool(toolPicker):null} on:release={()=>previousTool===toolBrush&¤tTool===toolPicker?swapTool(toolBrush):null} /> swapTool(toolFill)} /> diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index c9b6653..3f82ae1 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -92,4 +92,89 @@ export class PixelsPlaceUndoable implements Undoable { file.canvas.setPixel(pixel.x, pixel.y, pixel.index) } } +} + +export class SelectionSetUndoable implements Undoable { + private oldPixels: { x: number, y: number, marked: boolean }[] + private pixels: { x: number, y: number, marked: boolean }[] + private clear: boolean + + constructor(pixels: {x: number, y: number, marked: boolean}[], clear: boolean) { + this.pixels = pixels + this.clear = clear + } + apply(file: LoadedFile) { + if (!this.oldPixels) { + this.oldPixels = [] + for (let y = 0; y < file.selection.pixelMaskCanvasPixels.height; y++) { + for (let x = 0; x < file.selection.pixelMaskCanvasPixels.width; x++) { + this.oldPixels.push({x, y, marked: file.selection.pixelMaskCanvasPixels.data[(y * file.selection.pixelMaskCanvasPixels.width + x) * 4 + 3] !== 0}) + } + } + } + if (this.clear) { + file.selection.clear() + } + for (let pixel of this.pixels) { + file.selection.setPixel(pixel.x, pixel.y, pixel.marked) + } + } + unapply(file: LoadedFile) { + for (let pixel of this.oldPixels) { + file.selection.setPixel(pixel.x, pixel.y, pixel.marked) + } + } +} + +export class SelectionMoveUndoable implements Undoable { + private oldPixels: { x: number, y: number, marked: boolean }[] + + private dx: number + private dy: number + + constructor(dx: number, dy: number) { + this.dx = dx + this.dy = dy + } + apply(file: LoadedFile) { + if (!this.oldPixels) { + this.oldPixels = [] + for (let y = 0; y < file.selection.pixelMaskCanvasPixels.height; y++) { + for (let x = 0; x < file.selection.pixelMaskCanvasPixels.width; x++) { + this.oldPixels.push({x, y, marked: file.selection.pixelMaskCanvasPixels.data[(y * file.selection.pixelMaskCanvasPixels.width + x) * 4 + 3] !== 0}) + } + } + } + file.selection.move(this.dx, this.dy) + } + unapply(file: LoadedFile) { + file.selection.clear() + for (let pixel of this.oldPixels) { + file.selection.setPixel(pixel.x, pixel.y, pixel.marked) + } + } +} + +export class SelectionClearUndoable implements Undoable { + private oldPixels: { x: number, y: number, marked: boolean }[] + private oldActive: boolean + constructor() { + this.oldPixels = [] + } + apply(file: LoadedFile) { + this.oldActive = file.selection.active + for (let y = 0; y < file.selection.pixelMaskCanvasPixels.height; y++) { + for (let x = 0; x < file.selection.pixelMaskCanvasPixels.width; x++) { + this.oldPixels.push({x, y, marked: file.selection.pixelMaskCanvasPixels.data[(y * file.selection.pixelMaskCanvasPixels.width + x) * 4 + 3] !== 0}) + } + } + file.selection.clear() + file.selection.active = false + } + unapply(file: LoadedFile) { + for (let pixel of this.oldPixels) { + file.selection.setPixel(pixel.x, pixel.y, pixel.marked) + } + file.selection.active = this.oldActive + } } \ No newline at end of file diff --git a/frontend/src/types/selection.ts b/frontend/src/types/selection.ts index 1fed326..e5377f1 100644 --- a/frontend/src/types/selection.ts +++ b/frontend/src/types/selection.ts @@ -8,7 +8,7 @@ export class SelectionArea { private canvas: HTMLCanvasElement private pixelMaskCanvas: HTMLCanvasElement - private pixelMaskCanvasPixels: ImageData + public pixelMaskCanvasPixels: ImageData private redrawPixelMask: boolean private checkerboard: HTMLCanvasElement @@ -77,6 +77,25 @@ export class SelectionArea { this.refresh() } + public move(dx: number, dy: number) { + let pixelMaskCanvasPixels = new ImageData(this.pixelMaskCanvas.width, this.pixelMaskCanvas.height) + for (let y = 0; y < this.pixelMaskCanvas.height; y++) { + for (let x = 0; x < this.pixelMaskCanvas.width; x++) { + let i = (y * this.pixelMaskCanvas.width + x) * 4 + let ii = ((y + dy) * this.pixelMaskCanvas.width + (x + dx)) * 4 + if (ii >= 0 && ii < pixelMaskCanvasPixels.data.length) { + pixelMaskCanvasPixels.data[i + 0] = this.pixelMaskCanvasPixels.data[ii + 0] + pixelMaskCanvasPixels.data[i + 1] = this.pixelMaskCanvasPixels.data[ii + 1] + pixelMaskCanvasPixels.data[i + 2] = this.pixelMaskCanvasPixels.data[ii + 2] + pixelMaskCanvasPixels.data[i + 3] = this.pixelMaskCanvasPixels.data[ii + 3] + } + } + } + this.pixelMaskCanvasPixels = pixelMaskCanvasPixels + + this.redrawPixelMask = true + } + public refresh() { if (this.redrawPixelMask) { this.pixelMaskCanvas.getContext('2d').putImageData(this.pixelMaskCanvasPixels, 0, 0) diff --git a/frontend/src/types/tools.ts b/frontend/src/types/tools.ts index aa59a3d..7a0487f 100644 --- a/frontend/src/types/tools.ts +++ b/frontend/src/types/tools.ts @@ -1,4 +1,4 @@ -import { PixelPlaceUndoable, type LoadedFile, PixelsPlaceUndoable } from "./file" +import { PixelPlaceUndoable, type LoadedFile, PixelsPlaceUndoable, SelectionClearUndoable, SelectionSetUndoable, SelectionMoveUndoable } from "./file" import { FilledCircle, FilledSquare, type PixelPosition } from "./shapes" export interface ToolContext { @@ -252,15 +252,15 @@ export class SelectionTool implements Tool { } pointerUp(ctx: ToolContext & SelectionToolContext, ptr: Pointer) { if (this.startX === this.endX && this.startY === this.endY) { - ctx.file.selection.clear() - ctx.file.selection.active = false + ctx.file.push(new SelectionClearUndoable()) this.active = false return } let value = true + let clear = false if (!ptr.shift && !ptr.control) { - ctx.file.selection.clear() + clear = true } if (ptr.control) { value = false @@ -268,12 +268,15 @@ export class SelectionTool implements Tool { let {x: startX, y: startY, width, height} = this.getArea() + let pixels: { x: number, y: number, marked: boolean }[] = [] for (let x = startX; x <= startX+width-1; x++) { for (let y = startY; y <= startY+height-1; y++) { - ctx.file.selection.setPixel(x, y, value) + pixels.push({x, y, marked: value}) } } + ctx.file.push(new SelectionSetUndoable(pixels, clear)) + this.active = false } } @@ -289,6 +292,13 @@ export class MoveTool implements Tool { return this.active } + shift(ctx: ToolContext, ptr: Pointer) { + this.startX = 0 + this.startY = 0 + this.endX = ptr.x + this.endY = ptr.y + this.pointerUp(ctx, ptr) + } pointerDown(ctx: ToolContext, ptr: Pointer) { this.startX = this.endX = ptr.x this.startY = this.endY = ptr.y @@ -326,6 +336,7 @@ export class MoveTool implements Tool { } ctx.file.capture() + ctx.file.push(new SelectionMoveUndoable(-dx, -dy)) ctx.file.push(new PixelsPlaceUndoable(clearPixels)) ctx.file.push(new PixelsPlaceUndoable(pixels)) ctx.file.release()