diff --git a/src/lib/players/eraserbrush.js b/src/lib/players/eraserbrush.js index 27aa24877d..fa75279cd4 100644 --- a/src/lib/players/eraserbrush.js +++ b/src/lib/players/eraserbrush.js @@ -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`. @@ -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) diff --git a/tests/unit/lib/eraserbrush.spec.js b/tests/unit/lib/eraserbrush.spec.js index b243ee5933..8b4fd08b36 100644 --- a/tests/unit/lib/eraserbrush.spec.js +++ b/tests/unit/lib/eraserbrush.spec.js @@ -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 { @@ -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() @@ -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])