Skip to content

Commit

Permalink
feat(util): transform utils (#7614)
Browse files Browse the repository at this point in the history
* Update misc.js

* Update misc.js

* rename

* better JSDOC

* Update misc.js

* Update util.js

* ci(): lint

* better JSDOC

* `sendObjectToPlane`

* Update misc.js

* Update util.js

* Update util.js

* Update misc.js

* rename

* Update misc.js

* Update util.js

* Update misc.js

* remove redundant tests

* Update util.js

* Update misc.js

* Update misc.js

* Update util.js

* Update util.js

* fix(): reversed transform order

* allow passing null for `sourceObject`

* Update util.js

* lint

* Update misc.js

* Update misc.js

* ci: adjust tests to accept error

* Update misc.js

* Update util.js

* build

* Revert "Update misc.js"

This reverts commit fb83a71.

* Update misc.js

* checkout

* Update util.js

* Update misc.js

* Update util.js

* typo

* Update misc.js

* optional parent

* refactor around orphan objects

* Update misc.js

* add warning

* Update object_geometry.mixin.js

* lint

* rename

* JSDOC

* Revert "JSDOC"

This reverts commit a88b0ab.

* remove unsafe `calcPlaneMatrix`

* typo

* Update misc.js

* Update misc.js
  • Loading branch information
ShaMan123 authored Feb 24, 2022
1 parent 1d447b0 commit 109efe5
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 11 deletions.
22 changes: 11 additions & 11 deletions src/canvas.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,27 +888,27 @@
/**
* Returns pointer coordinates relative to canvas.
* Can return coordinates with or without viewportTransform.
* ignoreZoom false gives back coordinates that represent
* ignoreVpt false gives back coordinates that represent
* the point clicked on canvas element.
* ignoreZoom true gives back coordinates after being processed
* ignoreVpt true gives back coordinates after being processed
* by the viewportTransform ( sort of coordinates of what is displayed
* on the canvas where you are clicking.
* ignoreZoom true = HTMLElement coordinates relative to top,left
* ignoreZoom false, default = fabric space coordinates, the same used for shape position
* To interact with your shapes top and left you want to use ignoreZoom true
* most of the time, while ignoreZoom false will give you coordinates
* ignoreVpt true = HTMLElement coordinates relative to top,left
* ignoreVpt false, default = fabric space coordinates, the same used for shape position
* To interact with your shapes top and left you want to use ignoreVpt true
* most of the time, while ignoreVpt false will give you coordinates
* compatible with the object.oCoords system.
* of the time.
* @param {Event} e
* @param {Boolean} ignoreZoom
* @param {Boolean} ignoreVpt
* @return {Object} object with "x" and "y" number values
*/
getPointer: function (e, ignoreZoom) {
getPointer: function (e, ignoreVpt) {
// return cached values if we are in the event processing chain
if (this._absolutePointer && !ignoreZoom) {
if (this._absolutePointer && !ignoreVpt) {
return this._absolutePointer;
}
if (this._pointer && ignoreZoom) {
if (this._pointer && ignoreVpt) {
return this._pointer;
}

Expand All @@ -931,7 +931,7 @@
this.calcOffset();
pointer.x = pointer.x - this._offset.left;
pointer.y = pointer.y - this._offset.top;
if (!ignoreZoom) {
if (!ignoreVpt) {
pointer = this.restorePointerVpt(pointer);
}

Expand Down
110 changes: 110 additions & 0 deletions src/util/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
PiBy180 = Math.PI / 180,
PiBy2 = Math.PI / 2;

/**
* @typedef {[number,number,number,number,number,number]} Matrix
*/

/**
* @namespace fabric.util
*/
Expand Down Expand Up @@ -287,8 +291,70 @@
);
},

/**
* Sends a point from the source coordinate plane to the destination coordinate plane.\
* From the canvas/viewer's perspective the point remains unchanged.
*
* @example <caption>Send point from canvas plane to group plane</caption>
* var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 });
* var group = new fabric.Group([obj], { strokeWidth: 0 });
* var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix());
* var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix());
* console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group
*
* @static
* @memberOf fabric.util
* @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas
* @param {fabric.Point} point
* @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane.
* @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane.
* @returns {fabric.Point} transformed point
*/
sendPointToPlane: function (point, from, to) {
// we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping)
// the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from)
var inv = fabric.util.invertTransform(to || fabric.iMatrix);
var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix);
return fabric.util.transformPoint(point, t);
},

/**
* Transform point relative to canvas.
* From the viewport/viewer's perspective the point remains unchanged.
*
* `child` relation means `point` exists in the coordinate plane created by `canvas`.
* In other words point is measured acoording to canvas' top left corner
* meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner.
*
* `sibling` relation means `point` exists in the same coordinate plane as canvas.
* In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas.
*
* @static
* @memberOf fabric.util
* @param {fabric.Point} point
* @param {fabric.StaticCanvas} canvas
* @param {'sibling'|'child'} relationBefore current relation of point to canvas
* @param {'sibling'|'child'} relationAfter desired relation of point to canvas
* @returns {fabric.Point} transformed point
*/
transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) {
if (relationBefore !== 'child' && relationBefore !== 'sibling') {
throw new Error('fabric.js: recieved bad argument ' + relationBefore);
}
if (relationAfter !== 'child' && relationAfter !== 'sibling') {
throw new Error('fabric.js: recieved bad argument ' + relationAfter);
}
if (relationBefore === relationAfter) {
return point;
}
var t = canvas.viewportTransform;
return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t);
},

/**
* Returns coordinates of points's bounding rectangle (left, top, width, height)
* @static
* @memberOf fabric.util
* @param {Array} points 4 points array
* @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix
* @return {Object} Object with left, top, width, height properties
Expand Down Expand Up @@ -1026,6 +1092,50 @@
object.setPositionByOrigin(center, 'center', 'center');
},

/**
*
* A util that abstracts applying transform to objects.\
* Sends `object` to the destination coordinate plane by applying the relevant transformations.\
* Changes the space/plane where `object` is drawn.\
* From the canvas/viewer's perspective `object` remains unchanged.
*
* @example <caption>Move clip path from one object to another while preserving it's appearance as viewed by canvas/viewer</caption>
* let obj, obj2;
* let clipPath = new fabric.Circle({ radius: 50 });
* obj.clipPath = clipPath;
* // render
* fabric.util.sendObjectToPlane(clipPath, obj.calcTransformMatrix(), obj2.calcTransformMatrix());
* obj.clipPath = undefined;
* obj2.clipPath = clipPath;
* // render, clipPath now clips obj2 but seems unchanged from the eyes of the viewer
*
* @example <caption>Clip an object's clip path with an existing object</caption>
* let obj, existingObj;
* let clipPath = new fabric.Circle({ radius: 50 });
* obj.clipPath = clipPath;
* let transformTo = fabric.util.multiplyTransformMatrices(obj.calcTransformMatrix(), clipPath.calcTransformMatrix());
* fabric.util.sendObjectToPlane(existingObj, existingObj.group?.calcTransformMatrix(), transformTo);
* clipPath.clipPath = existingObj;
*
* @static
* @memberof fabric.util
* @param {fabric.Object} object
* @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `object` is a direct child of canvas.
* @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `object` should be sent to the canvas coordinate plane.
* @returns {Matrix} the transform matrix that was applied to `object`
*/
sendObjectToPlane: function (object, from, to) {
// we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping)
// the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from)
var inv = fabric.util.invertTransform(to || fabric.iMatrix);
var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix);
fabric.util.applyTransformToObject(
object,
fabric.util.multiplyTransformMatrices(t, object.calcOwnMatrix())
);
return t;
},

/**
* given a width and height, return the size of the bounding box
* that can contains the box with width/height with applied transform
Expand Down
137 changes: 137 additions & 0 deletions test/unit/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,143 @@
assert.equal(Math.round(tp.y), 8);
});

/**
*
* @param {*} actual
* @param {*} expected
* @param {*} [message]
* @param {number} [error] floating point percision, defaults to 10
*/
QUnit.assert.matrixIsEqualEnough = function (actual, expected, message, error) {
var error = Math.pow(10, error ? -error : -10);
this.pushResult({
result: actual.every((x, i) => Math.abs(x - expected[i]) < error),
actual: actual,
expected: expected,
message: message
})
}

QUnit.test('sendPointToPlane', function (assert) {
assert.ok(typeof fabric.util.sendPointToPlane === 'function');
var m1 = [3, 0, 0, 2, 10, 4],
m2 = [1, 2, 3, 4, 5, 6],
p, t,
obj1 = new fabric.Object(),
obj2 = new fabric.Object(),
point = new fabric.Point(2, 2),
applyTransformToObject = fabric.util.applyTransformToObject,
invert = fabric.util.invertTransform,
multiply = fabric.util.multiplyTransformMatrices,
transformPoint = fabric.util.transformPoint;

function sendPointToPlane(point, from, to, relationFrom, relationTo) {
return fabric.util.sendPointToPlane(
point,
from ?
relationFrom === 'child' ? from.calcTransformMatrix() : from.group?.calcTransformMatrix() :
null,
to ?
relationTo === 'child' ? to.calcTransformMatrix() : to.group?.calcTransformMatrix() :
null
);
}

applyTransformToObject(obj1, m1);
applyTransformToObject(obj2, m2);
obj1.group = new fabric.Object();
obj2.group = new fabric.Object();
applyTransformToObject(obj1.group, m1);
applyTransformToObject(obj2.group, m2);
p = sendPointToPlane(point, obj1, obj2, 'child', 'child');
t = multiply(invert(obj2.calcTransformMatrix()), obj1.calcTransformMatrix());
assert.deepEqual(p, transformPoint(point, t));
p = sendPointToPlane(point, obj1, obj2, 'sibling', 'child');
t = multiply(invert(obj2.calcTransformMatrix()), obj1.group.calcTransformMatrix());
assert.deepEqual(p, transformPoint(point, t));
p = sendPointToPlane(point, obj1, obj2, 'child', 'sibling');
t = multiply(invert(obj2.group.calcTransformMatrix()), obj1.calcTransformMatrix());
assert.deepEqual(p, transformPoint(point, t));
p = sendPointToPlane(point, obj1, obj2, 'sibling', 'sibling');
t = multiply(invert(obj2.group.calcTransformMatrix()), obj1.group.calcTransformMatrix());
assert.deepEqual(p, transformPoint(point, t));
p = sendPointToPlane(point, null, obj2, null, 'sibling');
t = invert(obj2.group.calcTransformMatrix());
assert.deepEqual(p, transformPoint(point, t));

var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 });
var group = new fabric.Group([obj], { strokeWidth: 0 });
var sentPoint = sendPointToPlane(new fabric.Point(50, 50), null, obj, null, 'sibling');
assert.deepEqual(sentPoint, new fabric.Point(0, 0));
sentPoint = sendPointToPlane(new fabric.Point(50, 50), null, group, null, 'child');
assert.deepEqual(sentPoint, new fabric.Point(0, 0));
group.scaleX = 2;
sentPoint = sendPointToPlane(new fabric.Point(80, 50), null, obj, null, 'sibling');
assert.deepEqual(sentPoint, new fabric.Point(0, 0));
sentPoint = sendPointToPlane(new fabric.Point(80, 50), null, group, null, 'child');
assert.deepEqual(sentPoint, new fabric.Point(0, 0));
assert.deepEqual(sendPointToPlane(point), point, 'sending to nowhere, point remains unchanged');
});

QUnit.test('transformPointRelativeToCanvas', function(assert) {
assert.ok(typeof fabric.util.transformPointRelativeToCanvas === 'function');
var point = new fabric.Point(2, 2);
var matrix = [3, 0, 0, 2, 10, 4];
var canvas = {
viewportTransform: matrix
}
var transformPoint = fabric.util.transformPoint;
var invertTransform = fabric.util.invertTransform;
var transformPointRelativeToCanvas = fabric.util.transformPointRelativeToCanvas;
var p = transformPointRelativeToCanvas(point, canvas, 'sibling', 'child');
assert.deepEqual(p, transformPoint(point, invertTransform(matrix)));
p = transformPointRelativeToCanvas(point, canvas, 'child', 'sibling');
assert.deepEqual(p, transformPoint(point, matrix));
p = transformPointRelativeToCanvas(point, canvas, 'child', 'child');
assert.deepEqual(p, point);
p = transformPointRelativeToCanvas(point, canvas, 'sibling', 'sibling');
assert.deepEqual(p, point);
assert.throws(function () {
transformPointRelativeToCanvas(point, canvas, 'sibling');
});
assert.throws(function () {
transformPointRelativeToCanvas(point, canvas, 'sibling', true);
});
assert.throws(function () {
transformPointRelativeToCanvas(point, canvas, 'sibling', 'chil');
});
});

QUnit.test('sendObjectToPlane', function (assert) {
assert.ok(typeof fabric.util.sendObjectToPlane === 'function');
var m = [6, Math.SQRT1_2, 0, 3, 2, 1],
m1 = [3, 0, 0, 2, 10, 4],
m2 = [1, Math.SQRT1_2, Math.SQRT1_2, 4, 5, 6],
actual, expected,
obj1 = new fabric.Object(),
obj2 = new fabric.Object(),
obj = new fabric.Object(),
sendObjectToPlane = fabric.util.sendObjectToPlane,
applyTransformToObject = fabric.util.applyTransformToObject,
invert = fabric.util.invertTransform,
multiply = fabric.util.multiplyTransformMatrices;
// silence group check
obj1.isOnACache = () => false;

applyTransformToObject(obj, m);
applyTransformToObject(obj1, m1);
applyTransformToObject(obj2, m2);
obj.group = obj1;
actual = sendObjectToPlane(obj, obj1.calcTransformMatrix(), obj2.calcTransformMatrix());
expected = multiply(invert(obj2.calcTransformMatrix()), obj1.calcTransformMatrix());
assert.matrixIsEqualEnough(actual, expected);
assert.matrixIsEqualEnough(obj.calcOwnMatrix(), multiply(actual, m));
obj.group = obj2;
assert.matrixIsEqualEnough(obj.calcTransformMatrix(), multiply(multiply(obj2.calcTransformMatrix(), actual), m));
assert.deepEqual(sendObjectToPlane(obj2), fabric.iMatrix, 'sending to nowhere, no transform was applied');
assert.matrixIsEqualEnough(obj2.calcOwnMatrix(), m2, 'sending to nowhere, no transform was applied');
});

QUnit.test('makeBoundingBoxFromPoints', function(assert) {
assert.ok(typeof fabric.util.makeBoundingBoxFromPoints === 'function');
});
Expand Down

0 comments on commit 109efe5

Please sign in to comment.