Move selection interaction to the undo system
parent
381d435fd7
commit
dc52b856da
|
@ -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&¤tTool===toolPicker?swapTool(toolBrush):null} />
|
||||
<Shortcut global cmd='fill' keys={['f']} on:trigger={()=>swapTool(toolFill)} />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue