Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/lib/players/eraserbrush.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import {
// gesture is degenerate (a single point). Matches fabric's own internal check.
const EMPTY_SVG_PATH = 'M 0 0 Q 0 0 0 0 L 0 0'

const inflateRect = (rect, amount) => ({
left: rect.left - amount,
top: rect.top - amount,
width: rect.width + amount * 2,
height: rect.height + amount * 2
})

const rectsOverlap = (a, b) =>
a.left <= b.left + b.width &&
a.left + a.width >= b.left &&
a.top <= b.top + b.height &&
a.top + a.height >= b.top

// An object's eraser: a group of paths in the object's LOCAL coordinates.
// FixedLayout + center origin: its dimensions don't change when paths are
// added; they're realigned onto the object by `_drawClipPath`.
Expand Down Expand Up @@ -238,10 +251,14 @@ export class EraserBrush extends PencilBrush {
path.setCoords && path.setCoords()
this.canvas.fire('before:path:created', { path })
const context = { targets: [], subTargets: [] }
// Fabric's object intersection ignores stroke width, so include the
// eraser radius in the bounds used to decide which objects receive a mask.
const eraserRadius = (path.strokeWidth || this.width || 1) / 2
const eraserBounds = inflateRect(path.getBoundingRect(), eraserRadius)
const tasks = this.canvas._objects.map(
obj =>
obj.erasable &&
obj.intersectsWithObject(path, true, true) &&
rectsOverlap(obj.getBoundingRect(), eraserBounds) &&
this._addPathToObjectEraser(obj, path, context)
)
await Promise.all(tasks)
Expand Down
46 changes: 44 additions & 2 deletions tests/unit/lib/eraserbrush.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ vi.mock('fabric', () => {
set(props) { Object.assign(this, props); return this }
calcTransformMatrix() { return [1, 0, 0, 1, 0, 0] }
clone() { return Promise.resolve(new FakePath(this.path, { ...this })) }
getBoundingRect() { return { left: 0, top: 0, width: 10, height: 0 } }
setCoords() {}
}
class FakePencilBrush {
Expand Down Expand Up @@ -228,11 +229,23 @@ describe('EraserBrush._addPathToObjectEraser', () => {
})

describe('EraserBrush._finalizeAndAddPath', () => {
const makeErasable = erasable => ({
const makeErasable = (
erasable,
bounds = { left: -5, top: -5, width: 20, height: 20 }
) => ({
erasable,
eraser: undefined,
group: undefined,
intersectsWithObject: () => true,
getBoundingRect: () => bounds,
intersectsWithObject: path => {
const pathBounds = path.getBoundingRect()
return (
bounds.left <= pathBounds.left + pathBounds.width &&
bounds.left + bounds.width >= pathBounds.left &&
bounds.top <= pathBounds.top + pathBounds.height &&
bounds.top + bounds.height >= pathBounds.top
)
},
calcTransformMatrix: () => [1, 0, 0, 1, 0, 0],
set: vi.fn(),
fire: vi.fn()
Expand Down Expand Up @@ -267,6 +280,35 @@ describe('EraserBrush._finalizeAndAddPath', () => {
expect(end.data.path).toBeDefined()
})

it('uses the eraser stroke width when detecting affected objects', async () => {
const radius = 10
const touchingSeparation = radius * 1.5
const outsideSeparation = radius * 4
const parallelStrokeBounds = centerY => ({
left: 0,
top: centerY - radius,
width: 10,
height: radius * 2
})

const touchingParallelStroke = makeErasable(
true,
parallelStrokeBounds(touchingSeparation)
)
const outsideParallelStroke = makeErasable(
true,
parallelStrokeBounds(outsideSeparation)
)
const canvas = makeCanvas([touchingParallelStroke, outsideParallelStroke])
const brush = new EraserBrush(canvas)
brush.width = radius * 2
brush._points = [{ x: 0, y: 0 }, { x: 10, y: 0 }]
await brush._finalizeAndAddPath()
const end = canvas._fired.find(f => f.name === 'erasing:end')
expect(end.data.targets).toContain(touchingParallelStroke)
expect(end.data.targets).not.toContain(outsideParallelStroke)
})

it('fires a bare erasing:end and creates no path when there are too few points', async () => {
const obj = makeErasable(true)
const canvas = makeCanvas([obj])
Expand Down