Move selection interaction to the undo system

main
kts of kettek 2024-02-16 23:23:00 -08:00
parent 381d435fd7
commit dc52b856da
4 changed files with 127 additions and 7 deletions

View File

@ -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 @@
<Button isSelected={currentTool === toolPicker} kind="ghost" size="small" icon={Eyedropper} iconDescription="pick" tooltipPosition="right" on:click={()=>swapTool(toolPicker)}></Button>
<Button isSelected={currentTool === toolErase} kind="ghost" size="small" icon={Erase} iconDescription="erase" tooltipPosition="right" on:click={()=>swapTool(toolErase)}></Button>
<Shortcuts group='editor2D'>
<Shortcut global cmd='clear selection' keys={['escape']} on:trigger={()=>focusedFile?.push(new SelectionClearUndoable())} />
<Shortcut global cmd='selection' keys={['s']} on:trigger={()=>swapTool(toolSelection)} />
<Shortcut global cmd='move' keys={['m']} on:trigger={()=>swapTool(toolMove)} />
<Shortcut global cmd='move left' keys={['arrowleft']} on:trigger={()=>currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: -1, y: 0, id: 0}):null} />
<Shortcut global cmd='move right' keys={['arrowright']} on:trigger={()=>currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 1, y: 0, id: 0}):null} />
<Shortcut global cmd='move up' keys={['arrowup']} on:trigger={()=>currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 0, y: -1, id: 0}):null} />
<Shortcut global cmd='move down' keys={['arrowdown']} on:trigger={()=>currentTool===toolMove?toolMove.shift({file: focusedFile}, {x: 0, y: 1, id: 0}):null} />
<Shortcut global cmd='brush' keys={['b']} on:trigger={()=>swapTool(toolBrush)} />
<Shortcut global cmd='brushToPicker' keys={['alt']} on:trigger={()=>currentTool===toolBrush?swapTool(toolPicker):null} on:release={()=>previousTool===toolBrush&&currentTool===toolPicker?swapTool(toolBrush):null} />
<Shortcut global cmd='fill' keys={['f']} on:trigger={()=>swapTool(toolFill)} />

View File

@ -92,4 +92,89 @@ export class PixelsPlaceUndoable implements Undoable<LoadedFile> {
file.canvas.setPixel(pixel.x, pixel.y, pixel.index)
}
}
}
export class SelectionSetUndoable implements Undoable<LoadedFile> {
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<LoadedFile> {
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<LoadedFile> {
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
}
}

View File

@ -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)

View File

@ -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()