diff --git a/CHANGELOG.md b/CHANGELOG.md index 499011a1e1b..5045ab38c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,9 @@ - fix(Utils): fix exported svg color [#9408](https://github.com/fabricjs/fabric.js/pull/9408) ## [6.0.0-beta13] +- fix(): `searchPossibleTargets` `targets` value [#9343](https://github.com/fabricjs/fabric.js/pull/9343) + +## [6.0.0-b3] - fix(Textbox): implemente a fix for the style shifting issues on new lines [#9197](https://github.com/fabricjs/fabric.js/pull/9197) - Fix(Control) fix a regression in `wrap with fixed anchor`, regression from #8400 [#9326](https://github.com/fabricjs/fabric.js/pull/9326) diff --git a/src/canvas/Canvas.ts b/src/canvas/Canvas.ts index db1cbb07706..9a55c2933d9 100644 --- a/src/canvas/Canvas.ts +++ b/src/canvas/Canvas.ts @@ -393,14 +393,13 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { * Override at will */ protected findDragTargets(e: DragEvent) { - this.targets = []; - const target = this._searchPossibleTargets( + const { target, targets } = this.findTargets( this._objects, this.getViewportPoint(e) ); return { target, - targets: [...this.targets], + targets: target ? targets.slice(0, targets.indexOf(target)) : targets, }; } @@ -1404,10 +1403,10 @@ export class Canvas extends SelectableCanvas implements CanvasOptions { const pointer = this.getViewportPoint(e); target = // first search active objects for a target to remove - this.searchPossibleTargets(prevActiveObjects, pointer) || + this.findTargets(prevActiveObjects, pointer).target || // if not found, search under active selection for a target to add // `prevActiveObjects` will be searched but we already know they will not be found - this.searchPossibleTargets(this._objects, pointer); + this.findTargets(this._objects, pointer).target; // if nothing is found bail out if (!target || !target.selectable) { return false; diff --git a/src/canvas/SelectableCanvas.ts b/src/canvas/SelectableCanvas.ts index 7a3fce244d9..f4ff3361977 100644 --- a/src/canvas/SelectableCanvas.ts +++ b/src/canvas/SelectableCanvas.ts @@ -689,11 +689,11 @@ export class SelectableCanvas } /** - * Method that determines what object we are clicking on + * Determines what object and its sub targets {@link e} should target * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target * or the outside part of the corner. * @param {Event} e mouse event - * @return {FabricObject | null} the target found + * @return {FabricObject | undefined} the target found */ findTarget(e: TPointerEvent): FabricObject | undefined { if (this.skipTargetFind) { @@ -704,45 +704,59 @@ export class SelectableCanvas activeObject = this._activeObject, aObjects = this.getActiveObjects(); - this.targets = []; - if (activeObject && aObjects.length >= 1) { if (activeObject.findControl(pointer, isTouchEvent(e))) { // if we hit the corner of the active object, let's return that. return activeObject; - } else if ( - aObjects.length > 1 && - // check pointer is over active selection and possibly perform `subTargetCheck` - this.searchPossibleTargets([activeObject], pointer) - ) { + } + + // check pointer is over active selection and possibly perform `subTargetCheck` + const { target: selectedTarget, targets: selectedTargets } = + this.findTargets([activeObject], pointer); + + if (selectedTarget && aObjects.length > 1) { // active selection does not select sub targets like normal groups + // remove active selection for the array + this.targets = selectedTargets.slice(0, -1); return activeObject; - } else if ( - activeObject === this.searchPossibleTargets([activeObject], pointer) - ) { + } else if (activeObject === selectedTarget) { // active object is not an active selection if (!this.preserveObjectStacking) { + this.targets = selectedTargets.slice( + 0, + selectedTargets.indexOf(activeObject) + ); return activeObject; } else { - const subTargets = this.targets; - this.targets = []; - const target = this.searchPossibleTargets(this._objects, pointer); + const { target, targets: canvasTargets } = this.findTargets( + this._objects, + pointer + ); + if ( e[this.altSelectionKey as ModifierKey] && target && target !== activeObject ) { // alt selection: select active object even though it is not the top most target - // restore targets - this.targets = subTargets; + this.targets = canvasTargets.slice( + 0, + canvasTargets.indexOf(activeObject) + ); return activeObject; } + + this.targets = target + ? canvasTargets.slice(0, canvasTargets.indexOf(target)) + : canvasTargets; return target; } } } - return this.searchPossibleTargets(this._objects, pointer); + const { target, targets } = this.findTargets(this._objects, pointer); + this.targets = target ? targets.slice(0, targets.indexOf(target)) : targets; + return target; } /** @@ -819,72 +833,81 @@ export class SelectableCanvas } /** - * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted - * @param {Array} [objects] objects array to look into - * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {FabricObject} **top most object from given `objects`** that contains pointer - * @private + * Search for objects containing {@link pointer}. + * + * @param {FabricObject[]} objects objects array to look into + * @param {Point} pointer point canvas element plane coordinates to check + * @param {boolean} [param2.searchStrategy] strategy + * @returns {FabricObject[]} path of objects starting from **top most** object on screen. */ - _searchPossibleTargets( + protected findTargetsTraversal( objects: FabricObject[], - pointer: Point - ): FabricObject | undefined { - // Cache all targets where their bounding box contains point. - let i = objects.length; - // Do not check for currently grouped objects, since we check the parent group itself. - // until we call this function specifically to search inside the activeGroup - while (i--) { - const target = objects[i]; - if (this._checkTarget(target, pointer)) { + pointer: Point, + options: { searchStrategy: 'first-hit' | 'search-all' } + ): FabricObject[] { + const targets: FabricObject[] = []; + for (let index = objects.length - 1; index >= 0; index--) { + const target = objects[index]; + const pointerToUse = target.group + ? this._normalizePointer(target.group, pointer) + : pointer; + if (this._checkTarget(pointerToUse, target, pointer)) { if (isCollection(target) && target.subTargetCheck) { - const subTarget = this._searchPossibleTargets( - target._objects as FabricObject[], - pointer + targets.push( + ...this.findTargetsTraversal( + target._objects as FabricObject[], + pointer, + options + ) ); - subTarget && this.targets.push(subTarget); } - return target; + targets.push(target); + if (options.searchStrategy === 'first-hit') { + break; + } } } + return targets; } /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted - * @see {@link _searchPossibleTargets} + * Search objects for an object containing {@link pointer} + * depending on the tree's configuration (`subTargetCheck`, `interactive`, `selectable`) + * + * @see {@link findTarget} and {@link findTargetsTraversal} + * * @param {FabricObject[]} [objects] objects array to look into - * @param {Point} [pointer] coordinates from viewport to check. - * @return {FabricObject} **top most object on screen** that contains pointer + * @param {Point} pointer viewport point + * @return {FabricObject} **top most selectable object on screen** that contains {@link pointer} */ - searchPossibleTargets( + findTargets( objects: FabricObject[], - pointer: Point - ): FabricObject | undefined { - const target = this._searchPossibleTargets(objects, pointer); - - // if we found something in this.targets, and the group is interactive, return the innermost subTarget - // that is still interactive - // TODO: reverify why interactive. the target should be returned always, but selected only - // if interactive. - if ( - target && - isCollection(target) && - target.interactive && - this.targets[0] - ) { - /** targets[0] is the innermost nested target, but it could be inside non interactive groups and so not a selection target */ - const targets = this.targets; - for (let i = targets.length - 1; i > 0; i--) { - const t = targets[i]; - if (!(isCollection(t) && t.interactive)) { - // one of the subtargets was not interactive. that is the last subtarget we can return. - // we can't dig more deep; - return t; - } - } - return targets[0]; - } + pointer: Point, + { + searchStrategy = 'first-hit', + }: { searchStrategy?: 'first-hit' | 'search-all' } = {} + ) { + const targets = this.findTargetsTraversal(objects, pointer, { + searchStrategy, + }); - return target; + const target = targets.find((target) => { + return ( + !target.group || target.group.interactive || objects.includes(target) + ); + }); + + return { + target, + targets, + }; + } + + /** + * @deprecated use {@link findTargets} instead + */ + searchPossibleTargets(objects: FabricObject[], pointer: Point) { + return this.findTargets(objects, pointer).target; } /** diff --git a/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap b/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap index 696591c9ed2..2ad384401ec 100644 --- a/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap +++ b/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap @@ -114,6 +114,29 @@ exports[`Canvas event data HTML event "dragend" should fire a corresponding canv "y": -13, }, "currentSubTargets": [], + "button": 1, + "e": MouseEvent { + "isTrusted": false, + }, + "isClick": false, + "pointer": Point { + "x": 50, + "y": 50, + }, + "subTargets": [], + "target": "Drag Target", + "transform": null, + }, + ], + [ + "mouse:up", + { + "absolutePointer": Point { + "x": -30, + "y": -13, + }, + "button": 1, + "currentSubTargets": [], "currentTarget": "Drag Target", "e": MouseEvent { "isTrusted": false, @@ -127,7 +150,9 @@ exports[`Canvas event data HTML event "dragend" should fire a corresponding canv "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "transform": null, "viewportPoint": Point { @@ -228,7 +253,9 @@ exports[`Canvas event data HTML event "dragenter" should fire a corresponding ca "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "viewportPoint": Point { "x": 50, @@ -310,7 +337,9 @@ exports[`Canvas event data HTML event "dragover" should fire a corresponding can "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "viewportPoint": Point { "x": 50, @@ -353,7 +382,9 @@ exports[`Canvas event data HTML event "drop" should fire a corresponding canvas "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "viewportPoint": Point { "x": 50, @@ -382,7 +413,9 @@ exports[`Canvas event data HTML event "drop" should fire a corresponding canvas "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "viewportPoint": Point { "x": 50, @@ -411,7 +444,9 @@ exports[`Canvas event data HTML event "drop" should fire a corresponding canvas "x": -30, "y": -13, }, - "subTargets": [], + "subTargets": [ + "Drag Target", + ], "target": "Drag Target", "viewportPoint": Point { "x": 50, diff --git a/src/canvas/__tests__/eventData.test.ts b/src/canvas/__tests__/eventData.test.ts index 3d32723cdc6..ea2ad93fe38 100644 --- a/src/canvas/__tests__/eventData.test.ts +++ b/src/canvas/__tests__/eventData.test.ts @@ -892,3 +892,773 @@ describe('Event targets', () => { expect(canvasSpy.mock.calls).toMatchSnapshot(); }); }); + +describe('Event targets', () => { + it('A selected subtarget should not fire an event twice', () => { + const target = new FabricObject(); + const group = new Group([target], { + subTargetCheck: true, + interactive: true, + }); + const canvas = new Canvas(null); + canvas.add(group); + const targetSpy = jest.fn(); + target.on('mousedown', targetSpy); + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + canvas.getSelectionElement().dispatchEvent( + new MouseEvent('mousedown', { + clientX: 50, + clientY: 50, + }) + ); + expect(targetSpy).toHaveBeenCalledTimes(1); + }); + + test('mouseover and mouseout with subTargetCheck', () => { + const rect1 = new FabricObject({ + width: 5, + height: 5, + left: 5, + top: 0, + strokeWidth: 0, + }); + const rect2 = new FabricObject({ + width: 5, + height: 5, + left: 5, + top: 5, + strokeWidth: 0, + }); + const rect3 = new FabricObject({ + width: 5, + height: 5, + left: 0, + top: 5, + strokeWidth: 0, + }); + const rect4 = new FabricObject({ + width: 5, + height: 5, + left: 0, + top: 0, + strokeWidth: 0, + }); + const rect5 = new FabricObject({ + width: 5, + height: 5, + left: 2.5, + top: 2.5, + strokeWidth: 0, + }); + const group1 = new Group([rect1, rect2], { + subTargetCheck: true, + }); + const group2 = new Group([rect3, rect4], { + subTargetCheck: true, + }); + // a group with 2 groups, with 2 rects each, one group left one group right + // each with 2 rects vertically aligned + const group = new Group([group1, group2], { + subTargetCheck: true, + }); + + const enter = jest.fn(); + const exit = jest.fn(); + + const getTargetsFromEventStream = (mock: jest.Mock) => + mock.mock.calls.map((args) => args[0].target); + + registerTestObjects({ + rect1, + rect2, + rect3, + rect4, + rect5, + group1, + group2, + group, + }); + + Object.values({ + rect1, + rect2, + rect3, + rect4, + rect5, + group1, + group2, + group, + }).forEach((object) => { + object.on('mouseover', enter); + object.on('mouseout', exit); + }); + + const canvas = new Canvas(); + canvas.add(group, rect5); + + const fire = (x: number, y: number) => { + enter.mockClear(); + exit.mockClear(); + canvas + .getSelectionElement() + .dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y })); + }; + + fire(1, 1); + expect(getTargetsFromEventStream(enter)).toEqual([group, rect4, group2]); + expect(getTargetsFromEventStream(exit)).toEqual([]); + + fire(5, 5); + expect(getTargetsFromEventStream(enter)).toEqual([rect5]); + expect(getTargetsFromEventStream(exit)).toEqual([group, rect4, group2]); + + fire(9, 9); + expect(getTargetsFromEventStream(enter)).toEqual([group, rect2, group1]); + expect(getTargetsFromEventStream(exit)).toEqual([rect5]); + + fire(9, 1); + expect(getTargetsFromEventStream(enter)).toEqual([rect1]); + expect(getTargetsFromEventStream(exit)).toEqual([rect2]); + }); + + describe('findTarget', () => { + const mockEvent = ({ + canvas, + ...init + }: MouseEventInit & { canvas: Canvas }) => { + const e = new MouseEvent('mousedown', { + ...init, + }); + jest + .spyOn(e, 'target', 'get') + .mockReturnValue(canvas.getSelectionElement()); + return e; + }; + + const findTarget = (canvas: Canvas, ev?: MouseEventInit) => { + const target = canvas.findTarget( + mockEvent({ canvas, clientX: 0, clientY: 0, ...ev }) + ); + const targets = canvas.targets; + canvas.targets = []; + return { target, targets }; + }; + + test.each([true, false])( + 'findTargetsTraversal: search all is %s', + (searchAll) => { + const subTarget1 = new FabricObject(); + const target1 = new Group([subTarget1], { + subTargetCheck: true, + interactive: true, + }); + const subTarget2 = new FabricObject(); + const target2 = new Group([subTarget2], { + subTargetCheck: true, + }); + const parent = new Group([target1, target2], { + subTargetCheck: true, + interactive: true, + }); + registerTestObjects({ + subTarget1, + target1, + subTarget2, + target2, + parent, + }); + + const canvas = new Canvas(null); + canvas.add(parent); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + const found = canvas['findTargetsTraversal']([parent], new Point(), { + searchStrategy: searchAll ? 'search-all' : 'first-hit', + }); + expect(found).toEqual( + searchAll + ? [subTarget2, target2, subTarget1, target1, parent] + : [subTarget2, target2, parent] + ); + } + ); + + test('findTargets', () => { + const subTarget = new FabricObject(); + const target = new Group([subTarget], { + subTargetCheck: true, + }); + const parent = new Group([target], { + subTargetCheck: true, + interactive: true, + }); + registerTestObjects({ subTarget, target, parent }); + + const canvas = new Canvas(null); + canvas.add(parent); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + expect(canvas.findTargets([parent], new Point())).toEqual({ + target, + targets: [subTarget, target, parent], + }); + }); + + test.each([true, false])( + 'findTargets with selection and subTargetCheck %s', + (subTargetCheck) => { + const subTarget = new FabricObject(); + const target = new Group([subTarget], { + subTargetCheck: true, + }); + const other = new FabricObject(); + const activeSelection = new ActiveSelection([], { + subTargetCheck, + }); + registerTestObjects({ subTarget, target, other, activeSelection }); + + const canvas = new Canvas(null, { activeSelection }); + canvas.add(other, target); + activeSelection.add(target, other); + canvas.setActiveObject(activeSelection); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + + const foundTargets = canvas['findTargetsTraversal']( + [activeSelection], + new Point(), + { searchStrategy: 'search-all' } + ); + expect(foundTargets).toEqual( + subTargetCheck + ? [other, subTarget, target, activeSelection] + : [activeSelection] + ); + + expect(canvas.findTargets([activeSelection], new Point())).toEqual({ + target: activeSelection, + targets: subTargetCheck + ? [other, activeSelection] + : [activeSelection], + }); + + expect( + canvas.findTargets(canvas.getActiveObjects(), new Point()) + ).toEqual({ + target: other, + targets: [other], + }); + + expect( + canvas.findTargets(canvas.getActiveObjects(), new Point(), { + searchStrategy: 'search-all', + }) + ).toEqual({ + target: other, + targets: [other, subTarget, target], + }); + } + ); + + test('findTarget clears prev targets', () => { + const canvas = new Canvas(); + canvas.targets = [new FabricObject()]; + expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ + target: undefined, + targets: [], + }); + }); + + test('findTarget preserveObjectStacking false', () => { + const rect = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + controls: {}, + }); + const rectOver = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + controls: {}, + }); + registerTestObjects({ rect, rectOver }); + + const canvas = new Canvas(null, { preserveObjectStacking: false }); + canvas.add(rect, rectOver); + canvas.setActiveObject(rect); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: rect, + targets: [], + }); + }); + + test('findTarget preserveObjectStacking true', () => { + const rect = new FabricObject({ left: 0, top: 0, width: 30, height: 30 }); + const rectOver = new FabricObject({ + left: 0, + top: 0, + width: 30, + height: 30, + }); + registerTestObjects({ rect, rectOver }); + + const canvas = new Canvas(null, { preserveObjectStacking: true }); + canvas.add(rect, rectOver); + + const e = { + clientX: 15, + clientY: 15, + shiftKey: true, + }; + const e2 = { clientX: 4, clientY: 4 }; + + expect(findTarget(canvas, e)).toEqual( + { target: rectOver, targets: [] } + // 'Should return the rectOver, rect is not considered' + ); + + canvas.setActiveObject(rect); + expect(findTarget(canvas, e)).toEqual( + { target: rectOver, targets: [] } + // 'Should still return rectOver because is above active object' + ); + + expect(findTarget(canvas, e2)).toEqual( + { target: rect, targets: [] } + // 'Should rect because a corner of the activeObject has been hit' + ); + + canvas.altSelectionKey = 'shiftKey'; + expect(findTarget(canvas, e)).toEqual( + { target: rect, targets: [] } + // 'Should rect because active and altSelectionKey is pressed' + ); + }); + + test('findTarget with subTargetCheck', () => { + const canvas = new Canvas(); + const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10 }); + const rect2 = new FabricObject({ + left: 30, + top: 30, + width: 10, + height: 10, + }); + const group = new Group([rect, rect2]); + registerTestObjects({ rect, rect2, group }); + canvas.add(group); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: group, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ + target: group, + targets: [], + }); + + group.subTargetCheck = true; + group.setCoords(); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: group, + targets: [rect], + }); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: group, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ + target: group, + targets: [rect2], + }); + }); + + test('findTarget with subTargetCheck and canvas zoom', () => { + const nested1 = new FabricObject({ + width: 100, + height: 100, + fill: 'yellow', + }); + const nested2 = new FabricObject({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'purple', + }); + const nestedGroup = new Group([nested1, nested2], { + scaleX: 0.5, + scaleY: 0.5, + top: 100, + left: 0, + subTargetCheck: true, + }); + const rect1 = new FabricObject({ + width: 100, + height: 100, + fill: 'red', + }); + const rect2 = new FabricObject({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'blue', + }); + const group = new Group([rect1, rect2, nestedGroup], { + top: -150, + left: -50, + subTargetCheck: true, + }); + registerTestObjects({ + rect1, + rect2, + nested1, + nested2, + nestedGroup, + group, + }); + + const canvas = new Canvas(null, { + viewportTransform: [0.1, 0, 0, 0.1, 100, 200], + }); + canvas.add(group); + + expect(findTarget(canvas, { clientX: 96, clientY: 186 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 98, clientY: 188 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 100, clientY: 190 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 102, clientY: 192 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 104, clientY: 194 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 106, clientY: 196 })).toEqual({ + target: group, + targets: [rect2], + }); + }); + + test.each([true, false])( + 'findTarget on activeObject with subTargetCheck and preserveObjectStacking %s', + (preserveObjectStacking) => { + const rect = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new FabricObject({ + left: 30, + top: 30, + width: 10, + height: 10, + }); + const group = new Group([rect, rect2], { subTargetCheck: true }); + registerTestObjects({ rect, rect2, group }); + + const canvas = new Canvas(null, { preserveObjectStacking }); + canvas.add(group); + canvas.setActiveObject(group); + + expect(findTarget(canvas, { clientX: 9, clientY: 9 })).toEqual({ + target: group, + targets: [rect], + }); + } + ); + + test('findTarget with perPixelTargetFind', () => { + const triangle = new Triangle({ width: 30, height: 30 }); + registerTestObjects({ triangle }); + + const canvas = new Canvas(); + canvas.add(triangle); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: triangle, + targets: [], + }); + + canvas.perPixelTargetFind = true; + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: undefined, + targets: [], + }); + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: triangle, + targets: [], + }); + }); + + describe('findTarget with perPixelTargetFind in nested group', () => { + const prepareTest = () => { + const deepTriangle = new Triangle({ + left: 0, + top: 0, + width: 30, + height: 30, + fill: 'yellow', + }); + const triangle2 = new Triangle({ + left: 100, + top: 120, + width: 30, + height: 30, + angle: 100, + fill: 'pink', + }); + const deepCircle = new Circle({ + radius: 30, + top: 0, + left: 30, + fill: 'blue', + }); + const circle2 = new Circle({ + scaleX: 2, + scaleY: 2, + radius: 10, + top: 120, + left: -20, + fill: 'purple', + }); + const deepRect = new Rect({ + width: 50, + height: 30, + top: 10, + left: 110, + fill: 'red', + skewX: 40, + skewY: 20, + }); + const rect2 = new Rect({ + width: 100, + height: 80, + top: 50, + left: 60, + fill: 'green', + }); + const deepGroup = new Group([deepTriangle, deepCircle, deepRect], { + subTargetCheck: true, + }); + const group2 = new Group([deepGroup, circle2, rect2, triangle2], { + subTargetCheck: true, + }); + const group3 = new Group([group2], { subTargetCheck: true }); + + registerTestObjects({ + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + }); + + const canvas = new Canvas(null, { perPixelTargetFind: true }); + canvas.add(group3); + + return { + canvas, + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + }; + }; + + test.each([ + { x: 5, y: 5 }, + { x: 21, y: 9 }, + { x: 37, y: 7 }, + { x: 89, y: 47 }, + { x: 16, y: 122 }, + { x: 127, y: 37 }, + { x: 87, y: 139 }, + ])('transparent hit on %s', ({ x: clientX, y: clientY }) => { + const { canvas } = prepareTest(); + expect(findTarget(canvas, { clientX, clientY })).toEqual({ + target: undefined, + targets: [], + }); + }); + + test('findTarget with perPixelTargetFind in nested group', () => { + const { + canvas, + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + } = prepareTest(); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: group3, + targets: [deepTriangle, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 50, clientY: 20 })).toEqual({ + target: group3, + targets: [deepCircle, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 117, clientY: 16 })).toEqual({ + target: group3, + targets: [deepRect, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 100, clientY: 90 })).toEqual({ + target: group3, + targets: [rect2, group2], + }); + + expect(findTarget(canvas, { clientX: 9, clientY: 145 })).toEqual({ + target: group3, + targets: [circle2, group2], + }); + + expect(findTarget(canvas, { clientX: 66, clientY: 143 })).toEqual({ + target: group3, + targets: [triangle2, group2], + }); + }); + }); + + test('findTarget on active selection', () => { + const rect1 = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new FabricObject({ + left: 20, + top: 20, + width: 10, + height: 10, + }); + const rect3 = new FabricObject({ + left: 20, + top: 0, + width: 10, + height: 10, + }); + const activeSelection = new ActiveSelection([rect1, rect2], { + subTargetCheck: true, + cornerSize: 2, + }); + registerTestObjects({ rect1, rect2, rect3, activeSelection }); + + const canvas = new Canvas(null, { activeSelection }); + canvas.add(rect1, rect2, rect3); + canvas.setActiveObject(activeSelection); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: activeSelection, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 40, clientY: 15 })).toEqual({ + target: undefined, + targets: [], + }); + expect(activeSelection.__corner).toBeUndefined(); + + expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ + target: activeSelection, + targets: [], + }); + expect(activeSelection.__corner).toBe('tl'); + + expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( + { + target: activeSelection, + targets: [], + } + // 'Should not return the rect behind active selection' + ); + + canvas.discardActiveObject(); + expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( + { + target: rect3, + targets: [], + } + // 'Should return the rect after clearing selection' + ); + }); + + test('findTarget on active selection with perPixelTargetFind', () => { + const rect1 = new Rect({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new Rect({ + left: 20, + top: 20, + width: 10, + height: 10, + }); + const activeSelection = new ActiveSelection([rect1, rect2]); + registerTestObjects({ rect1, rect2, activeSelection }); + + const canvas = new Canvas(null, { + activeSelection, + perPixelTargetFind: true, + preserveObjectStacking: true, + }); + canvas.add(rect1, rect2); + canvas.setActiveObject(activeSelection); + + expect(findTarget(canvas, { clientX: 8, clientY: 8 })).toEqual({ + target: activeSelection, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: undefined, + targets: [], + }); + }); + }); +});