Skip to content

Commit

Permalink
feat: allow to undo and redo erase operation (antfu#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
kermanx authored Feb 19, 2024
1 parent 21ada4c commit 4c7867d
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 27 deletions.
80 changes: 64 additions & 16 deletions packages/core/src/drauu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createNanoEvents } from 'nanoevents'
import { createModels } from './models'
import type { Brush, DrawingMode, EventsMap, Options } from './types'
import type { Brush, DrawingMode, EventsMap, Operation, Options } from './types'

export class Drauu {
el: SVGSVGElement | null = null
Expand All @@ -14,8 +14,10 @@ export class Drauu {
private _originalPointerId: number | null = null
private _models = createModels(this)
private _currentNode: SVGElement | undefined
private _undoStack: Node[] = []
private _opStack: Operation[] = []
private _opIndex = 0
private _disposables: (() => void)[] = []
private _elements: (SVGElement | null)[] = []

constructor(public options: Options = {}) {
if (!this.options.brush)
Expand Down Expand Up @@ -103,6 +105,7 @@ export class Drauu {
unmount() {
this._disposables.forEach(fn => fn())
this._disposables.length = 0
this._elements.length = 0
this.el = null

this._emitter.emit('unmounted')
Expand All @@ -113,29 +116,27 @@ export class Drauu {
}

undo() {
const el = this.el!
if (!el.lastElementChild)
if (!this.canUndo() || this.drawing)
return false
this._undoStack.push(el.lastElementChild.cloneNode(true))
el.lastElementChild.remove()
this._opStack[--this._opIndex].undo()
this._emitter.emit('changed')
return true
}

redo() {
if (!this._undoStack.length)
if (!this.canRedo() || this.drawing)
return false
this.el!.appendChild(this._undoStack.pop()!)
this._opStack[this._opIndex++].redo()
this._emitter.emit('changed')
return true
}

canRedo() {
return !!this._undoStack.length
return this._opIndex < this._opStack.length
}

canUndo() {
return !!this.el?.lastElementChild
return this._opIndex > 0
}

private eventMove(event: PointerEvent) {
Expand Down Expand Up @@ -172,10 +173,16 @@ export class Drauu {
if (!result) {
this.cancel()
}
else if (result === true) {
const el = this._currentNode!
this._appendNode(el)
this.commit({
undo: () => this._removeNode(el),
redo: () => this._restoreNode(el),
})
}
else {
if (result instanceof Element && result !== this._currentNode)
this._currentNode = result
this.commit()
this.commit(result)
}
this.drawing = false
this._emitter.emit('end')
Expand All @@ -200,15 +207,19 @@ export class Drauu {
}
}

private commit() {
this._undoStack.length = 0
private commit(op: Operation) {
this._opStack.length = this._opIndex
this._opStack.push(op)
this._opIndex++

const node = this._currentNode
this._currentNode = undefined
this._emitter.emit('committed', node)
}

clear() {
this._undoStack.length = 0
this._opStack.length = 0
this._opIndex = 0
this.cancel()
this.el!.innerHTML = ''
this._emitter.emit('changed')
Expand All @@ -230,6 +241,43 @@ export class Drauu {
this.clear()
this.el!.innerHTML = svg
}

/**
* @internal
*/
_appendNode(node: SVGElement) {
const last = this._elements.at(-1)
if (last)
last.after(node)
else
this.el!.append(node)
const index = this._elements.push(node) - 1
node.dataset.drauu_index = index.toString()
}

/**
* @internal
*/
_removeNode(node: SVGElement) {
node.remove()
this._elements[+node.dataset.drauu_index!] = null
}

/**
* @internal
*/
_restoreNode(node: SVGElement) {
const index = +node.dataset.drauu_index!
this._elements[index] = node
for (let i = index - 1; i >= 0; i--) {
const last = this._elements[i]
if (last) {
last.after(node)
return
}
}
this.el!.prepend(node)
}
}

export function createDrauu(options?: Options) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/models/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Brush, Point } from '../types'
import type { Brush, Operation, Point } from '../types'
import type { Drauu } from '../drauu'
import { D } from '../utils'

Expand All @@ -24,7 +24,7 @@ export abstract class BaseModel<T extends SVGElement> {
return false
}

onEnd(_point: Point): SVGElement | boolean | undefined {
onEnd(_point: Point): Operation | boolean | undefined {
return undefined
}

Expand Down
24 changes: 15 additions & 9 deletions packages/core/src/models/eraser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Point } from '../types'
import type { Operation, Point } from '../types'
import { BaseModel } from './base'

export class EraserModel extends BaseModel<SVGRectElement> {
Expand All @@ -8,6 +8,8 @@ export class EraserModel extends BaseModel<SVGRectElement> {
pathSubFactor = 20
pathFragments: { x1: number; x2: number; y1: number; y2: number; segment: number; element: any }[] = []

private _erased: SVGElement[] = []

onSelected(el: SVGSVGElement | null): void {
const calculatePathFragments = (children: any, element?: any): void => {
if (children && children.length) {
Expand Down Expand Up @@ -61,14 +63,18 @@ export class EraserModel extends BaseModel<SVGRectElement> {
return erased
}

override onEnd() {
override onEnd(): Operation {
this.svgPointPrevious = undefined
this.svgPointCurrent = undefined
return true
const erased = this._erased
this._erased = []
return {
undo: () => erased.forEach(v => this.drauu._restoreNode(v)),
redo: () => erased.forEach(v => this.drauu._removeNode(v)),
}
}

private checkAndEraseElement() {
const erased: number[] = []
if (this.pathFragments.length) {
for (let i = 0; i < this.pathFragments.length; i++) {
const segment = this.pathFragments[i]
Expand All @@ -79,15 +85,15 @@ export class EraserModel extends BaseModel<SVGRectElement> {
y2: this.svgPointCurrent!.y,
}
if (this.lineLineIntersect(segment, line)) {
segment.element.remove()
erased.push(i)
this.drauu._removeNode(segment.element)
this._erased.push(segment.element)
}
}
}

if (erased.length)
this.pathFragments = this.pathFragments.filter((v, i) => !erased.includes(i))
return erased.length > 0
if (this._erased.length)
this.pathFragments = this.pathFragments.filter(v => !this._erased.includes(v.element))
return this._erased.length > 0
}

private lineLineIntersect(line1: any, line2: any): boolean {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ export interface EventsMap {
mounted: () => void
unmounted: () => void
}

export interface Operation {
undo(): void
redo(): void
}

0 comments on commit 4c7867d

Please sign in to comment.