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(Group): layout manager fromObject #9614

Closed
wants to merge 4 commits into from

Conversation

ShaMan123
Copy link
Contributor

@ShaMan123 ShaMan123 commented Jan 21, 2024

Description

Motivation #9605

A different approach to the NoopLayoutManager
This approach uses the standard API to skip layout: shouldPerformLayout and makes it an edge case handled within the API and not externally, making it even easier to use because now shouldPerformLayout handles all cases

During this PR I found a bug:
Since the NoopLayoutManager skipped performLayout children were not subscribed on initialization. This would not have happened if we noop calcLayoutResult which is what this PR does using the API

I also removed the setCoords call because it was introduced in #9522 (comment) wrongly

In Action

Copy link

codesandbox bot commented Jan 21, 2024

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

Copy link
Contributor

github-actions bot commented Jan 21, 2024

Build Stats

file / KB (diff) bundled minified
fabric 907.965 (+0.449) 304.649 (+0.080)

Copy link
Contributor Author

@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 better IMO

Comment on lines 743 to 766
it('should subscribe objects when created `fromObject`', async () => {
const objectData = {
width: 2,
height: 3,
left: 6,
top: 4,
strokeWidth: 0,
objects: [
new Rect({
width: 100,
height: 100,
top: 0,
left: 0,
strokeWidth: 0,
}).toObject(),
],
};

const group = await Group.fromObject(objectData);
const rect = group.item(0);
expect(
Array.from(group.layoutManager['_subscriptions'].keys())
).toMatchObject([rect]);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will fail on master

Copy link
Contributor

Coverage after merging fix/layout-manager-from-object into master will be

82.75%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
index.node.ts14.29%100%0%25%23, 26, 29, 41, 44, 47
src
   ClassRegistry.ts100%100%100%100%
   Collection.ts94.85%94.83%87.10%97.14%109, 112, 215–216, 241–242
   CommonMethods.ts96.55%87.50%100%100%9
   Intersection.ts100%100%100%100%
   Observable.ts87.76%85.29%87.50%89.58%134–135, 160–161, 32–33, 41, 50, 78, 89
   Point.ts100%100%100%100%
   Shadow.ts98.36%95.65%100%100%178
   cache.ts97.06%90%100%100%57
   config.ts74.14%65%66.67%82.76%131, 139, 139, 139, 139, 139–141, 152–154, 30
   constants.ts100%100%100%100%
src/LayoutManager
   LayoutManager.ts87.50%89.29%76.92%90%133–134, 136–137, 137, 137, 137, 137, 231, 289–290, 301, 46
   constants.ts100%100%100%100%
src/LayoutManager/LayoutStrategies
   ClipPathLayout.ts82.05%72.22%100%88.89%30–31, 42, 54, 57–58, 65
   FitContentLayout.ts100%100%100%100%
   FixedLayout.ts20%0%0%40%20–21, 23, 23, 23, 23, 23
   LayoutStrategy.ts89.47%72.22%100%96.67%62, 62, 62, 70, 90–91
   utils.ts91.67%85.71%100%100%28, 34
src/Pattern
   Pattern.ts90.54%93.94%80%90.32%119, 130, 139, 32, 36
src/brushes
   BaseBrush.ts100%100%100%100%
   CircleBrush.ts0%0%0%0%108, 108, 108, 110, 112, 114–116, 118–121, 128–129, 13, 136, 138, 23–24, 32–36, 40–44, 51–54, 62–66, 68, 76, 76, 76, 76, 76–77, 79, 79, 79–82, 84, 92–93, 95, 97–99
   PatternBrush.ts97.06%87.50%100%100%21
   PencilBrush.ts91.06%82.35%100%93.81%122–123, 152, 152–154, 176, 176, 276, 280, 285–286, 68–69, 84–85
   SprayBrush.ts0%0%0%0%107, 107, 107, 107, 107–108, 110–111, 118–119, 121, 123–127, 136, 140–141, 141, 148, 148, 148–151, 153–156, 160–161, 163, 165–168, 17, 171, 178–179, 18, 181, 183–184, 186, 193–194, 196–197, 20, 200, 200, 207, 207, 21, 211, 22, 22, 22–24, 28, 32, 39, 46, 53, 60, 67, 84–86, 94–96, 98–99
src/canvas
   Canvas.ts78.66%77.04%82.14%79.33%1001–1003, 1006–1007, 1011–1012, 1123–1125, 1128–1129, 1202, 1314, 1435, 158, 183, 289, 289–294, 299, 322–323, 328–333, 353, 353, 353–354, 354, 354–355, 363, 368–369, 369, 369–370, 372, 381, 387–388, 388, 388, 42, 431, 439, 443, 443, 443–444, 446, 46, 528–529, 529, 529–530, 536, 536, 536–538, 558, 560, 560, 560–561, 561, 561, 564, 564, 564–565, 568, 577–578, 580–581, 583, 583–584, 586–587, 599–600, 600, 600–601, 603–608, 614, 621, 658, 658, 658, 660, 662–667, 673, 679, 679, 679–680, 682, 685, 690, 703, 731, 783–784, 784, 784, 784, 784, 784, 787–788, 791, 791–793, 796–797, 879, 886, 886, 886, 899, 928–929, 929, 929–931, 934–935, 935, 935, 937, 945, 945, 945–947, 947, 947, 954–955, 963–964, 964, 964–965, 971, 973
   CanvasOptions.ts100%100%100%100%
   SelectableCanvas.ts90.22%87.30%94.55%91.74%1004, 1012, 1123, 1125, 1127–1128, 1199, 1220–1221, 1239–1240, 460–461, 463–464, 464, 464, 513–514, 591, 596, 666, 671–672, 699–700, 724, 727–730, 730, 730–731, 731, 731, 737–738, 740, 760–761, 766–770, 772, 807–808, 811, 811, 811–812, 931, 931–932, 935, 955, 955
   StaticCanvas.ts96.51%93.04%98.91%98.28%1014, 1024, 1075–1077, 1080, 1115–1116, 1192, 1201, 1201, 1205, 1205, 1252–1253, 179–180, 196, 554, 566–567, 897–898, 898, 898–899
   StaticCanvasOptions.ts100%100%100%100%
   TextEditingManager.ts86%71.43%91.67%91.67%17, 17, 17–18, 18, 18
src/canvas/DOMManagers
   CanvasDOMManager.ts95.52%70%100%100%21–22, 29
   StaticCanvasDOMManager.ts97.50%88.89%100%100%33
   util.ts86.67%80.56%83.33%93.94%14, 26, 63–64, 67, 67, 74, 93–94
src/color
   Color.ts94.96%91.67%96.30%96.05%233, 258–259, 267–268, 48
   color_map.ts100%100%100%100%
   constants.ts100%100%100%100%
   util.ts85.71%76.92%100%89.74%55–56, 56, 58, 58, 58–59, 61–62, 89
src/controls
   Control.ts94.44%93.10%91.67%96.77%183, 249, 354
   changeWidth.ts100%100%100%100%
   commonControls.ts100%100%100%100%
   controlRendering.ts81.63%78%100%84.78%106, 111, 121, 121, 45, 50, 61, 61, 65–72, 81–82
   drag.ts100%100%100%100%
   fireEvent.ts88.89%75%100%100%13
   polyControl.ts10.87%0%0%16.13%114, 114, 114, 114, 114, 116–119, 119, 122, 129, 24–26, 50–52, 58–59, 61, 71, 77–79, 79, 81, 84, 86, 90–92, 94, 99
   rotate.ts19.57%12.50%50%21.43%41, 45, 51, 51, 51–52, 55–57, 59, 59, 59,

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Jan 21, 2024

@zhe-he take a look
This should provide you for what you asked for
You will need to override LayoutStrategy#shouldPerformLayout and return true for the specific case of context.type === 'initialization' && this._fromObject

@asturur
Copy link
Member

asturur commented Jan 21, 2024

Please remove the bug in its own PR, so that can go in in any moment.

I don't want to discuss the api change before we know why this would be necessary at all.

@ShaMan123
Copy link
Contributor Author

will do

@ShaMan123
Copy link
Contributor Author

Wait, not good.
Since the subscriptions will be present of NoopLayoutManager it will cause a bug when unsubscribing.

@ShaMan123
Copy link
Contributor Author

shows that it is better to stick to the API

@zhe-he
Copy link
Contributor

zhe-he commented Jan 22, 2024

Hmm... I'll explain why I customized this.

Similar to Figma's Frame, Frame can have auto-layout properties or not. When it doesn't have auto-layout properties, you set the width and height yourself. When it does have auto-layout properties and its width is adaptive, then during initialization, I need to calculate the widths of all its subsets to determine its width.

Now I'm solving this issue by setting it again after initialization.

I think this approach (LayoutStrategy#shouldPerformLayout) should also work. I'll look into it first. Thank you for your suggestion.

@asturur
Copy link
Member

asturur commented Jan 22, 2024

@zhe-he but what is wrong with the suggestion of just having your fromObject method in your class?
We don't have to solve problems with new apis that can be solved already with the current api.

This proposal of fromObject is a step back in the world of 'initialized: true/false' that we always said we don't like.

@asturur
Copy link
Member

asturur commented Jan 22, 2024

Wait, not good. Since the subscriptions will be present of NoopLayoutManager it will cause a bug when unsubscribing.

can you explain this comment here?

@asturur
Copy link
Member

asturur commented Jan 22, 2024

the fromObject with the flag is a big no go for me.
Since there is that bug of the subscriptions that needs to be fixed, i will revert the pr of the noop first of all,
then we can reason of how to go back there without the bug.

I rather prefer an extra type than a flag.

@asturur
Copy link
Member

asturur commented Jan 22, 2024

or make a direct call to the object registration after assigning the correct layout manager

@zhe-he
Copy link
Contributor

zhe-he commented Jan 23, 2024

The Group as of now is trusting its serialized representation when restoring.
That works well and a lot of tests and code rely on that. The LayoutManager itself won't work if we don't follow this right now.
Recalculating the Layout is expensive, if a group is correctly serialized it doesn't need to do that, and this is the premise on which the actual code has been built.

I may not modify the properties of child objects on the canvas, but instead directly modify the properties of child objects in the exported serialized data. In this case, I have two options in front of me: one is to modify the parent object's properties in the exported serialized data through computation, and the other is to recompute the parent object's properties at initialization. If I choose the second approach, I cannot trust its serialized representation. So I would like to execute a custom performantLayout instead of an empty performantLayout during initialization.


I see that the fromObject method has been added here, and by modifying the value of _fromObject, the performLayout is no longer executed when loading serialized data. If I don't trust the serialized data, I can simply modify the return value of shouldPerformLayout in the custom object to execute performLayout during initialization. I think this method is great.


the fromObject with the flag is a big no go for me.

Can we add an additional property 'dirty' to the LayoutManager, with a default value of false or undefined? If the user manually changes it to true, performLayout will be executed during initialization; otherwise, it won't be executed.
Alternatively, we can split performLayout into two functions: the original performLayout and a new performLayout2, which is only executed after loading serialized objects. performLayout2 is initially an empty function for users to customize, and its invocation can be placed inside Group.fromObject.

@ShaMan123
Copy link
Contributor Author

I like both approaches
The first approach is very simple and is a fabric convention, therefore a great approach.
For the second, how would you know to skip the initialization layout if performLayout2 will be called? We will need a flag in the constructor so the first approach is better
The dev could then flag the layout as dirty when constructing the layout classes

@zhe-he
Copy link
Contributor

zhe-he commented Jan 23, 2024

The second solution I want to propose is probably like this.

class Group {
// noop
fromObjectPerformLayout() {},
fromObject() {
      // All layouts skip performLayout during initialization.
      const group = new this(objects, ...);
      // Custom layouts can override the performLayout2 method to recalculate.
      group.fromObjectPerformLayout();
}
}
class Frame extends Group {
    fromObjectPerformLayout() {
          this.layoutManager.performLayout({ type: "xxx" });
    }
}

@asturur
Copy link
Member

asturur commented Jan 23, 2024

The old pr can't be easily reverted.
There are test changes and a lot of stuff that need to be re-done again.

@asturur
Copy link
Member

asturur commented Jan 23, 2024

@zhe-he is still unclear why you can't write your own fromObject method in the Frame class, that just adds the layout manager, exactly as you did with your initial pr.

@zhe-he
Copy link
Contributor

zhe-he commented Jan 23, 2024

@zhe-he is still unclear why you can't write your own fromObject method in the Frame class, that just adds the layout manager, exactly as you did with your initial pr.

Hmm... I can modify the fromObject method in Frame to solve my issue.
It may not be necessary to modify the fromObject method in Group.
Perhaps the pull request I submitted could be a mistake?
I didn't consider that Fabric relies on the imported serializable objects when I submitted the pull request.

@zhe-he
Copy link
Contributor

zhe-he commented Jan 23, 2024

If not modified, all custom layout objects will not be initialized during data import. If they need to be, they must override the fromObject method.

@asturur
Copy link
Member

asturur commented Jan 23, 2024

@zhe-he i still think i have very little informations to give you suggestions.
How many custom classes you made?
I understand one, the Frame.
The Frame wants to run the performLayout at the point of restore, contrary to what group does.
If this is the only issue it can be solved with Frame.fromObject being custom. And this we said.
If there is more maybe this is not enough.

@zhe-he
Copy link
Contributor

zhe-he commented Jan 24, 2024

@asturur
Currently, there are three custom classes. One is "Frame," which inherits from "Group." The second one is "Component," which inherits from "Frame." The third one is "SubComponent," which inherits from "Component." They are in a chain relationship. Therefore, I only need to modify the "frameObject" of "Frame" to solve this problem.

@asturur
Copy link
Member

asturur commented Feb 8, 2024

The registration bug needs to be fixed, and i don't want to do internal flags for fromObject.
I have a different proposal to disconnect the registration of objects from the performLayout, let me see if i can make a different example

@asturur
Copy link
Member

asturur commented Feb 8, 2024

well what i wanted to do can't be really done as good as i wish because i don't like the NoopClassManager hack i have to add. {} as Group
But this is a tentative of separating registration from layouting.
https://github.com/fabricjs/fabric.js/compare/registering-objects-group-fromObject?expand=1

It would be also ok to make subscribe public, and just call it on every object in the Group FromObject.

I didn't even test it for now, i wanted to put on the table first.

I have some notes on the context, one is that i can't see where we use 'stopPropagaton' at all, and i wonder why is there.

@asturur
Copy link
Member

asturur commented Feb 10, 2024

I think after much thinking my preferred solution is this one:
#9661

Just expose the subscription and call it when necessary.

Please let me know, i see you are maybe all busy, but if in 1 or 2 weeks i don't get a feedback i ll just move on.

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Feb 10, 2024

What about adding a prop/flag/indication to the options passed by fromObject?

new FabricObject({ ...options, deserialized:true })

I think this is so much easier and straight forward and provides useful context.

We can protect it by wrapping in with a private class and checking instanceof if you want to be protective but I am not sure that is needed

class DeserializedOptions {
   constructor(options) { 
      Object.assign(this, options)
  }
}



class FabricObject {
   static fromObject(options) {
      return new FabricObject(new DeserializedOptions(options));
  }
  constructor(options) {
    const isDeserialized = options instanceof DeserializedOptions
  }

}

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Feb 10, 2024

I dislike both of the suggestions you provided, I commented in the PR

@asturur
Copy link
Member

asturur commented Feb 12, 2024

@ShaMan123 'dislike' is a sensation that does not really matter.
Just explain why in your opinion the solution isn't good.

You pointed out the restriction in the api context and that has been restored, what else isn't working in your opinion?

We both said in the past that internal flags to say 'initialized: true/false' or specific flags to initialize the object differently were something that long term didn't work out.
Is not that i don't like them, we excluded them in the past.
Indeed we removed the flag from the group that was taking care of doing a dividing the codepath for objects that were restored from json, i don't see why we would like to put it back now. What changed?

That is why i don't wan't to add back a flag like fromObject() or '{ fromObject: true }'.

@asturur asturur closed this Feb 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants