Skip to content

Commit

Permalink
fix(Canvas): Fix searchPossibleTargets for non-interactive nested tar…
Browse files Browse the repository at this point in the history
…gets (#9762)

Co-authored-by: Andrea Bogazzi <[email protected]>
Co-authored-by: Shachar <[email protected]>
  • Loading branch information
3 people authored Apr 9, 2024
1 parent 9ec5a3d commit 8ab8862
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [next]

- fix(Canvas): Fix searchPossibleTargets for non-interactive nested targets [#9762](https://github.com/fabricjs/fabric.js/pull/9762)
- test(): Rename svg tests [#9775](https://github.com/fabricjs/fabric.js/pull/9775)
- refactor(): `_findTargetCorner` is now called `findControl` and returns the key and the control and the coordinates [#9668](https://github.com/fabricjs/fabric.js/pull/9668)
- feat(LayoutManager): Handle the case of activeSelection with objects inside different groups [#9651](https://github.com/fabricjs/fabric.js/pull/9651)
Expand Down
131 changes: 131 additions & 0 deletions src/canvas/SelectableCanvas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,137 @@ describe('Selectable Canvas', () => {
});
});

describe('searchPossibleTargets', () => {
test('the target returned will stop at the first non interactive container', () => {
const object = new FabricObject({
id: 'a',
left: 0,
top: 0,
width: 10,
height: 10,
padding: 0,
strokeWidth: 0,
});
const groupB = new Group([object], {
id: 'b',
interactive: true,
subTargetCheck: true,
});
const groupC = new Group([groupB], {
id: 'c',
interactive: false,
subTargetCheck: true,
});
const groupD = new Group([groupC], {
id: 'd',
interactive: true,
subTargetCheck: true,
});
const canvas = new Canvas(undefined, { renderOnAddRemove: false });
canvas.add(groupD);
const target = canvas.searchPossibleTargets(
canvas.getObjects(),
groupD.getCenterPoint()
);
expect(target).toBe(groupC);
expect(canvas.targets.map((obj) => obj.id)).toEqual(['a', 'b', 'c']);
});
test('a interactive group covered by a non interactive group wont be selected', () => {
const object = new FabricObject({
id: 'a',
left: 0,
top: 0,
width: 10,
height: 10,
padding: 0,
strokeWidth: 0,
});
const groupB = new Group([object], {
id: 'b',
interactive: true,
subTargetCheck: true,
});
const groupC = new Group([groupB], {
id: 'c',
interactive: false,
subTargetCheck: true,
});
const groupD = new Group([groupC], {
id: 'd',
interactive: true,
subTargetCheck: true,
});
const groupE = new Group([groupD], {
id: 'e',
interactive: true,
subTargetCheck: true,
});
const canvas = new Canvas(undefined, { renderOnAddRemove: false });
canvas.add(groupE);
const target = canvas.searchPossibleTargets(
canvas.getObjects(),
groupD.getCenterPoint()
);
expect(target).toBe(groupC);
expect(canvas.targets.map((obj) => obj.id)).toEqual(['a', 'b', 'c', 'd']);
});

test('nested non interactive groups with subTargetCheck', () => {
const object = new FabricObject({
left: 0,
top: 0,
width: 10,
height: 10,
padding: 0,
strokeWidth: 0,
});

const object2 = new FabricObject({
left: 20,
top: 0,
width: 10,
height: 10,
padding: 0,
strokeWidth: 0,
});

const object3 = new FabricObject({
left: 40,
top: 0,
width: 10,
height: 10,
padding: 0,
strokeWidth: 0,
});

const nestedGroup = new Group([object2, object3], {
interactive: false,
subTargetCheck: true,
});

const canvas = new Canvas(undefined, { renderOnAddRemove: false });
const group = new Group([object, nestedGroup], {
interactive: true,
subTargetCheck: true,
});
canvas.add(group);

const object2Position = object2.getCenterPoint();
const target = canvas.searchPossibleTargets(
canvas.getObjects(),
object2Position
);
expect(target).toBe(nestedGroup);

nestedGroup.set({ interactive: true });
const nestedTarget = canvas.searchPossibleTargets(
canvas.getObjects(),
object2Position
);
expect(nestedTarget).toBe(object2);
});
});

describe('setupCurrentTransform', () => {
test.each(
['tl', 'mt', 'tr', 'mr', 'br', 'mb', 'bl', 'ml', 'mtr']
Expand Down
26 changes: 21 additions & 5 deletions src/canvas/SelectableCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
declare fireMiddleClick: boolean;

/**
* Keep track of the subTargets for Mouse Events
* Keep track of the subTargets for Mouse Events, ordered bottom up from innermost nested subTarget
* @type FabricObject[]
*/
targets: FabricObject[] = [];
Expand Down Expand Up @@ -860,15 +860,31 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
pointer: Point
): FabricObject | undefined {
const target = this._searchPossibleTargets(objects, pointer);
// if we found something in this.targets, and the group is interactive, return that subTarget

// 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.
return target &&
if (
target &&
isCollection(target) &&
target.interactive &&
this.targets[0]
? this.targets[0]
: target;
) {
/** 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];
}

return target;
}

/**
Expand Down

0 comments on commit 8ab8862

Please sign in to comment.