diff --git a/src/brushes/spray_brush.class.js b/src/brushes/spray_brush.class.js
index 50202a4131b..43e3dd4e3ca 100644
--- a/src/brushes/spray_brush.class.js
+++ b/src/brushes/spray_brush.class.js
@@ -112,7 +112,12 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric
rects = this._getOptimizedRects(rects);
}
- var group = new fabric.Group(rects);
+ var group = new fabric.Group(rects, {
+ objectCaching: true,
+ layout: 'fixed',
+ subTargetCheck: false,
+ interactive: false
+ });
this.shadow && group.set('shadow', new fabric.Shadow(this.shadow));
this.canvas.fire('before:path:created', { path: group });
this.canvas.add(group);
diff --git a/src/canvas.class.js b/src/canvas.class.js
index 4fe6fa45601..1fe1fb1eef4 100644
--- a/src/canvas.class.js
+++ b/src/canvas.class.js
@@ -441,7 +441,7 @@
}
objsToRender.push.apply(objsToRender, activeGroupObjects);
}
- // in case a single object is selected render it's entire above the other objects
+ // in case a single object is selected render it's entire parent above the other objects
else if (!this.preserveObjectStacking && activeObjects.length === 1) {
var target = activeObjects[0], ancestors = target.getAncestors(true);
var topAncestor = ancestors.length === 0 ? target : ancestors.pop();
@@ -892,7 +892,7 @@
*/
searchPossibleTargets: function (objects, pointer) {
var target = this._searchPossibleTargets(objects, pointer);
- return target;
+ return target && target.interactive && this.targets[0] ? this.targets[0] : target;
},
/**
diff --git a/src/mixins/animation.mixin.js b/src/mixins/animation.mixin.js
index 28d84661f36..4890a78094c 100644
--- a/src/mixins/animation.mixin.js
+++ b/src/mixins/animation.mixin.js
@@ -25,11 +25,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
return fabric.util.animate({
target: this,
- startValue: object.left,
+ startValue: object.getX(),
endValue: this.getCenterPoint().x,
duration: this.FX_DURATION,
onChange: function(value) {
- object.set('left', value);
+ object.setX(value);
_this.requestRenderAll();
onChange();
},
@@ -58,11 +58,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
return fabric.util.animate({
target: this,
- startValue: object.top,
+ startValue: object.getY(),
endValue: this.getCenterPoint().y,
duration: this.FX_DURATION,
onChange: function(value) {
- object.set('top', value);
+ object.setY(value);
_this.requestRenderAll();
onChange();
},
diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js
index fb7c63f2b74..f56ed9fef16 100644
--- a/src/mixins/canvas_events.mixin.js
+++ b/src/mixins/canvas_events.mixin.js
@@ -712,9 +712,13 @@
}
}
}
+ var invalidate = shouldRender || shouldGroup;
+ // we clear `_objectsToRender` in case of a change in order to repopulate it at rendering
+ // run before firing the `down` event to give the dev a chance to populate it themselves
+ invalidate && (this._objectsToRender = undefined);
this._handleEvent(e, 'down');
// we must renderAll so that we update the visuals
- (shouldRender || shouldGroup) && this.requestRenderAll();
+ invalidate && this.requestRenderAll();
},
/**
@@ -900,13 +904,19 @@
*/
_transformObject: function(e) {
var pointer = this.getPointer(e),
- transform = this._currentTransform;
+ transform = this._currentTransform,
+ target = transform.target,
+ // transform pointer to target's containing coordinate plane
+ // both pointer and object should agree on every point
+ localPointer = target.group ?
+ fabric.util.sendPointToPlane(pointer, null, target.group.calcTransformMatrix()) :
+ pointer;
transform.reset = false;
transform.shiftKey = e.shiftKey;
transform.altKey = e[this.centeredKey];
- this._performTransformAction(e, transform, pointer);
+ this._performTransformAction(e, transform, localPointer);
transform.actionPerformed && this.requestRenderAll();
},
diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js
index b59661a38ab..70ef167038e 100644
--- a/src/mixins/canvas_grouping.mixin.js
+++ b/src/mixins/canvas_grouping.mixin.js
@@ -13,8 +13,19 @@
*/
_shouldGroup: function(e, target) {
var activeObject = this._activeObject;
- return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection &&
- (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e });
+ // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection.
+ return !!activeObject && this._isSelectionKeyPressed(e) && this.selection
+ // on top of that the user also has to hit a target that is selectable.
+ && !!target && target.selectable
+ // if all pre-requisite pass, the target is either something different from the current
+ // activeObject or if an activeSelection already exists
+ // TODO at time of writing why `activeObject.type === 'activeSelection'` matter is unclear.
+ // is a very old condition uncertain if still valid.
+ && (activeObject !== target || activeObject.type === 'activeSelection')
+ // make sure `activeObject` and `target` aren't ancestors of each other
+ && !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target)
+ // target accepts selection
+ && !target.onSelect({ e: e });
},
/**
@@ -50,7 +61,7 @@
_updateActiveSelection: function(target, e) {
var activeSelection = this._activeObject,
currentActiveObjects = activeSelection._objects.slice(0);
- if (activeSelection.contains(target)) {
+ if (target.group === activeSelection) {
activeSelection.remove(target);
this._hoveredTarget = target;
this._hoveredTargets = this.targets.concat();
@@ -80,17 +91,19 @@
this._fireSelectionEvents(currentActives, e);
},
+
/**
* @private
* @param {Object} target
+ * @returns {fabric.ActiveSelection}
*/
_createGroup: function(target) {
- var objects = this._objects,
- isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target),
- groupObjects = isActiveLower
- ? [this._activeObject, target]
- : [target, this._activeObject];
- this._activeObject.isEditing && this._activeObject.exitEditing();
+ var activeObject = this._activeObject;
+ var groupObjects = target.isInFrontOf(activeObject) ?
+ [activeObject, target] :
+ [target, activeObject];
+ activeObject.isEditing && activeObject.exitEditing();
+ // handle case: target is nested
return new fabric.ActiveSelection(groupObjects, {
canvas: this
});
diff --git a/src/mixins/collection.mixin.js b/src/mixins/collection.mixin.js
index 9966d8986c2..1e91693fc4c 100644
--- a/src/mixins/collection.mixin.js
+++ b/src/mixins/collection.mixin.js
@@ -20,7 +20,7 @@ fabric.Collection = {
add: function (objects, callback) {
var size = this._objects.push.apply(this._objects, objects);
if (callback) {
- for (var i = 0, length = objects.length; i < length; i++) {
+ for (var i = 0; i < objects.length; i++) {
callback.call(this, objects[i]);
}
}
@@ -39,7 +39,7 @@ fabric.Collection = {
var args = [index, 0].concat(objects);
this._objects.splice.apply(this._objects, args);
if (callback) {
- for (var i = 2, length = args.length; i < length; i++) {
+ for (var i = 2; i < args.length; i++) {
callback.call(this, args[i]);
}
}
@@ -50,22 +50,21 @@ fabric.Collection = {
* @private
* @param {fabric.Object[]} objectsToRemove objects to remove
* @param {(object:fabric.Object) => any} [callback] function to call for each object removed
- * @returns {boolean} true if objects were removed
+ * @returns {fabric.Object[]} removed objects
*/
remove: function(objectsToRemove, callback) {
- var objects = this._objects,
- index, somethingRemoved = false;
-
- for (var i = 0, length = objectsToRemove.length; i < length; i++) {
- index = objects.indexOf(objectsToRemove[i]);
+ var objects = this._objects, removed = [];
+ for (var i = 0, object, index; i < objectsToRemove.length; i++) {
+ object = objectsToRemove[i];
+ index = objects.indexOf(object);
// only call onObjectRemoved if an object was actually removed
if (index !== -1) {
- somethingRemoved = true;
objects.splice(index, 1);
- callback && callback.call(this, objectsToRemove[i]);
+ removed.push(object);
+ callback && callback.call(this, object);
}
}
- return somethingRemoved;
+ return removed;
},
/**
@@ -82,7 +81,7 @@ fabric.Collection = {
*/
forEachObject: function(callback, context) {
var objects = this.getObjects();
- for (var i = 0, len = objects.length; i < len; i++) {
+ for (var i = 0; i < objects.length; i++) {
callback.call(context, objects[i], i, objects);
}
return this;
@@ -131,6 +130,7 @@ fabric.Collection = {
/**
* Returns true if collection contains an object.\
* **Prefer using {@link `fabric.Object#isDescendantOf`} for performance reasons**
+ * instead of a.contains(b) use b.isDescendantOf(a)
* @param {Object} object Object to check against
* @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects`
* @return {Boolean} `true` if collection contains an object
diff --git a/src/mixins/eraser_brush.mixin.js b/src/mixins/eraser_brush.mixin.js
index 4400e5aca41..a8647165ff0 100644
--- a/src/mixins/eraser_brush.mixin.js
+++ b/src/mixins/eraser_brush.mixin.js
@@ -125,7 +125,6 @@
/* _TO_SVG_END_ */
});
- var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState;
fabric.util.object.extend(fabric.Group.prototype, {
/**
* @private
@@ -181,15 +180,6 @@
});
}
});
- },
-
- /**
- * Propagate the group's eraser to its objects, crucial for proper functionality of the eraser within the group and nested objects.
- * @private
- */
- _restoreObjectsState: function () {
- this.erasable === true && this.applyEraserToObjects();
- return __restoreObjectsState.call(this);
}
});
@@ -217,14 +207,6 @@
*/
originY: 'center',
- drawObject: function (ctx) {
- ctx.save();
- ctx.fillStyle = 'black';
- ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
- ctx.restore();
- this.callSuper('drawObject', ctx);
- },
-
/**
* eraser should retain size
* dimensions should not change when paths are added or removed
@@ -232,8 +214,14 @@
* @override
* @private
*/
- _getBounds: function () {
- // noop
+ layout: 'fixed',
+
+ drawObject: function (ctx) {
+ ctx.save();
+ ctx.fillStyle = 'black';
+ ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
+ ctx.restore();
+ this.callSuper('drawObject', ctx);
},
/* _TO_SVG_START_ */
diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js
index 8c58cf68d5d..9ded51c0747 100644
--- a/src/mixins/itext_behavior.mixin.js
+++ b/src/mixins/itext_behavior.mixin.js
@@ -25,8 +25,9 @@
*/
initAddedHandler: function() {
var _this = this;
- this.on('added', function() {
- var canvas = _this.canvas;
+ this.on('added', function (opt) {
+ // make sure we listen to the canvas added event
+ var canvas = opt.target;
if (canvas) {
if (!canvas._hasITextHandlers) {
canvas._hasITextHandlers = true;
@@ -40,8 +41,9 @@
initRemovedHandler: function() {
var _this = this;
- this.on('removed', function() {
- var canvas = _this.canvas;
+ this.on('removed', function (opt) {
+ // make sure we listen to the canvas removed event
+ var canvas = opt.target;
if (canvas) {
canvas._iTextInstances = canvas._iTextInstances || [];
fabric.util.removeFromArray(canvas._iTextInstances, _this);
diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js
index 69b14ed9a13..b1bac2b8895 100644
--- a/src/mixins/itext_click_behavior.mixin.js
+++ b/src/mixins/itext_click_behavior.mixin.js
@@ -152,7 +152,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
*/
mouseUpHandler: function(options) {
this.__isMousedown = false;
- if (!this.editable || this.group ||
+ if (!this.editable ||
+ (this.group && !this.group.interactive) ||
(options.transform && options.transform.actionPerformed) ||
(options.e.button && options.e.button !== 1)) {
return;
diff --git a/src/mixins/object_ancestry.mixin.js b/src/mixins/object_ancestry.mixin.js
index 594da24a03d..8c74ea48d2b 100644
--- a/src/mixins/object_ancestry.mixin.js
+++ b/src/mixins/object_ancestry.mixin.js
@@ -1,17 +1,122 @@
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
- *
- * @param {boolean} [strict] returns only ancestors that are objects (without canvas)
- * @returns {(fabric.Object | fabric.StaticCanvas)[]} ancestors from bottom to top
- */
+ * Checks if object is decendant of target
+ * Should be used instead of @link {fabric.Collection.contains} for performance reasons
+ * @param {fabric.Object|fabric.StaticCanvas} target
+ * @returns {boolean}
+ */
+ isDescendantOf: function (target) {
+ var parent = this.group || this.canvas;
+ while (parent) {
+ if (target === parent) {
+ return true;
+ }
+ else if (parent instanceof fabric.StaticCanvas) {
+ // happens after all parents were traversed through without a match
+ return false;
+ }
+ parent = parent.group || parent.canvas;
+ }
+ return false;
+ },
+
+ /**
+ *
+ * @typedef {fabric.Object[] | [...fabric.Object[], fabric.StaticCanvas]} Ancestors
+ *
+ * @param {boolean} [strict] returns only ancestors that are objects (without canvas)
+ * @returns {Ancestors} ancestors from bottom to top
+ */
getAncestors: function (strict) {
var ancestors = [];
- var parent = this.group || (!strict ? this.canvas : undefined);
+ var parent = this.group || (strict ? undefined : this.canvas);
while (parent) {
ancestors.push(parent);
- parent = parent.group || (!strict ? parent.canvas : undefined);
+ parent = parent.group || (strict ? undefined : parent.canvas);
}
return ancestors;
+ },
+
+ /**
+ * Returns an object that represent the ancestry situation.
+ *
+ * @typedef {object} AncestryComparison
+ * @property {Ancestors} common ancestors of `this` and `other` (may include `this` | `other`)
+ * @property {Ancestors} fork ancestors that are of `this` only
+ * @property {Ancestors} otherFork ancestors that are of `other` only
+ *
+ * @param {fabric.Object} other
+ * @param {boolean} [strict] finds only ancestors that are objects (without canvas)
+ * @returns {AncestryComparison | undefined}
+ *
+ */
+ findCommonAncestors: function (other, strict) {
+ if (this === other) {
+ return {
+ fork: [],
+ otherFork: [],
+ common: [this].concat(this.getAncestors(strict))
+ };
+ }
+ else if (!other) {
+ // meh, warn and inform, and not my issue.
+ // the argument is NOT optional, we can't end up here.
+ return undefined;
+ }
+ var ancestors = this.getAncestors(strict);
+ var otherAncestors = other.getAncestors(strict);
+ // if `this` has no ancestors and `this` is top ancestor of `other` we must handle the following case
+ if (ancestors.length === 0 && otherAncestors.length > 0 && this === otherAncestors[otherAncestors.length - 1]) {
+ return {
+ fork: [],
+ otherFork: [other].concat(otherAncestors.slice(0, otherAncestors.length - 1)),
+ common: [this]
+ };
+ }
+ // compare ancestors
+ for (var i = 0, ancestor; i < ancestors.length; i++) {
+ ancestor = ancestors[i];
+ if (ancestor === other) {
+ return {
+ fork: [this].concat(ancestors.slice(0, i)),
+ otherFork: [],
+ common: ancestors.slice(i)
+ };
+ }
+ for (var j = 0; j < otherAncestors.length; j++) {
+ if (this === otherAncestors[j]) {
+ return {
+ fork: [],
+ otherFork: [other].concat(otherAncestors.slice(0, j)),
+ common: [this].concat(ancestors)
+ };
+ }
+ if (ancestor === otherAncestors[j]) {
+ return {
+ fork: [this].concat(ancestors.slice(0, i)),
+ otherFork: [other].concat(otherAncestors.slice(0, j)),
+ common: ancestors.slice(i)
+ };
+ }
+ }
+ }
+ // nothing shared
+ return {
+ fork: [this].concat(ancestors),
+ otherFork: [other].concat(otherAncestors),
+ common: []
+ };
+ },
+
+ /**
+ *
+ * @param {fabric.Object} other
+ * @param {boolean} [strict] checks only ancestors that are objects (without canvas)
+ * @returns {boolean}
+ */
+ hasCommonAncestors: function (other, strict) {
+ var commonAncestors = this.findCommonAncestors(other, strict);
+ return commonAncestors && !!commonAncestors.ancestors.length;
}
});
diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js
index 43cce0216fa..c1a796cc418 100644
--- a/src/mixins/object_geometry.mixin.js
+++ b/src/mixins/object_geometry.mixin.js
@@ -66,6 +66,113 @@
*/
controls: { },
+ /**
+ * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane
+ */
+ getX: function () {
+ return this.getXY().x;
+ },
+
+ /**
+ * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane
+ */
+ setX: function (value) {
+ this.setXY(this.getXY().setX(value));
+ },
+
+ /**
+ * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\
+ * if parent is canvas then this property is identical to {@link fabric.Object#getX}
+ */
+ getRelativeX: function () {
+ return this.left;
+ },
+
+ /**
+ * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\
+ * if parent is canvas then this method is identical to {@link fabric.Object#setX}
+ */
+ setRelativeX: function (value) {
+ this.left = value;
+ },
+
+ /**
+ * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane
+ */
+ getY: function () {
+ return this.getXY().y;
+ },
+
+ /**
+ * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane
+ */
+ setY: function (value) {
+ this.setXY(this.getXY().setY(value));
+ },
+
+ /**
+ * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\
+ * if parent is canvas then this property is identical to {@link fabric.Object#getY}
+ */
+ getRelativeY: function () {
+ return this.top;
+ },
+
+ /**
+ * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\
+ * if parent is canvas then this property is identical to {@link fabric.Object#setY}
+ */
+ setRelativeY: function (value) {
+ this.top = value;
+ },
+
+ /**
+ * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane
+ */
+ getXY: function () {
+ var relativePosition = this.getRelativeXY();
+ return this.group ?
+ fabric.util.transformPoint(relativePosition, this.group.calcTransformMatrix()) :
+ relativePosition;
+ },
+
+ /**
+ * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate.
+ * You can specify {@link fabric.Object#originX} and {@link fabric.Object#originY} values,
+ * that otherwise are the object's current values.
+ * @example
Set object's bottom left corner to point (5,5) on canvas
+ * object.setXY(new fabric.Point(5, 5), 'left', 'bottom').
+ * @param {fabric.Point} point position in canvas coordinate plane
+ * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right'
+ * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom'
+ */
+ setXY: function (point, originX, originY) {
+ if (this.group) {
+ point = fabric.util.transformPoint(
+ point,
+ fabric.util.invertTransform(this.group.calcTransformMatrix())
+ );
+ }
+ this.setRelativeXY(point, originX, originY);
+ },
+
+ /**
+ * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane
+ */
+ getRelativeXY: function () {
+ return new fabric.Point(this.left, this.top);
+ },
+
+ /**
+ * As {@link fabric.Object#setXY}, but in current parent's coordinate plane ( the current group if any or the canvas)
+ * @param {fabric.Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane
+ * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right'
+ * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom'
+ */
+ setRelativeXY: function (point, originX, originY) {
+ this.setPositionByOrigin(point, originX || this.originX, originY || this.originY);
+ },
+
/**
* return correct set of coordinates for intersection
* this will return either aCoords or lineCoords.
@@ -88,8 +195,15 @@
* The coords are returned in an array.
* @return {Array} [tl, tr, br, bl] of points
*/
- getCoords: function(absolute, calculate) {
- return arrayFromCoords(this._getCoords(absolute, calculate));
+ getCoords: function (absolute, calculate) {
+ var coords = arrayFromCoords(this._getCoords(absolute, calculate));
+ if (this.group) {
+ var t = this.group.calcTransformMatrix();
+ return coords.map(function (p) {
+ return util.transformPoint(p, t);
+ });
+ }
+ return coords;
},
/**
@@ -630,9 +744,12 @@
scaleY: this.scaleY,
skewX: this.skewX,
skewY: this.skewY,
+ width: this.width,
+ height: this.height,
+ strokeWidth: this.strokeWidth
}, options || {});
// stroke is applied before/after transformations are applied according to `strokeUniform`
- var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = this.strokeWidth;
+ var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = options.strokeWidth;
if (this.strokeUniform) {
preScalingStrokeValue = 0;
postScalingStrokeValue = strokeWidth;
@@ -641,8 +758,8 @@
preScalingStrokeValue = strokeWidth;
postScalingStrokeValue = 0;
}
- var dimX = this.width + preScalingStrokeValue,
- dimY = this.height + preScalingStrokeValue,
+ var dimX = options.width + preScalingStrokeValue,
+ dimY = options.height + preScalingStrokeValue,
finalDimensions,
noSkew = options.skewX === 0 && options.skewY === 0;
if (noSkew) {
diff --git a/src/mixins/object_stacking.mixin.js b/src/mixins/object_stacking.mixin.js
index 8c8e87d5efd..6af83291af0 100644
--- a/src/mixins/object_stacking.mixin.js
+++ b/src/mixins/object_stacking.mixin.js
@@ -76,5 +76,35 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
this.canvas.moveTo(this, index);
}
return this;
+ },
+
+ /**
+ *
+ * @param {fabric.Object} other object to compare against
+ * @returns {boolean | undefined} if objects do not share a common ancestor or they are strictly equal it is impossible to determine which is in front of the other; in such cases the function returns `undefined`
+ */
+ isInFrontOf: function (other) {
+ if (this === other) {
+ return undefined;
+ }
+ var ancestorData = this.findCommonAncestors(other);
+ if (!ancestorData) {
+ return undefined;
+ }
+ if (ancestorData.fork.includes(other)) {
+ return true;
+ }
+ if (ancestorData.otherFork.includes(this)) {
+ return false;
+ }
+ var firstCommonAncestor = ancestorData.common[0];
+ if (!firstCommonAncestor) {
+ return undefined;
+ }
+ var headOfFork = ancestorData.fork.pop(),
+ headOfOtherFork = ancestorData.otherFork.pop(),
+ thisIndex = firstCommonAncestor._objects.indexOf(headOfFork),
+ otherIndex = firstCommonAncestor._objects.indexOf(headOfOtherFork);
+ return thisIndex > -1 && thisIndex > otherIndex;
}
});
diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js
index a2b648752d0..4d64b2aaa19 100644
--- a/src/shapes/active_selection.class.js
+++ b/src/shapes/active_selection.class.js
@@ -52,6 +52,33 @@
this.setCoords();
},
+ /**
+ * @private
+ */
+ _shouldSetNestedCoords: function () {
+ return true;
+ },
+
+ /**
+ * @private
+ * @param {fabric.Object} object
+ * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
+ * @returns {boolean} true if object entered group
+ */
+ enterGroup: function (object, removeParentTransform) {
+ if (!this.canEnter(object)) {
+ return false;
+ }
+ if (object.group) {
+ // save ref to group for later in order to return to it
+ var parent = object.group;
+ parent._exitGroup(object);
+ object.__owningGroup = parent;
+ }
+ this._enterGroup(object, removeParentTransform);
+ return true;
+ },
+
/**
* we want objects to retain their canvas ref when exiting instance
* @private
@@ -60,6 +87,36 @@
*/
exitGroup: function (object, removeParentTransform) {
this._exitGroup(object, removeParentTransform);
+ var parent = object.__owningGroup;
+ if (parent) {
+ // return to owning group
+ parent.enterGroup(object);
+ delete object.__owningGroup;
+ }
+ },
+
+ /**
+ * @private
+ * @param {'added'|'removed'} type
+ * @param {fabric.Object[]} targets
+ */
+ _onAfterObjectsChange: function (type, targets) {
+ var groups = [];
+ targets.forEach(function (object) {
+ object.group && !groups.includes(object.group) && groups.push(object.group);
+ });
+ if (type === 'removed') {
+ // invalidate groups' layout and mark as dirty
+ groups.forEach(function (group) {
+ group._onAfterObjectsChange('added', targets);
+ });
+ }
+ else {
+ // mark groups as dirty
+ groups.forEach(function (group) {
+ group._set('dirty', true);
+ });
+ }
},
/**
@@ -115,7 +172,7 @@
childrenOverride.hasControls = false;
}
childrenOverride.forActiveSelection = true;
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ for (var i = 0; i < this._objects.length; i++) {
this._objects[i]._renderControls(ctx, childrenOverride);
}
ctx.restore();
diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js
index 9e9c47905d6..e26412d6d91 100644
--- a/src/shapes/group.class.js
+++ b/src/shapes/group.class.js
@@ -58,7 +58,7 @@
/**
* Used to optimize performance
- * set to `false` if you don't need caontained objects to be target of events
+ * set to `false` if you don't need contained objects to be targets of events
* @default
* @type boolean
*/
@@ -66,8 +66,8 @@
/**
* 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
+ * set to true if you want to select an object inside a group.\
+ * **REQUIRES** `subTargetCheck` set to true
* @default
* @type boolean
*/
@@ -97,20 +97,9 @@
this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false);
this._firstLayoutDone = false;
this.callSuper('initialize', options);
- if (objectsRelativeToGroup) {
- this.forEachObject(function (object) {
- this.enterGroup(object, false);
- }, this);
- }
- else {
- // we need to preserve object's center point in relation to canvas and apply group's transform to it
- var inv = invertTransform(this.calcTransformMatrix());
- this.forEachObject(function (object) {
- this.enterGroup(object, false);
- var center = transformPoint(object.getCenterPoint(), inv);
- object.setPositionByOrigin(center, 'center', 'center');
- }, this);
- }
+ this.forEachObject(function (object) {
+ this.enterGroup(object, false);
+ }, this);
this._applyLayoutStrategy({
type: 'initialization',
options: options,
@@ -140,6 +129,13 @@
return this;
},
+ /**
+ * @private
+ */
+ _shouldSetNestedCoords: function () {
+ return this.subTargetCheck;
+ },
+
/**
* Add objects
* @param {...fabric.Object} objects
@@ -155,17 +151,19 @@
* @param {Number} index Index to insert object at
*/
insertAt: function (objects, index) {
- fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded);
+ fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded);
this._onAfterObjectsChange('added', Array.isArray(objects) ? objects : [objects]);
},
/**
* Remove objects
* @param {...fabric.Object} objects
+ * @returns {fabric.Object[]} removed objects
*/
remove: function () {
- fabric.Collection.remove.call(this, arguments, this._onObjectRemoved);
- this._onAfterObjectsChange('removed', Array.from(arguments));
+ var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved);
+ this._onAfterObjectsChange('removed', removed);
+ return removed;
},
/**
@@ -174,9 +172,7 @@
*/
removeAll: function () {
this._activeObjects = [];
- var remove = this._objects.slice();
- this.remove.apply(this, remove);
- return remove;
+ return this.remove.apply(this, this._objects.slice());
},
/**
@@ -224,23 +220,53 @@
object[directive]('deselected', this.__objectSelectionDisposer);
},
+ /**
+ * Checks if object can enter group and logs relevant warnings
+ * @private
+ * @param {fabric.Object} object
+ * @returns
+ */
+ canEnter: function (object) {
+ if (object === this || this.isDescendantOf(object)) {
+ /* _DEV_MODE_START_ */
+ console.warn('fabric.Group: trying to add group to itself, this call has no effect');
+ /* _DEV_MODE_END_ */
+ return false;
+ }
+ else if (object.group && object.group === this) {
+ /* _DEV_MODE_START_ */
+ console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect');
+ /* _DEV_MODE_END_ */
+ return false;
+ }
+ return true;
+ },
+
/**
* @private
* @param {fabric.Object} object
* @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
+ * @returns {boolean} true if object entered group
*/
enterGroup: function (object, removeParentTransform) {
+ if (!this.canEnter(object)) {
+ return false;
+ }
if (object.group) {
- if (object.group === this) {
- /* _DEV_MODE_START_ */
- console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect');
- /* _DEV_MODE_END_ */
- return;
- }
object.group.remove(object);
}
- // can be this converted to utils?
+ this._enterGroup(object, removeParentTransform);
+ return true;
+ },
+
+ /**
+ * @private
+ * @param {fabric.Object} object
+ * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
+ */
+ _enterGroup: function (object, removeParentTransform) {
if (removeParentTransform) {
+ // can this be converted to utils (sendObjectToPlane)?
applyTransformToObject(
object,
multiplyTransformMatrices(
@@ -249,14 +275,13 @@
)
);
}
- object.setCoords();
+ this._shouldSetNestedCoords() && object.setCoords();
object._set('group', this);
object._set('canvas', this.canvas);
this.interactive && this._watchObject(true, object);
var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject();
// if we are adding the activeObject in a group
- // TODO migrate back to isDescendantOf
- if (activeObject && (activeObject === object || (object.contains && object.contains(activeObject)))) {
+ if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) {
this._activeObjects.push(object);
}
},
@@ -346,7 +371,7 @@
shouldCache: function() {
var ownCache = fabric.Object.prototype.shouldCache.call(this);
if (ownCache) {
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ for (var i = 0; i < this._objects.length; i++) {
if (this._objects[i].willDrawShadow()) {
this.ownCaching = false;
return false;
@@ -364,7 +389,7 @@
if (fabric.Object.prototype.willDrawShadow.call(this)) {
return true;
}
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ for (var i = 0; i < this._objects.length; i++) {
if (this._objects[i].willDrawShadow()) {
return true;
}
@@ -385,7 +410,8 @@
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawObject: function(ctx) {
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ this._renderBackground(ctx);
+ for (var i = 0; i < this._objects.length; i++) {
this._objects[i].render(ctx);
}
this._drawClipPath(ctx, this.clipPath);
@@ -401,7 +427,7 @@
if (!this.statefullCache) {
return false;
}
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ for (var i = 0; i < this._objects.length; i++) {
if (this._objects[i].isCacheDirty(true)) {
if (this._cacheCanvas) {
// if this group has not a cache canvas there is nothing to clean
@@ -420,7 +446,7 @@
*/
setCoords: function () {
this.callSuper('setCoords');
- (this.subTargetCheck || this.type === 'activeSelection') && this.forEachObject(function (object) {
+ this._shouldSetNestedCoords() && this.forEachObject(function (object) {
object.setCoords();
});
},
@@ -462,7 +488,7 @@
/**
* initial layout logic:
- * calculate bbox of objects (if necessary) and translate it according to options recieved from the constructor (left, top, width, height)
+ * calculate bbox of objects (if necessary) and translate it according to options received from the constructor (left, top, width, height)
* so it is placed in the center of the bbox received from the constructor
*
* @private
@@ -850,6 +876,19 @@
/* _TO_SVG_START_ */
+ /**
+ * @private
+ */
+ _createSVGBgRect: function (reviver) {
+ if (!this.backgroundColor) {
+ return '';
+ }
+ var fillStroke = fabric.Rect.prototype._toSVG.call(this, reviver);
+ var commons = fillStroke.indexOf('COMMON_PARTS');
+ fillStroke[commons] = 'for="group" ';
+ return fillStroke.join('');
+ },
+
/**
* Returns svg representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
@@ -857,7 +896,9 @@
*/
_toSVG: function (reviver) {
var svgString = ['\n'];
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ var bg = this._createSVGBgRect(reviver);
+ bg && svgString.push('\t\t', bg);
+ for (var i = 0; i < this._objects.length; i++) {
svgString.push('\t\t', this._objects[i].toSVG(reviver));
}
svgString.push('\n');
@@ -886,7 +927,9 @@
*/
toClipPathSVG: function (reviver) {
var svgString = [];
- for (var i = 0, len = this._objects.length; i < len; i++) {
+ var bg = this._createSVGBgRect(reviver);
+ bg && svgString.push('\t', bg);
+ for (var i = 0; i < this._objects.length; i++) {
svgString.push('\t', this._objects[i].toClipPathSVG(reviver));
}
return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver });
diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js
index 614afcd3df8..ca254eadb6e 100644
--- a/src/static_canvas.class.js
+++ b/src/static_canvas.class.js
@@ -596,8 +596,8 @@
* @chainable
*/
remove: function () {
- var didRemove = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved);
- didRemove && this.renderOnAddRemove && this.requestRenderAll();
+ var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved);
+ removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll();
return this;
},
@@ -617,7 +617,7 @@
obj._set('canvas', this);
obj.setCoords();
this.fire('object:added', { target: obj });
- obj.fire('added');
+ obj.fire('added', { target: this });
},
/**
@@ -626,8 +626,8 @@
*/
_onObjectRemoved: function(obj) {
this.fire('object:removed', { target: obj });
- obj.fire('removed');
- delete obj.canvas;
+ obj.fire('removed', { target: this });
+ obj._set('canvas', undefined);
},
/**
@@ -966,7 +966,7 @@
* @chainable
*/
_centerObject: function(object, center) {
- object.setPositionByOrigin(center, 'center', 'center');
+ object.setXY(center, 'center', 'center');
object.setCoords();
this.renderOnAddRemove && this.requestRenderAll();
return this;
diff --git a/src/util/misc.js b/src/util/misc.js
index 6c9c122d0ef..5fe519b357e 100644
--- a/src/util/misc.js
+++ b/src/util/misc.js
@@ -339,10 +339,10 @@
*/
transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) {
if (relationBefore !== 'child' && relationBefore !== 'sibling') {
- throw new Error('fabric.js: recieved bad argument ' + relationBefore);
+ throw new Error('fabric.js: received bad argument ' + relationBefore);
}
if (relationAfter !== 'child' && relationAfter !== 'sibling') {
- throw new Error('fabric.js: recieved bad argument ' + relationAfter);
+ throw new Error('fabric.js: received bad argument ' + relationAfter);
}
if (relationBefore === relationAfter) {
return point;
diff --git a/test/unit/collection.js b/test/unit/collection.js
index 064fae5d4d7..e2e5504457d 100644
--- a/test/unit/collection.js
+++ b/test/unit/collection.js
@@ -95,16 +95,19 @@
assert.ok(typeof collection.remove === 'function', 'has remove method');
var returned = collection.remove([obj]);
assert.ok(returned, 'removed obj');
- assert.ok(!collection.remove([{ prop: 'foo' }]), 'nothing removed');
+ assert.ok(Array.isArray(collection.remove([])), 'should return empty array');
+ assert.equal(collection.remove([{ prop: 'foo' }]).length, 0, 'nothing removed');
assert.equal(collection._objects.indexOf(obj), -1, 'obj is no more in array');
assert.equal(collection._objects.length, previousLength - 1, 'length has changed');
assert.equal(fired, 0, 'fired is 0');
var callback = function() {
fired++;
};
- collection.remove([obj2], callback);
+ var removed = collection.remove([obj2], callback);
assert.equal(fired, 1, 'fired is incremented if there is a callback');
- collection.remove([obj2], callback);
+ assert.deepEqual(removed, [obj2], 'should return removed objects');
+ removed = collection.remove([obj2], callback);
+ assert.deepEqual(removed, [], 'should return removed objects');
assert.equal(fired, 1, 'fired is not incremented again if there is no object to remove');
collection.add([obj2]);
@@ -112,7 +115,8 @@
collection.remove([obj2], callback);
previousLength = collection._objects.length;
fired = 0;
- collection.remove([obj, obj3], callback);
+ removed = collection.remove([obj, obj3, obj2], callback);
+ assert.deepEqual(removed, [obj, obj3], 'should return removed objects');
assert.equal(collection._objects.length, previousLength - 2, 'we have 2 objects less');
assert.equal(fired, 2, 'fired is incremented for every object removed');
});
diff --git a/test/unit/group.js b/test/unit/group.js
index 88aa19625f9..b3183c00058 100644
--- a/test/unit/group.js
+++ b/test/unit/group.js
@@ -100,10 +100,12 @@
group = new fabric.Group([rect1, rect2, rect3]);
assert.ok(typeof group.remove === 'function');
- group.remove(rect2);
+ var removed = group.remove(rect2);
+ assert.deepEqual(removed, [rect2], 'should return removed objects');
assert.deepEqual(group.getObjects(), [rect1, rect3], 'should remove object properly');
- group.remove(rect1, rect3);
+ var removed = group.remove(rect1, rect3);
+ assert.deepEqual(removed, [rect1, rect3], 'should return removed objects');
assert.equal(group.isEmpty(), true, 'group should be empty');
});
@@ -316,7 +318,8 @@
assert.ok(initialLeftValue !== firstObject.get('left'));
assert.ok(initialTopValue !== firstObject.get('top'));
- group.removeAll();
+ var objects = group.getObjects();
+ assert.deepEqual(group.removeAll(), objects, 'should remove all objects');
assert.equal(firstObject.get('left'), initialLeftValue, 'should restore initial left value');
assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value');
});
@@ -424,6 +427,7 @@
var group = makeGroupWith2ObjectsWithOpacity();
var groupObject = group.toObject();
+ groupObject.subTargetCheck = true;
fabric.Group.fromObject(groupObject).then(function(newGroupFromObject) {
assert.ok(newGroupFromObject._objects[0].lineCoords.tl, 'acoords 0 are restored');
@@ -692,6 +696,35 @@
assert.notEqual(coords, newCoords, 'object coords have been recalculated - add');
});
+ QUnit.test('group add edge cases', function (assert) {
+ var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false }),
+ rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false }),
+ group = new fabric.Group([rect1]);
+
+ // duplicate
+ assert.notOk(group.canEnter(rect1));
+ // group.add(rect1);
+ // assert.deepEqual(group.getObjects(), [rect1], 'objects should not have changed');
+ // duplicate on same call
+ assert.ok(group.canEnter(rect2));
+ // group.add(rect2, rect2);
+ // assert.deepEqual(group.getObjects(), [rect1, rect2], '`rect2` should have entered once');
+ // adding self
+ assert.notOk(group.canEnter(group));
+ // group.add(group);
+ // assert.deepEqual(group.getObjects(), [rect1, rect2], 'objects should not have changed');
+ // nested object should be removed from group
+ var nestedGroup = new fabric.Group([rect1]);
+ assert.ok(group.canEnter(nestedGroup));
+ // group.add(nestedGroup);
+ // assert.deepEqual(group.getObjects(), [rect2, nestedGroup], '`rect1` was removed from group once it entered `nestedGroup`');
+ // circular group
+ var circularGroup = new fabric.Group([rect2, group]);
+ assert.notOk(group.canEnter(circularGroup), 'circular group should be denied entry');
+ // group.add(circularGroup);
+ // assert.deepEqual(group.getObjects(), [rect2, nestedGroup], 'objects should not have changed');
+ });
+
QUnit.test('group remove', function(assert) {
var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}),
rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}),
diff --git a/test/unit/itext_click_behaviour.js b/test/unit/itext_click_behaviour.js
index 7d43901c7d3..fb5af39f3dd 100644
--- a/test/unit/itext_click_behaviour.js
+++ b/test/unit/itext_click_behaviour.js
@@ -167,21 +167,40 @@
iText.selected = true;
iText.__lastSelected = true;
iText.mouseUpHandler({ e: {} });
- assert.equal(iText.isEditing, false, 'iText did not enter editing');
+ assert.equal(iText.isEditing, false, 'iText should not enter editing');
iText.exitEditing();
});
- QUnit.test('_mouseUpHandler on a selected text in a group DOES NOT enter edit', function(assert) {
+ QUnit.test('_mouseUpHandler on a selected text in a group does NOT enter editing', function(assert) {
var iText = new fabric.IText('test');
iText.initDelayedCursor = function() {};
iText.renderCursorOrSelection = function() {};
assert.equal(iText.isEditing, false, 'iText not editing');
- iText.canvas = canvas;
+ var group = new fabric.Group([iText], { subTargetCheck: false });
+ canvas.add(group);
iText.selected = true;
iText.__lastSelected = true;
- iText.group = true;
- iText.mouseUpHandler({ e: {} });
- assert.equal(iText.isEditing, false, 'iText did not entered editing');
+ canvas.__onMouseUp({ clientX: 1, clientY: 1 });
+ assert.equal(canvas._target, group, 'group should be found as target');
+ assert.equal(iText.isEditing, false, 'iText should not enter editing');
+ iText.exitEditing();
+ });
+ QUnit.test('_mouseUpHandler on a text in a group', function (assert) {
+ var iText = new fabric.IText('test');
+ iText.initDelayedCursor = function () { };
+ iText.renderCursorOrSelection = function () { };
+ assert.equal(iText.isEditing, false, 'iText not editing');
+ var group = new fabric.Group([iText], { subTargetCheck: true, interactive: true });
+ canvas.add(group);
+ iText.selected = true;
+ iText.__lastSelected = true;
+ canvas.__onMouseUp({ clientX: 1, clientY: 1 });
+ assert.equal(iText.isEditing, true, 'iText should enter editing');
iText.exitEditing();
+ group.interactive = false;
+ iText.selected = true;
+ iText.__lastSelected = true;
+ canvas.__onMouseUp({ clientX: 1, clientY: 1 });
+ assert.equal(iText.isEditing, false, 'iText should not enter editing');
});
QUnit.test('_mouseUpHandler on a corner of selected text DOES NOT enter edit', function(assert) {
var iText = new fabric.IText('test');
@@ -193,7 +212,7 @@
iText.__lastSelected = true;
iText.__corner = 'mt';
iText.mouseUpHandler({ e: {} });
- assert.equal(iText.isEditing, false, 'iText did not entered editing');
+ assert.equal(iText.isEditing, false, 'iText should not enter editing');
iText.exitEditing();
canvas.renderAll();
});
diff --git a/test/unit/object.js b/test/unit/object.js
index 54807171622..b664497acc4 100644
--- a/test/unit/object.js
+++ b/test/unit/object.js
@@ -622,7 +622,10 @@
var object = new fabric.Object();
var addedEventFired = false;
- object.on('added', function() { addedEventFired = true; });
+ object.on('added', function (opt) {
+ addedEventFired = true;
+ assert.ok(opt.target === canvas, 'target should equal to canvas');
+ });
canvas.add(object);
assert.ok(addedEventFired);
@@ -645,7 +648,10 @@
canvas.add(object);
- object.on('removed', function() { removedEventFired = true; });
+ object.on('removed', function (opt) {
+ removedEventFired = true;
+ assert.ok(opt.target === canvas, 'target should equal to canvas');
+ });
canvas.remove(object);
assert.ok(removedEventFired);
@@ -803,6 +809,289 @@
assert.equal(object.moveTo(), object, 'should be chainable');
});
+ QUnit.test('isDescendantOf', function (assert) {
+ var object = new fabric.Object();
+ var parent = new fabric.Object();
+ assert.ok(typeof object.isDescendantOf === 'function');
+ parent.canvas = canvas;
+ object.group = parent;
+ assert.ok(object.isDescendantOf(parent));
+ object.group = {
+ group: parent
+ };
+ assert.ok(object.isDescendantOf(parent));
+ assert.ok(object.isDescendantOf(canvas));
+ object.group = undefined;
+ assert.ok(object.isDescendantOf(parent) === false);
+ assert.ok(object.isDescendantOf(canvas) === false);
+ object.canvas = canvas;
+ assert.ok(object.isDescendantOf(canvas));
+ assert.ok(object.isDescendantOf(object) === false);
+ });
+
+ QUnit.test('getAncestors', function (assert) {
+ var object = new fabric.Object();
+ var parent = new fabric.Object();
+ var other = new fabric.Object();
+ assert.ok(typeof object.getAncestors === 'function');
+ assert.deepEqual(object.getAncestors(), []);
+ object.group = parent;
+ assert.deepEqual(object.getAncestors(), [parent]);
+ parent.canvas = canvas;
+ assert.deepEqual(object.getAncestors(), [parent, canvas]);
+ parent.group = other;
+ assert.deepEqual(object.getAncestors(), [parent, other]);
+ other.canvas = canvas;
+ assert.deepEqual(object.getAncestors(), [parent, other, canvas]);
+ delete object.group;
+ assert.deepEqual(object.getAncestors(), []);
+ });
+
+ function prepareObjectsForTreeTesting() {
+ var TObject = fabric.util.createClass(fabric.Object, {
+ toJSON: function () {
+ return {
+ id: this.id,
+ objects: this._objects?.map(o => o.id),
+ parent: this.parent?.id,
+ canvas: this.canvas?.id
+ }
+ },
+ toString: function () {
+ return JSON.stringify(this.toJSON(), null, '\t');
+ }
+ });
+ var Collection = fabric.util.createClass(TObject, fabric.Collection, {
+ initialize: function ({ id }) {
+ this.id = id;
+ this._objects = [];
+ },
+ add: function () {
+ fabric.Collection.add.call(this, arguments, this._onObjectAdded);
+ },
+ insertAt: function (objects, index) {
+ fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded);
+ },
+ remove: function () {
+ fabric.Collection.remove.call(this, arguments, this._onObjectRemoved);
+ },
+ _onObjectAdded: function (object) {
+ object.group = this;
+ },
+ _onObjectRemoved: function (object) {
+ delete object.group;
+ },
+ removeAll: function () {
+ this.remove.apply(this, this._objects);
+ },
+ });
+ var canvas = fabric.util.object.extend(new Collection({ id: 'canvas' }), {
+ _onObjectAdded: function (object) {
+ object.canvas = this;
+ },
+ _onObjectRemoved: function (object) {
+ delete object.canvas;
+ },
+ });
+ return {
+ object: new TObject({ id: 'object' }),
+ other: new TObject({ id: 'other' }),
+ a: new Collection({ id: 'a' }),
+ b: new Collection({ id: 'b' }),
+ c: new Collection({ id: 'c' }),
+ canvas
+ }
+ }
+
+ QUnit.test('findCommonAncestors', function (assert) {
+ function findCommonAncestors(object, other, strict, expected, message) {
+ var common = object.findCommonAncestors(other, strict);
+ assert.deepEqual(
+ common.fork.map((obj) => obj.id),
+ expected.fork.map((obj) => obj.id),
+ message || `fork property should match check between '${object.id}' and '${other.id}'`
+ );
+ assert.deepEqual(
+ common.otherFork.map((obj) => obj.id),
+ expected.otherFork.map((obj) => obj.id),
+ message || `otherFork property should match check between '${object.id}' and '${other.id}'`
+ );
+ assert.deepEqual(
+ common.common.map((obj) => obj.id),
+ expected.common.map((obj) => obj.id),
+ message || `common property should match check between '${object.id}' and '${other.id}'`
+ );
+ var oppositeCommon = other.findCommonAncestors(object, strict);
+ assert.deepEqual(
+ oppositeCommon.fork.map((obj) => obj.id),
+ expected.otherFork.map((obj) => obj.id),
+ message || `fork property should match opposite check between '${other.id}' and '${object.id}'`
+ );
+ assert.deepEqual(
+ oppositeCommon.otherFork.map((obj) => obj.id),
+ expected.fork.map((obj) => obj.id),
+ message || `otherFork property should match opposite check between '${other.id}' and '${object.id}'`
+ );
+ assert.deepEqual(
+ oppositeCommon.common.map((obj) => obj.id),
+ expected.common.map((obj) => obj.id),
+ message || `common property should match opposite check between '${other.id}' and '${object.id}'`
+ );
+ }
+ var { object, other, a, b, c, canvas } = prepareObjectsForTreeTesting();
+ assert.ok(typeof object.findCommonAncestors === 'function');
+ assert.ok(Array.isArray(a._objects));
+ assert.ok(a._objects !== b._objects);
+ // same object
+ findCommonAncestors(object, object, false, { fork: [], otherFork: [] , common: [object] });
+ // foreign objects
+ findCommonAncestors(object, other, false, { fork: [object], otherFork: [other] , common: [] });
+ // same level
+ a.add(object, other);
+ findCommonAncestors(object, other, false, { fork: [object], otherFork: [other], common: [a] });
+ findCommonAncestors(object, a, false, { fork: [object], otherFork: [], common: [a] });
+ findCommonAncestors(other, a, false, { fork: [other], otherFork: [], common: [a] });
+ findCommonAncestors(a, object, false, { fork: [], otherFork: [object], common: [a] });
+ findCommonAncestors(a, object, true, { fork: [], otherFork: [object], common: [a] }, 'strict option should have no effect when outside canvas');
+ // different level
+ a.remove(object);
+ b.add(object);
+ a.add(b);
+ findCommonAncestors(object, b, false, { fork: [object], otherFork: [], common: [b, a] });
+ findCommonAncestors(b, a, false, { fork: [b], otherFork: [], common: [a] });
+ findCommonAncestors(object, other, false, { fork: [object, b], otherFork: [other], common: [a] });
+ // with common ancestor
+ assert.equal(c.size(), 0, 'c should be empty');
+ c.add(a);
+ assert.equal(c.size(), 1, 'c should contain a');
+ findCommonAncestors(object, b, false, { fork: [object], otherFork: [], common: [b, a, c] });
+ findCommonAncestors(b, a, false, { fork: [b], otherFork: [], common: [a, c] });
+ findCommonAncestors(object, other, false, { fork: [object, b], otherFork: [other], common: [a, c] });
+ findCommonAncestors(object, c, false, { fork: [object, b, a], otherFork: [], common: [c] });
+ findCommonAncestors(other, c, false, { fork: [other, a], otherFork: [], common: [c] });
+ findCommonAncestors(b, c, false, { fork: [b, a], otherFork: [], common: [c] });
+ findCommonAncestors(a, c, false, { fork: [a], otherFork: [], common: [c] });
+ // deeper asymmetrical
+ c.removeAll();
+ assert.equal(c.size(), 0, 'c should be cleared');
+ a.remove(other);
+ c.add(other, a);
+ findCommonAncestors(object, b, false, { fork: [object], otherFork: [], common: [b, a, c] });
+ findCommonAncestors(b, a, false, { fork: [b], otherFork: [], common: [a, c] });
+ findCommonAncestors(a, other, false, { fork: [a], otherFork: [other], common: [c] });
+ findCommonAncestors(object, other, false, { fork: [object, b, a], otherFork: [other], common: [c] });
+ findCommonAncestors(object, c, false, { fork: [object, b, a], otherFork: [], common: [c] });
+ findCommonAncestors(other, c, false, { fork: [other], otherFork: [], common: [c] });
+ findCommonAncestors(b, c, false, { fork: [b, a], otherFork: [], common: [c] });
+ findCommonAncestors(a, c, false, { fork: [a], otherFork: [], common: [c] });
+ // with canvas
+ a.removeAll();
+ b.removeAll();
+ c.removeAll();
+ canvas.add(object, other);
+ findCommonAncestors(object, other, true, { fork: [object], otherFork: [other], common: [] });
+ findCommonAncestors(object, other, false, { fork: [object], otherFork: [other], common: [canvas] });
+ findCommonAncestors(object, canvas, true, { fork: [object], otherFork: [canvas], common: [] });
+ findCommonAncestors(object, canvas, false, { fork: [object], otherFork: [], common: [canvas] });
+ findCommonAncestors(other, canvas, false, { fork: [other], otherFork: [], common: [canvas] });
+ // parent precedes canvas when checking ancestor
+ a.add(object);
+ assert.ok(object.canvas === canvas, 'object should have canvas set');
+ findCommonAncestors(object, other, true, { fork: [object, a], otherFork: [other], common: [] });
+ findCommonAncestors(object, other, false, { fork: [object, a], otherFork: [other, canvas], common: [] });
+ canvas.insertAt(a, 0);
+ findCommonAncestors(object, other, true, { fork: [object, a], otherFork: [other], common: [] });
+ findCommonAncestors(object, other, false, { fork: [object, a], otherFork: [other], common: [canvas] });
+ findCommonAncestors(a, other, false, { fork: [a], otherFork: [other], common: [canvas] });
+ findCommonAncestors(a, canvas, false, { fork: [a], otherFork: [], common: [canvas] });
+ findCommonAncestors(object, canvas, false, { fork: [object, a], otherFork: [], common: [canvas] });
+ findCommonAncestors(other, canvas, false, { fork: [other], otherFork: [], common: [canvas] });
+ });
+
+ QUnit.assert.isInFrontOf = function (object, other, expected) {
+ var actual = object.isInFrontOf(other);
+ this.pushResult({
+ expected: expected,
+ actual: actual,
+ result: actual === expected,
+ message: `'${expected ? object.id : other.id}' should be in front of '${expected ? other.id : object.id}'`
+ });
+ if (actual === expected && typeof expected === 'boolean') {
+ var actual2 = other.isInFrontOf(object);
+ this.pushResult({
+ expected: !expected,
+ actual: actual2,
+ result: actual2 === !expected,
+ message: `should match opposite check between '${object.id}' and '${other.id}'`
+ });
+ }
+ };
+
+ QUnit.test('isInFrontOf', function (assert) {
+ var { object, other, a, b, c, canvas } = prepareObjectsForTreeTesting();
+ assert.ok(typeof object.isInFrontOf === 'function');
+ assert.ok(Array.isArray(a._objects));
+ assert.ok(a._objects !== b._objects);
+ // same object
+ assert.isInFrontOf(object, object, undefined);
+ // foreign objects
+ assert.isInFrontOf(object, other, undefined);
+ // same level
+ a.add(object, other);
+ assert.isInFrontOf(object, other, false);
+ assert.isInFrontOf(object, a, true);
+ assert.isInFrontOf(other, a, true);
+ // different level
+ a.remove(object);
+ b.add(object);
+ a.add(b);
+ assert.isInFrontOf(object, b, true);
+ assert.isInFrontOf(b, a, true);
+ assert.isInFrontOf(object, other, true);
+ // with common ancestor
+ assert.equal(c.size(), 0, 'c should be empty');
+ c.add(a);
+ assert.equal(c.size(), 1, 'c should contain a');
+ assert.isInFrontOf(object, b, true);
+ assert.isInFrontOf(b, a, true);
+ assert.isInFrontOf(object, other, true);
+ assert.isInFrontOf(object, c, true);
+ assert.isInFrontOf(other, c, true);
+ assert.isInFrontOf(b, c, true);
+ assert.isInFrontOf(a, c, true);
+ // deeper asymmetrical
+ c.removeAll();
+ assert.equal(c.size(), 0, 'c should be cleared');
+ a.remove(other);
+ c.add(other, a);
+ assert.isInFrontOf(object, b, true);
+ assert.isInFrontOf(b, a, true);
+ assert.isInFrontOf(a, other, true);
+ assert.isInFrontOf(object, other, true);
+ assert.isInFrontOf(object, c, true);
+ assert.isInFrontOf(other, c, true);
+ assert.isInFrontOf(b, c, true);
+ assert.isInFrontOf(a, c, true);
+ // with canvas
+ a.removeAll();
+ b.removeAll();
+ c.removeAll();
+ canvas.add(object, other);
+ assert.isInFrontOf(object, other, false);
+ assert.isInFrontOf(object, canvas, true);
+ assert.isInFrontOf(other, canvas, true);
+ // parent precedes canvas when checking ancestor
+ a.add(object);
+ assert.ok(object.canvas === canvas, 'object should have canvas set');
+ assert.isInFrontOf(object, other, undefined);
+ canvas.insertAt(a, 0);
+ assert.isInFrontOf(object, other, false);
+ assert.isInFrontOf(a, other, false);
+ assert.isInFrontOf(a, canvas, true);
+ assert.isInFrontOf(object, canvas, true);
+ assert.isInFrontOf(other, canvas, true);
+ });
+
QUnit.test('getTotalObjectScaling with zoom', function(assert) {
var object = new fabric.Object({ scaleX: 3, scaleY: 2});
canvas.setZoom(3);
diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js
index ebdae56cf34..94db9052bc0 100644
--- a/test/visual/group_layout.js
+++ b/test/visual/group_layout.js
@@ -76,6 +76,27 @@
height: 300
});
+ function fitContentLayoutRelative(canvas, callback) {
+ var g = createGroupForLayoutTests('fit-content layout', {
+ backgroundColor: 'blue'
+ });
+ g.clone().then((clone) => {
+ canvas.add(clone);
+ canvas.renderAll();
+ callback(canvas.lowerCanvasEl);
+ })
+ }
+
+ tests.push({
+ test: 'fit-content layout',
+ code: fitContentLayoutRelative,
+ golden: 'group-layout/fit-content.png',
+ newModule: 'Group Layout',
+ percentage: 0.06,
+ width: 400,
+ height: 300
+ });
+
function fitContentReLayout(canvas, callback) {
var g = createGroupForLayoutTests('fit-content layout', {
backgroundColor: 'blue'
@@ -334,5 +355,5 @@
});
}
*/
- // tests.forEach(visualTestLoop(QUnit));
+ tests.forEach(visualTestLoop(QUnit));
})();