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

feat(util): transform utils #7614

Merged
merged 55 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
93d0dfb
Update misc.js
ShaMan123 Jan 15, 2022
2e3b181
Update misc.js
ShaMan123 Jan 16, 2022
c84e119
rename
ShaMan123 Jan 16, 2022
5431213
better JSDOC
ShaMan123 Jan 16, 2022
af59788
Update misc.js
ShaMan123 Jan 16, 2022
59ea35d
Update util.js
ShaMan123 Jan 16, 2022
120d42d
ci(): lint
ShaMan123 Jan 16, 2022
0d69c2f
better JSDOC
ShaMan123 Jan 16, 2022
938a0ca
`sendObjectToPlane`
ShaMan123 Jan 16, 2022
8010d17
Update misc.js
ShaMan123 Jan 16, 2022
d0c8681
Update util.js
ShaMan123 Jan 16, 2022
b1b992a
Update util.js
ShaMan123 Jan 16, 2022
396f6e2
Update misc.js
ShaMan123 Jan 16, 2022
8214e65
rename
ShaMan123 Jan 16, 2022
64b606e
Update misc.js
ShaMan123 Jan 16, 2022
309f17d
Update util.js
ShaMan123 Jan 16, 2022
2db1852
Update misc.js
ShaMan123 Jan 16, 2022
b39bf41
remove redundant tests
ShaMan123 Jan 16, 2022
a27e2d2
Update util.js
ShaMan123 Jan 17, 2022
795e711
Update misc.js
ShaMan123 Jan 17, 2022
adfc69e
Update misc.js
ShaMan123 Jan 17, 2022
fd84080
Update util.js
ShaMan123 Jan 17, 2022
8b20b03
Update util.js
ShaMan123 Jan 17, 2022
d69cea8
fix(): reversed transform order
ShaMan123 Jan 17, 2022
3d8fd12
allow passing null for `sourceObject`
ShaMan123 Jan 17, 2022
3581b4b
Update util.js
ShaMan123 Jan 17, 2022
32b2171
lint
ShaMan123 Jan 17, 2022
9ac76ec
Update misc.js
ShaMan123 Jan 17, 2022
d389146
Update misc.js
ShaMan123 Jan 17, 2022
8250ea4
ci: adjust tests to accept error
ShaMan123 Jan 19, 2022
fb83a71
Update misc.js
ShaMan123 Jan 19, 2022
afc5699
Update util.js
ShaMan123 Jan 19, 2022
d6337d9
build
ShaMan123 Jan 19, 2022
d89b955
Revert "Update misc.js"
ShaMan123 Jan 19, 2022
8f1fa29
Update misc.js
ShaMan123 Feb 22, 2022
08ac3da
checkout
ShaMan123 Feb 22, 2022
6a488c3
Update util.js
ShaMan123 Feb 22, 2022
536b8b6
Update misc.js
ShaMan123 Feb 23, 2022
56def92
Update util.js
ShaMan123 Feb 23, 2022
7c2135e
Merge remote-tracking branch 'upstream/master' into transform-point-util
ShaMan123 Feb 23, 2022
bec4d90
typo
ShaMan123 Feb 23, 2022
ee61cfa
Update misc.js
ShaMan123 Feb 23, 2022
480d7f9
optional parent
ShaMan123 Feb 23, 2022
13e6735
refactor around orphan objects
ShaMan123 Feb 23, 2022
8ec4f9b
Update misc.js
ShaMan123 Feb 23, 2022
957f85f
add warning
ShaMan123 Feb 23, 2022
52a1aaf
Update object_geometry.mixin.js
ShaMan123 Feb 23, 2022
59fcbc5
lint
ShaMan123 Feb 23, 2022
aae85f9
rename
ShaMan123 Feb 23, 2022
a88b0ab
JSDOC
ShaMan123 Feb 23, 2022
c038034
Revert "JSDOC"
ShaMan123 Feb 23, 2022
924f74d
remove unsafe `calcPlaneMatrix`
ShaMan123 Feb 23, 2022
955feeb
typo
ShaMan123 Feb 23, 2022
b2049b8
Update misc.js
ShaMan123 Feb 23, 2022
936efa0
Update misc.js
ShaMan123 Feb 23, 2022
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
16 changes: 16 additions & 0 deletions src/mixins/object_geometry.mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,22 @@
cache.value = util.composeMatrix(options);
return cache.value;
},

/**
* Returns the transform matrix of the coordinate plane that contains object.
*
* **WARNING**\
* Do **NOT** use with clip paths.
* A clip path object isn't aware of it's containing plane (parent).
* This means this method will return the identity matrix, which is **WRONG**
*
* @returns {Array} plane matrix relative to the coordinate plane created by a canvas
*/
calcPlaneMatrix: function () {
return this.group ?
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
this.group.calcTransformMatrix() :
fabric.iMatrix.concat();
},

/*
* Calculate object dimensions from its properties
Expand Down
108 changes: 108 additions & 0 deletions src/util/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
pow = Math.pow,
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, obj.calcPlaneMatrix());
* 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,48 @@
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>
* var 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 seems unchanged from the eyes of the viewer
*
* @example <caption>Clip an object's clip path with an existing object</caption>
* // obj, existingObj;
* var clipPath = new fabric.Circle({ radius: 50 });
* obj.clipPath = clipPath;
* fabric.util.sendObjectToPlane(existingObj, existingObj.calcPlaneMatrix(), clipPath.calcTransformMatrix());
* 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
23 changes: 23 additions & 0 deletions test/unit/object_geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,29 @@
assert.ok(!cObj.isOnScreen(), 'object is completely out of viewport');
});

QUnit.test('calcPlaneMatrix', function (assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 });
var group = new fabric.Object({ width: 10, height: 15, strokeWidth: 0, scaleX: 4, scaleY: 2 });
assert.ok(typeof cObj.calcPlaneMatrix === 'function', 'calcPlaneMatrix should exist');
cObj.group = group;
cObj.top = 0;
cObj.left = 0;
cObj.scaleX = 2;
cObj.scaleY = 3;
group.scaleX = 3;
assert.deepEqual(cObj.calcPlaneMatrix(), [3, 0, 0, 2, 15, 15], 'should return group matrix');
});

QUnit.test('calcPlaneMatrix with no group', function (assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 });
assert.ok(typeof cObj.calcPlaneMatrix === 'function', 'calcPlaneMatrix should exist');
cObj.top = 0;
cObj.left = 0;
cObj.scaleX = 2;
cObj.scaleY = 3;
assert.deepEqual(cObj.calcPlaneMatrix(), fabric.iMatrix, 'without group matrix is the identity');
});

QUnit.test('calcTransformMatrix with no group', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 });
assert.ok(typeof cObj.calcTransformMatrix === 'function', 'calcTransformMatrix should exist');
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.calcPlaneMatrix() :
null,
to ?
relationTo === 'child' ? to.calcTransformMatrix() : to.calcPlaneMatrix() :
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