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

perf(): reduce setCoords calls #9550

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
- refactor(): Separate defaults for base fabric object vs interactive object. Also some moving around of variables [#9474](https://github.com/fabricjs/fabric.js/pull/9474)
- refactor(): Change how LayoutManager handles restoring groups [#9522](https://github.com/fabricjs/fabric.js/pull/9522)
- fix(BaseConfiguration): set `devicePixelRatio` from window [#9470](https://github.com/fabricjs/fabric.js/pull/9470)
- perf(): reduce setCoords calls [#9550](https://github.com/fabricjs/fabric.js/pull/9550)
- fix(): bubble dirty flag for group only when true [#9540](https://github.com/fabricjs/fabric.js/pull/9540)
- test() Backport a test to capture a failing text style situation [#9531](https://github.com/fabricjs/fabric.js/pull/9531)

Expand Down
4 changes: 4 additions & 0 deletions src/LayoutManager/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export class LayoutManager {
target.setPositionByOrigin(nextCenter, CENTER, CENTER);
// invalidate
target.setCoords();
(target.subTargetCheck || target.interactive) &&
target.forEachObject((object) => object.setCoords());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

here is a good example of the benefit of separating setCoords to setCornerCoords and setControlCoords because here we need only setCornerCoords

Copy link
Contributor Author

@ShaMan123 ShaMan123 Jan 8, 2024

Choose a reason for hiding this comment

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

Do you have objections doing that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also this line is incorrect because it goes down 1 level but should recurse down

target.set('dirty', true);
}
}
Expand Down Expand Up @@ -286,6 +288,8 @@ export class LayoutManager {
left: object.left + offset.x,
top: object.top + offset.y,
});
// TODO: should we delete aCoords/oCoords here so they are not stale if subTargetCheck is false?
// or should we apply offset instead of recalculating aCoords?
Copy link
Member

Choose a reason for hiding this comment

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

an object without coords could create more damages than one with stale, different typings, always have to check if the coords are there.
We should document that if you have a group without subtargetCheck and you plan on using the group inner object coordinates, you have to update them when you use them.

}

protected onAfterLayout(
Expand Down
5 changes: 2 additions & 3 deletions src/brushes/PencilBrush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,12 @@ export class PencilBrush extends BaseBrush {

const path = this.createPath(pathData);
this.canvas.clearContext(this.canvas.contextTop);
this.canvas.fire('before:path:created', { path: path });
this.canvas.fire('before:path:created', { path });
this.canvas.add(path);
this.canvas.requestRenderAll();
path.setCoords();
this._resetShadow();

// fire event 'path' created
this.canvas.fire('path:created', { path: path });
this.canvas.fire('path:created', { path });
}
}
38 changes: 14 additions & 24 deletions src/canvas/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,19 +1304,16 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
transform: Transform,
pointer: Point
) {
const x = pointer.x,
y = pointer.y,
action = transform.action,
actionHandler = transform.actionHandler;
let actionPerformed = false;
// this object could be created from the function in the control handlers

if (actionHandler) {
actionPerformed = actionHandler(e, transform, x, y);
}
const { action, actionHandler, target } = transform;

const actionPerformed =
!!actionHandler && actionHandler(e, transform, pointer.x, pointer.y);
// This call needs to recurse down
actionPerformed && target.setCoords();

if (action === 'drag' && actionPerformed) {
transform.target.isMoving = true;
this.setCursor(transform.target.moveCursor || this.moveCursor);
target.isMoving = true;
this.setCursor(target.moveCursor || this.moveCursor);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

doesn't belong here

}
transform.actionPerformed = transform.actionPerformed || actionPerformed;
}
Expand All @@ -1332,7 +1329,6 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
this.setCursor(this.defaultCursor);
return;
}
let hoverCursor = target.hoverCursor || this.hoverCursor;
const activeSelection = isActiveSelection(this._activeObject)
? this._activeObject
: null,
Expand All @@ -1345,17 +1341,11 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
target.findControl(this.getViewportPoint(e));

if (!corner) {
if ((target as Group).subTargetCheck) {
// hoverCursor should come from top-most subTarget,
// so we walk the array backwards
Comment on lines -1349 to -1350
Copy link
Contributor Author

Choose a reason for hiding this comment

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

wrong

Copy link
Member

Choose a reason for hiding this comment

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

What is wrong exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the order of targets is [topMost, ..., bottomMost]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorted in descending z index order, top most object first

this.targets
.concat()
.reverse()
.map((_target) => {
hoverCursor = _target.hoverCursor || hoverCursor;
});
}
this.setCursor(hoverCursor);
// hoverCursor should come from top-most subTarget
const hoverCursor =
(target as Group).subTargetCheck &&
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should it be:

Suggested change
(target as Group).subTargetCheck &&
(target as Group).interactive &&

?

this.targets.find((target) => target.hoverCursor)?.hoverCursor;
this.setCursor(hoverCursor || target.hoverCursor || this.hoverCursor);
} else {
const control = corner.control;
this.setCursor(control.cursorStyleHandler(e, control, target));
Expand Down
8 changes: 6 additions & 2 deletions src/canvas/SelectableCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,10 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
while (i--) {
const target = objects[i];
if (this._checkTarget(target, pointer)) {
if (isCollection(target) && target.subTargetCheck) {
if (
isCollection(target) &&
(target.subTargetCheck || target.interactive)
Copy link
Member

Choose a reason for hiding this comment

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

target interactive requires subtargetcheck, so we shouldn't be checking for both.

Copy link
Contributor Author

@ShaMan123 ShaMan123 Jan 8, 2024

Choose a reason for hiding this comment

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

I thought I wrote about this in the description, sorry, updated now.

Another change I have introduced is interactive including subTargetCheck.
To be able to select sub targets both need to be flagged as true.
subTargetCheck flags searchPossibleTargets to traverse the children of a group.
interactive flags to return the top most sub target found within searchPossibleTargets.
So essentially interactive = true contains subTargetCheck = true.
It is more straight forward as well.

) {
const subTarget = this._searchPossibleTargets(
target._objects as FabricObject[],
pointer
Expand Down Expand Up @@ -1148,9 +1151,10 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>

if (isActiveSelection(object) && prevActiveObject !== object) {
object.set('canvas', this);
object.setCoords();
}

object.setCoords();

return true;
}

Expand Down
16 changes: 1 addition & 15 deletions src/canvas/StaticCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,21 +379,7 @@ export class StaticCanvas<
* @param {Array} vpt a Canvas 2D API transform matrix
*/
setViewportTransform(vpt: TMat2D) {
const backgroundObject = this.backgroundImage,
overlayObject = this.overlayImage,
len = this._objects.length;

this.viewportTransform = vpt;
for (let i = 0; i < len; i++) {
const object = this._objects[i];
object.group || object.setCoords();
}
if (backgroundObject) {
backgroundObject.setCoords();
}
if (overlayObject) {
overlayObject.setCoords();
}
Comment on lines -387 to -396
Copy link
Member

Choose a reason for hiding this comment

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

why deleting those? control coords need to be set when we change the vpt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need control coords only for the active object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And regarding corner coords, we need them on these objects only if they do not ignore the vpt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at the code we do not even check if these objects are on screen and these objects are not selectable apart from imperatively calling setActiveObject (which I believe should call setCoords if it doesn't) thus making the setCoords call redundant

Copy link
Member

Choose a reason for hiding this comment

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

So this was here because before we were selecting by lineCoords, while now we select by aCoords that doesn't change by the zoom or pan.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly, the active object calls setCoords, the rest should not. Mind that setActiveObject call setCoords so it seals the gap.

this.viewportTransform = [...vpt];
Copy link
Member

Choose a reason for hiding this comment

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

this wasn't spread before, it shouldn't be spread now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you sure?
When we init the canvas we spread to avoid mutation, why not here?

Copy link
Member

Choose a reason for hiding this comment

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

it is unrelated to what we are doing here. no?

this.calcViewportBoundaries();
this.renderOnAddRemove && this.requestRenderAll();
}
Expand Down
10 changes: 3 additions & 7 deletions src/shapes/ActiveSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface ActiveSelectionOptions extends GroupProps {
const activeSelectionDefaultValues: Partial<TClassProperties<ActiveSelection>> =
{
multiSelectionStacking: 'canvas-stacking',
subTargetCheck: true,
interactive: false,
};

/**
Expand Down Expand Up @@ -68,13 +70,6 @@ export class ActiveSelection extends Group {
});
}

/**
* @private
*/
_shouldSetNestedCoords() {
return true;
}

/**
* @private
* @override we don't want the selection monitor to be active
Expand Down Expand Up @@ -149,6 +144,7 @@ export class ActiveSelection extends Group {
// the object will be in the objects array of both the ActiveSelection and the Group
// but referenced in the group's _activeObjects so that it won't be rendered twice.
this._enterGroup(object, removeParentTransform);
object.setCoords();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

needs a mounted decision

}

/**
Expand Down
22 changes: 1 addition & 21 deletions src/shapes/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export class Group
/**
* Used to allow targeting of object inside groups.
* set to true if you want to select an object inside a group.\
* **REQUIRES** `subTargetCheck` set to true
* This will be not removed but slowly replaced with a method setInteractive
* that will take care of enabling subTargetCheck and necessary object events.
* There is too much attached to group interactivity to just be evaluated by a
Expand Down Expand Up @@ -284,13 +283,6 @@ export class Group
return this;
}

/**
* @private
*/
_shouldSetNestedCoords() {
return this.subTargetCheck;
}

/**
* Remove all objects
* @returns {FabricObject[]} removed objects
Expand Down Expand Up @@ -365,7 +357,7 @@ export class Group
)
);
}
this._shouldSetNestedCoords() && object.setCoords();

object._set('group', this);
object._set('canvas', this.canvas);
this._watchObject(true, object);
Expand Down Expand Up @@ -412,7 +404,6 @@ export class Group
object.calcTransformMatrix()
)
);
object.setCoords();
}
this._watchObject(false, object);
const index =
Expand Down Expand Up @@ -489,16 +480,6 @@ export class Group
this._drawClipPath(ctx, this.clipPath);
}

/**
* @override
* @return {Boolean}
*/
setCoords() {
super.setCoords();
this._shouldSetNestedCoords() &&
this.forEachObject((object) => object.setCoords());
}
Comment on lines -496 to -500
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jiayihu look here, this removes the perf hit you described


triggerLayout(options: ImperativeLayoutOptions = {}) {
this.layoutManager.performLayout({
target: this,
Expand Down Expand Up @@ -689,7 +670,6 @@ export class Group
target: group,
targets: group.getObjects(),
});
group.setCoords();
return group;
});
}
Expand Down
1 change: 0 additions & 1 deletion src/shapes/Object/InteractiveObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,6 @@ export class InteractiveFabricObject<
ctx.strokeStyle = options.cornerStrokeColor;
}
this._setLineDash(ctx, options.cornerDashArray);
this.setCoords();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

bad approach, leading to unnecessary computation and a stale state

this.forEachControl((control, key) => {
if (control.getVisibility(this, key)) {
const p = this.oCoords[key];
Expand Down
2 changes: 1 addition & 1 deletion src/shapes/Text/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,6 @@ export class FabricText<
this.setPathInfo();
}
this.initDimensions();
this.setCoords();
}

/**
Expand Down Expand Up @@ -1694,6 +1693,7 @@ export class FabricText<
}
if (this._forceClearCache) {
this.initDimensions();
this.setCoords();
}
Comment on lines 1694 to 1697
Copy link
Member

Choose a reason for hiding this comment

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

this whole thing the text has that can change the size just before rendering is wrong, is probably a patch that was done fore some reason, but this shouldn't exist. Render renders, period.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

suggestions?

Copy link
Member

Choose a reason for hiding this comment

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

i don't have a suggestion but is also evident that if there is a dependency between having the correct dimensions and being detected on screen, so for correctness this check and setCoords shoud be called before the onScreen early return, and also Object will do the isOnscreen check again.
This render method should take in account that if _forceClearCache is true, should execute the initDimension and then move to super.render. the precheck is not useful, is a double cost 99.9% of the time

super.render(ctx);
}
Expand Down
21 changes: 0 additions & 21 deletions test/unit/canvas_static.js
Original file line number Diff line number Diff line change
Expand Up @@ -1714,27 +1714,6 @@
canvas.viewportTransform = fabric.StaticCanvas.getDefaults().viewportTransform;
});

QUnit.test('setViewportTransform calls objects setCoords', function(assert) {
var vpt = [2, 0, 0, 2, 50, 50];
assert.deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix');
var rect = new fabric.Rect({ width: 10, heigth: 10 });
var rectBg = new fabric.Rect({ width: 10, heigth: 10 });
var rectOverlay = new fabric.Rect({ width: 10, heigth: 10 });
canvas.add(rect);
canvas.cancelRequestedRender();
rectBg.canvas = canvas;
canvas.backgroundImage = rectBg;
rectOverlay.canvas = canvas;
canvas.overlayImage = rectOverlay;
assert.deepEqual(new fabric.Point(rect.oCoords.tl), new fabric.Point(0,0), 'rect oCoords are set for normal viewport');
assert.equal(rectBg.oCoords, undefined, 'rectBg oCoords are not set');
assert.equal(rectOverlay.oCoords, undefined, 'rectOverlay oCoords are not set');
canvas.setViewportTransform(vpt);
assert.deepEqual(new fabric.Point(rect.oCoords.tl), new fabric.Point(50,50), 'rect oCoords are set');
assert.deepEqual(new fabric.Point(rectBg.oCoords.tl), new fabric.Point(50,50), 'rectBg oCoords are set');
assert.deepEqual(new fabric.Point(rectOverlay.oCoords.tl), new fabric.Point(50,50), 'rectOverlay oCoords are set');
});

QUnit.test('getZoom', function(assert) {
assert.ok(typeof canvas.getZoom === 'function');
var vpt = [2, 0, 0, 2, 50, 50];
Expand Down
Loading