Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Canvas): setActiveObject should update canvas#_activeSelection #9336

Merged
merged 15 commits into from
Oct 2, 2023

Conversation

zhe-he
Copy link
Contributor

@zhe-he zhe-he commented Sep 14, 2023

before:

const rect1 = new Rect({ top: 1, left: 1, width: 1, height: 1 });
const rect2 = new Rect({ top: 1, left: 1, width: 1, height: 1 });
canvas.add(rect1, rect2);

const arr = canvas.getObjects();
const ac = new ActiveSelection(arr, { canvas });
canvas.setActiveObject(ac);
console.log(canvas.getActiveObjects()); // [ActiveSelection]

after:

const rect1 = new Rect({ top: 1, left: 1, width: 1, height: 1 });
const rect2 = new Rect({ top: 1, left: 1, width: 1, height: 1 });
canvas.add(rect1, rect2);

const arr = canvas.getObjects();
const ac = new ActiveSelection(arr, { canvas });
canvas.setActiveObject(ac);
console.log(canvas.getActiveObjects()); // [Rect, Rect]

@asturur
Copy link
Member

asturur commented Sep 14, 2023

eh right.
So what are we fixing is not getObjects but the fact that using setActiveObject on an active selection right now bring to unexpected results.
ok this is mostly good, but let's start from the title that should be fix _setActiveObject.

Said so the readOnly was put there for a reason, i m not sure if we want to forbid devs to swap the active selection now

Copy link
Contributor

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very good, exacxtly what we were looking for to finilize the change.
@asturur removing readonly is a must because the ref changes now.
Please add a test in src/shapes/ActiveSelection.spec.ts

image

@zhe-he
Copy link
Contributor Author

zhe-he commented Sep 15, 2023

If we want to ensure that activeSelection is only readonly, we need to split the object when setting it, and we also need to consider event triggering, something like this?

setActiveObject(object: FabricObject) {
    ..
    if (object instanceof ActiveSelection) {
          const objs = object.getObjects();
          const lastObjs = this._activeSelection.getObjects();
          const dels = [];
          for (const item of lastObjs) {
              const fIndex = objs.indexOf(item);
              if (fIndex != -1) {
                   objs.splice(fIndex, 1);
                   continue;
              }
              dels.push(item);
          }
          const adds = objs;
          for (const item of dels) {
               this._activeSelection.remove(item);
          }
          this._activeSelection.multiSelectAdd(adds);
    }
     ...
},
_setActiveObject(object: FabricObject) {
   ...
    if (object instanceof ActiveSelection) {
          this._activeSelection.removeAll();
         this._activeSelection.add(object.getObjects());
    }
   ...
}

The incomplete code above will trigger add and delete callback events.
So do we need the _add and _remove methods to support addition and deletion without triggering events?

Copy link
Contributor

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent job
A few small changes to the test and this is good to merge

src/shapes/ActiveSelection.spec.ts Outdated Show resolved Hide resolved
src/shapes/ActiveSelection.spec.ts Show resolved Hide resolved
@ShaMan123 ShaMan123 changed the title fix: getActiveObjects fix(Canvas): setActiveObject should update canvas#_activeSelection Sep 15, 2023
CHANGELOG.md Outdated Show resolved Hide resolved
@ShaMan123
Copy link
Contributor

If we want to ensure that activeSelection is only readonly

There is no need, I didn't consider this use case when I made it readonly

@ShaMan123
Copy link
Contributor

Ahh I think we need to call removeAll dispose and to unset the canvas ref on the old active selection

@zhe-he
Copy link
Contributor Author

zhe-he commented Sep 15, 2023

If the user actively chooses to cancel.

canvas.setActiveObject(A);         // fire: selection:created
canvas._discardActiveObject();
canvas.setActiveObject(B);         // fire: selection:updated
canvas.setActiveObject(A);        // fire: selection:created
canvas.discardActiveObject();     // fire: selection:cleared
canvas.setActiveObject(B);        // fire: selection:created

Do we need to help the user cancel it when setting it up?
If we help the user cancel when setting, do we need to trigger the element's remove event?
So could we not do additional things when the user sets it? Let the user actively choose _discardActiveObject or discardActiveObject

Or we need to provide a new method ?

canvas.setAndClearActiveObject(B)
canvas._setAndClearActiveObject(B)

@ShaMan123
Copy link
Contributor

ShaMan123 commented Sep 15, 2023

good questions.
I am sure that if the new active selection if not equal to the prev active selection we need to clean the prev one.
I would say that if the active object was an active selection and after calling setActiveObject it remains an active selection (even if a different one) the only selection event that should fire is selection:updated but that is only my opinion.
That will also not break current behavior I think.

@ShaMan123
Copy link
Contributor

ShaMan123 commented Sep 15, 2023

Also consider what happens without discarding because fabric does that under the hood

canvas.setActiveObject(A);        // fire: selection:created
canvas.setActiveObject(B);        // fire: selection:updated??

@zhe-he
Copy link
Contributor Author

zhe-he commented Sep 15, 2023

em... Do I need to modify and submit it like this?

setActiveObject(object: FabricObject, e?: TPointerEvent) {
    // we can't inline this, since _setActiveObject will change what getActiveObjects returns
    const currentActives = this.getActiveObjects();
    if (object instanceof ActiveSelection) {
      object.forEachObject(o => {
        const fIndex = currentActives.indexOf(o);
        if (fIndex == -1) return;
        currentActives.splice(fIndex, 1);
      });
    }
    const selected = this._setActiveObject(object, e);
    this._fireSelectionEvents(currentActives, e);
    return selected;
  }

@ShaMan123
Copy link
Contributor

I am sure that if the new active selection if not equal to the prev active selection we need to clean the prev one.

My bad, this is incorrect. If an object was present in the active selection it should be removed by fabric once added to a new one, I think I need to fix a small thing about that.
So we should only unset the canvas ref on the prev active selection.

We should add a test for this specific case to see all is as expected including fired events.

@asturur
Copy link
Member

asturur commented Sep 15, 2023

Yes i didn't mean it needs to be readonly, i meant to bring up the point that there is a readonly and to think why.

asturur
asturur previously approved these changes Sep 15, 2023
Copy link
Member

@asturur asturur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change request i made is optional, it doesn't change anything is just some bytes saved

@ShaMan123
Copy link
Contributor

ShaMan123 commented Sep 15, 2023

There is a bug I will look into because I think I caused it.
But also we still need to relove the events etc.

@ShaMan123
Copy link
Contributor

There is a bug I will look into because I think I caused it. But also we still need to relove the events etc.

Fixed.
The selection events are not related because they use getActiveObjects and there are tests for that

Update SelectableCanvas.ts
ShaMan123
ShaMan123 previously approved these changes Sep 15, 2023
@ShaMan123
Copy link
Contributor

There is a bug I will look into because I think I caused it. But also we still need to relove the events etc.

Fixed. The selection events are not related because they use getActiveObjects and there are tests for that

@asturur I have found another related bug handled in #9349
I can merge it here but do not want to pollute this PR

@zhe-he
Copy link
Contributor Author

zhe-he commented Sep 18, 2023

The change request i made is optional, it doesn't change anything is just some bytes saved

done.

@asturur
Copy link
Member

asturur commented Sep 18, 2023

Since my english is never understood, from now on i will close PRs that try to fix 2 bugs in one pr.
Let's keep this to one bug.

@@ -86,7 +86,9 @@ export class ActiveSelection extends Group {
// save ref to group for later in order to return to it
const parent = object.group;
parent._exitGroup(object);
object.__owningGroup = parent;
// make sure we are setting the correct owning group
// in case `object` is transferred between active selections
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there shouldn't be transferring between active selections.
How that happens?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now you can do:

new ActiveSelection(canvas.getActiveObjects())

And that will hit this code

@@ -1105,6 +1105,13 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
}
this._activeObject = object;

if (object instanceof ActiveSelection && this._activeSelection !== object) {
this._activeSelection.dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a later addition that is problematic.
Disposing an activeSelection means disposing contained objects.
We can't do that. This is just setActiveObject() and we shouldn't dispose inside it because all the resources the developer is using may be still completely valid and needed for his/her app.

The old active selection being dereferenced will be garbage collected at some point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right - this should be a removeAll call

@ShaMan123
Copy link
Contributor

Since my english is never understood, from now on i will close PRs that try to fix 2 bugs in one pr. Let's keep this to one bug.

Your English is well understood most of the time
I have opened a PR

it is bad indeed to dispose
anyways it is safe to assume that the prev active selection has no children because it has been discarded and onDelect removed all the children
ShaMan123
ShaMan123 previously approved these changes Sep 23, 2023
@ShaMan123
Copy link
Contributor

@asturur you want anything else? Or I merge

@ShaMan123
Copy link
Contributor

@zhe-he Thanks, XieXie

@asturur
Copy link
Member

asturur commented Oct 2, 2023

totally forgout about this, i thought it was done

@asturur
Copy link
Member

asturur commented Oct 2, 2023

This sounds fine to me.
i have a comment anyway, things like:

Now you can do:

new ActiveSelection(canvas.getActiveObjects())

And that will hit this code

Start their own rabbit of holes of new possibilities, that i don't want to follow.
If playing around with active selection brings code in non functional cases, we need to put down in writing what a developer can do and cannot do with the active selection and make it the expected behaviour by documenting.

@asturur asturur merged commit e4364da into fabricjs:master Oct 2, 2023
20 of 22 checks passed
@ShaMan123
Copy link
Contributor

I think we should consider throwing in cases we find unfit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants