Implement #hitTestAll() on Item and Project

Along with unit tests and documentation.
Closes #536
This commit is contained in:
Jürg Lehni 2016-02-14 12:39:35 +01:00
parent 3ee46ffc5c
commit 4a947317fb
9 changed files with 371 additions and 172 deletions

View file

@ -106,7 +106,8 @@ var HitResult = Base.extend(/** @lends HitResult# */{
*
* @private
*/
getOptions: function(options) {
getOptions: function(args) {
var options = args && Base.read(args);
return Base.set({
// Type of item, for instanceof check: Group, Layer, Path,
// CompoundPath, Shape, Raster, SymbolItem, ...

View file

@ -70,7 +70,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
data: {}
}
},
new function() { // // Scope to inject various item event handlers
new function() { // Injection scope for various item event handlers
var handlers = ['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick',
'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'];
return Base.each(handlers,
@ -1628,6 +1628,8 @@ new function() { // // Scope to inject various item event handlers
},
/**
* {@grouptitle Geometric Tests}
*
* Checks whether the item's geometry contains the given point.
*
* @example {@paperscript} // Click within and outside the star below
@ -1710,20 +1712,79 @@ new function() { // // Scope to inject various item event handlers
// found, because all we care for here is there are some or none:
return this._asPathItem().getIntersections(item._asPathItem(), null,
_matrix, true).length > 0;
},
}
},
new function() { // Injection scope for hit-test functions shared with project
function hitTest(/* point, options */) {
return this._hitTest(
Point.read(arguments),
HitResult.getOptions(arguments));
}
function hitTestAll(/* point, options */) {
var point = Point.read(arguments),
options = HitResult.getOptions(arguments),
callback = options.match,
results = [];
options = Base.set({}, options, {
match: function(hit) {
if (!callback || callback(hit))
results.push(hit);
}
});
this._hitTest(point, options);
return results;
}
function hitTestChildren(point, options, _exclude) {
// NOTE: _exclude is only used in Group#_hitTestChildren()
var children = this._children;
if (children) {
// Loop backwards, so items that get drawn last are tested first.
for (var i = children.length - 1; i >= 0; i--) {
var child = children[i];
var res = child !== _exclude && child._hitTest(point, options);
if (res)
return res;
}
}
return null;
}
Project.inject({
hitTest: hitTest,
hitTestAll: hitTestAll,
_hitTest: hitTestChildren
});
return {
// NOTE: Documentation is in the scope that follows.
hitTest: hitTest,
hitTestAll: hitTestAll,
_hitTestChildren: hitTestChildren,
};
}, /** @lends Item# */{
/**
* Perform a hit-test on the item (and its children, if it is a {@link
* Group} or {@link Layer}) at the location of the specified point.
* {@grouptitle Hit-testing, Fetching and Matching Items}
*
* The options object allows you to control the specifics of the hit-test
* and may contain a combination of the following values:
* Performs a hit-test on the item and its children (if it is a {@link
* Group} or {@link Layer}) at the location of the specified point,
* returning the first found hit.
*
* The options object allows you to control the specifics of the hit-
* test and may contain a combination of the following values:
*
* @name Item#hitTest
* @function
*
* @option [options.tolerance={@link PaperScope#settings}.hitTolerance]
* {Number} the tolerance of the hit-test
* @option options.class {Function} only hit-test again a certain item class
* and its sub-classes: {@values Group, Layer, Path, CompoundPath,
* Shape, Raster, SymbolItem, PointText, ...}
* @option options.class {Function} only hit-test again a certain item
* class and its sub-classes: {@values Group, Layer, Path,
* CompoundPath, Shape, Raster, SymbolItem, PointText, ...}
* @option options.match {Function} a match function to be called for each
* found hit result: Return `true` to return the result, `false` to keep
* searching
* @option [options.fill=true] {Boolean} hit-test the fill of items
* @option [options.stroke=true] {Boolean} hit-test the stroke of path
* items, taking into account the setting of stroke color and width
@ -1746,19 +1807,33 @@ new function() { // // Scope to inject various item event handlers
* @param {Point} point the point where the hit-test should be performed
* @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }]
* @return {HitResult} a hit result object that contains more information
* about what exactly was hit or `null` if nothing was hit
* @return {HitResult} a hit result object describing what exactly was hit
* or `null` if nothing was hit
*/
/**
* Performs a hit-test on the item and its children (if it is a {@link
* Group} or {@link Layer}) at the location of the specified point,
* returning all found hits.
*
* The options object allows you to control the specifics of the hit-
* test. See {@link #hitTest(point[, options])} for a list of all options.
*
* @name Item#hitTestAll
* @function
* @param {Point} point the point where the hit-test should be performed
* @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }]
* @return {HitResult[]} hit result objects for all hits, describing what
* exactly was hit or `null` if nothing was hit
* @see #hitTest(point[, options]);
*/
hitTest: function(/* point, options */) {
return this._hitTest(
Point.read(arguments),
HitResult.getOptions(Base.read(arguments)));
},
_hitTest: function(point, options) {
if (this._locked || !this._visible || this._guide && !options.guides
|| this.isEmpty())
|| this.isEmpty()) {
return null;
}
// Check if the point is withing roughBounds + tolerance, but only if
// this item does not have children, since we'd have to travel up the
@ -1783,26 +1858,35 @@ new function() { // // Scope to inject various item event handlers
point = matrix._inverseTransform(point);
// If the matrix is non-reversible, point will now be `null`:
if (!point || !this._children &&
!this.getBounds({ internal: true, stroke: true, handle: true })
.expand(tolerancePadding.multiply(2))._containsPoint(point))
!this.getBounds({ internal: true, stroke: true, handle: true })
.expand(tolerancePadding.multiply(2))._containsPoint(point)) {
return null;
// Filter for type, guides and selected items if that's required.
}
// See if we should check self (own content), by filtering for type,
// guides and selected items if that's required.
var checkSelf = !(options.guides && !this._guide
|| options.selected && !this._selected
// Support legacy Item#type property to match hyphenated
// class-names.
|| options.type && options.type !== Base.hyphenate(this._class)
|| options.class && !(this instanceof options.class)),
callback = options.match,
that = this,
res;
function match(hit) {
return !callback || hit && callback(hit) ? hit : null;
}
function checkBounds(type, part) {
var pt = bounds['get' + part]();
// Since there are transformations, we cannot simply use a numerical
// tolerance value. Instead, we divide by a padding size, see above.
if (point.subtract(pt).divide(tolerancePadding).length <= 1)
if (point.subtract(pt).divide(tolerancePadding).length <= 1) {
return new HitResult(type, that,
{ name: Base.hyphenate(part), point: pt });
}
}
// Ignore top level layers by checking for _parent:
@ -1810,47 +1894,39 @@ new function() { // // Scope to inject various item event handlers
// Don't get the transformed bounds, check against transformed
// points instead
var bounds = this.getInternalBounds();
if (options.center)
if (options.center) {
res = checkBounds('center', 'Center');
}
if (!res && options.bounds) {
// TODO: Move these into a private scope
var points = [
'TopLeft', 'TopRight', 'BottomLeft', 'BottomRight',
'LeftCenter', 'TopCenter', 'RightCenter', 'BottomCenter'
];
for (var i = 0; i < 8 && !res; i++)
for (var i = 0; i < 8 && !res; i++) {
res = checkBounds('bounds', points[i]);
}
}
res = match(res);
}
if (!res) {
options._viewMatrix = viewMatrix;
options._strokeMatrix = strokeMatrix;
res = this._hitTestChildren(point, options)
|| checkSelf && this._hitTestSelf(point, options)
|| null;
// NOTE: We don't call callback on _hitTestChildren()
// because that's already called internally.
|| checkSelf
&& match(this._hitTestSelf(point, options, strokeMatrix))
|| null;
// Restore viewMatrix for next child, so appended matrix chains are
// calculated correctly.
options._viewMatrix = parentViewMatrix;
}
// Transform the point back to the outer coordinate system.
if (res && res.point)
if (res && res.point) {
res.point = matrix.transform(res.point);
return res;
},
_hitTestChildren: function(point, options, _exclude) {
// NOTE: _exclude is only used in Group#_hitTestChildren()
var children = this._children;
if (children) {
// Loop backwards, so items that get drawn last are tested first
for (var i = children.length - 1; i >= 0; i--) {
var child = children[i];
var res = child !== _exclude && child._hitTest(point, options);
if (res)
return res;
}
}
return res;
},
_hitTestSelf: function(point, options) {
@ -1860,21 +1936,19 @@ new function() { // // Scope to inject various item event handlers
},
/**
* {@grouptitle Fetching and matching items}
*
* Checks whether the item matches the criteria described by the given
* object, by iterating over all of its properties and matching against
* their values through {@link #matches(name, compare)}.
*
* See {@link Project#getItems(match)} for a selection of illustrated
* See {@link Project#getItems(options)} for a selection of illustrated
* examples.
*
* @name Item#matches
* @function
*
* @param {Object|Function} match the criteria to match against
* @param {Object|Function} options the criteria to match against
* @return {Boolean} {@true if the item matches all the criteria}
* @see #getItems(match)
* @see #getItems(options)
*/
/**
* Checks whether the item matches the given criteria. Extended matching is
@ -1884,7 +1958,7 @@ new function() { // // Scope to inject various item event handlers
* points with that x-value). Partial matching does work for
* {@link Item#data}.
*
* See {@link Project#getItems(match)} for a selection of illustrated
* See {@link Project#getItems(options)} for a selection of illustrated
* examples.
*
* @name Item#matches
@ -1894,7 +1968,7 @@ new function() { // // Scope to inject various item event handlers
* @param {Object} compare the value, function or regular expression to
* compare against
* @return {Boolean} {@true if the item matches the state}
* @see #getItems(match)
* @see #getItems(options)
*/
matches: function(name, compare) {
// matchObject() is used to match against objects in a nested manner.
@ -1965,31 +2039,31 @@ new function() { // // Scope to inject various item event handlers
* that x-value). Partial matching does work for {@link Item#data}.
*
* Matching items against a rectangular area is also possible, by setting
* either `match.inside` or `match.overlapping` to a rectangle describing
* either `options.inside` or `options.overlapping` to a rectangle describing
* the area in which the items either have to be fully or partly contained.
*
* See {@link Project#getItems(match)} for a selection of illustrated
* See {@link Project#getItems(options)} for a selection of illustrated
* examples.
*
* @option [match.recursive=true] {Boolean} whether to loop recursively
* @option [options.recursive=true] {Boolean} whether to loop recursively
* through all children, or stop at the current level
* @option match.match {Function} a match function to be called for each
* @option options.match {Function} a match function to be called for each
* item, allowing the definition of more flexible item checks that are
* not bound to properties. If no other match properties are defined,
* this function can also be passed instead of the `match` object
* @option match.class {Function} the constructor function of the item type
* this function can also be passed instead of the `options` object
* @option options.class {Function} the constructor function of the item type
* to match against
* @option match.inside {Rectangle} the rectangle in which the items need to
* @option options.inside {Rectangle} the rectangle in which the items need to
* be fully contained
* @option match.overlapping {Rectangle} the rectangle with which the items
* @option options.overlapping {Rectangle} the rectangle with which the items
* need to at least partly overlap
*
* @param {Object|Function} match the criteria to match against
* @param {Object|Function} options the criteria to match against
* @return {Item[]} the list of matching descendant items
* @see #matches(match)
* @see #matches(options)
*/
getItems: function(match) {
return Item._getItems(this, match, this._matrix);
getItems: function(options) {
return Item._getItems(this, options, this._matrix);
},
/**
@ -2007,19 +2081,20 @@ new function() { // // Scope to inject various item event handlers
* @return {Item} the first descendant item matching the given criteria
* @see #getItems(match)
*/
getItem: function(match) {
return Item._getItems(this, match, this._matrix, null, true)[0] || null;
getItem: function(options) {
return Item._getItems(this, options, this._matrix, null, true)[0]
|| null;
},
statics: {
// NOTE: We pass children instead of item as first argument so the
// method can be used for Project#layers as well in Project.
_getItems: function _getItems(item, match, matrix, param, firstOnly) {
_getItems: function _getItems(item, options, matrix, param, firstOnly) {
if (!param) {
// Set up a couple of "side-car" values for the recursive calls
// of _getItems below, mainly related to the handling of
// inside / overlapping:
var obj = typeof match === 'object' && match,
var obj = typeof options === 'object' && options,
overlapping = obj && obj.overlapping,
inside = obj && obj.inside,
// If overlapping is set, we also perform the inside check:
@ -2037,9 +2112,9 @@ new function() { // // Scope to inject various item event handlers
})
};
if (obj) {
// Create a copy of the match object that doesn't contain
// Create a copy of the options object that doesn't contain
// these special properties:
match = Base.filter({}, match, {
options = Base.filter({}, options, {
recursive: true, inside: true, overlapping: true
});
}
@ -2067,13 +2142,13 @@ new function() { // // Scope to inject various item event handlers
|| param.path.intersects(child, childMatrix))))
add = false;
}
if (add && child.matches(match)) {
if (add && child.matches(options)) {
items.push(child);
if (firstOnly)
break;
}
if (param.recursive !== false) {
_getItems(child, match, childMatrix, param, firstOnly);
_getItems(child, options, childMatrix, param, firstOnly);
}
if (firstOnly && items.length > 0)
break;

View file

@ -329,57 +329,6 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
selectedItems[i].setFullySelected(false);
},
/**
* Perform a hit-test on the items contained within the project at the
* location of the specified point.
*
* The options object allows you to control the specifics of the hit-test
* and may contain a combination of the following values:
*
* @option [options.tolerance={@link PaperScope#settings}.hitTolerance]
* {Number} the tolerance of the hit-test
* @option options.class {Function} only hit-test again a certain item class
* and its sub-classes: {@values Group, Layer, Path, CompoundPath,
* Shape, Raster, SymbolItem, PointText, ...}
* @option [options.fill=true] {Boolean} hit-test the fill of items
* @option [options.stroke=true] {Boolean} hit-test the stroke of path
* items, taking into account the setting of stroke color and width
* @option [options.segments=true] {Boolean} hit-test for {@link
* Segment#point} of {@link Path} items
* @option options.curves {Boolean} hit-test the curves of path items,
* without taking the stroke color or width into account
* @option options.handles {Boolean} hit-test for the handles ({@link
* Segment#handleIn} / {@link Segment#handleOut}) of path segments.
* @option options.ends {Boolean} only hit-test for the first or last
* segment points of open path items
* @option options.bounds {Boolean} hit-test the corners and side-centers of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.center {Boolean} hit-test the {@link Rectangle#center} of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.guides {Boolean} hit-test items that have {@link
* Item#guide} set to `true`
* @option options.selected {Boolean} only hit selected items
*
* @param {Point} point the point where the hit-test should be performed
* @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }]
* @return {HitResult} a hit result object that contains more information
* about what exactly was hit or `null` if nothing was hit
*/
hitTest: function(/* point, options */) {
// We don't need to do this here, but it speeds up things since we won't
// repeatedly convert in Item#hitTest() then.
var point = Point.read(arguments),
options = HitResult.getOptions(Base.read(arguments)),
children = this._children;
// Loop backwards, so layers that get drawn last are tested first
for (var i = children.length - 1; i >= 0; i--) {
var res = children[i]._hitTest(point, options);
if (res) return res;
}
return null;
},
/**
* {@grouptitle Hierarchy Operations}
*
@ -436,7 +385,72 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
},
/**
* {@grouptitle Fetching and matching items}
* {@grouptitle Hit-testing, Fetching and Matching Items}
*
* Performs a hit-test on the items contained within the project at the
* location of the specified point.
*
* The options object allows you to control the specifics of the hit-test
* and may contain a combination of the following values:
*
* @name Project#hitTest
* @function
*
* @option [options.tolerance={@link PaperScope#settings}.hitTolerance]
* {Number} the tolerance of the hit-test
* @option options.class {Function} only hit-test again a certain item
* class and its sub-classes: {@values Group, Layer, Path,
* CompoundPath, Shape, Raster, SymbolItem, PointText, ...}
* @option options.match {Function} a match function to be called for each
* found hit result: Return `true` to return the result, `false` to keep
* searching
* @option [options.fill=true] {Boolean} hit-test the fill of items
* @option [options.stroke=true] {Boolean} hit-test the stroke of path
* items, taking into account the setting of stroke color and width
* @option [options.segments=true] {Boolean} hit-test for {@link
* Segment#point} of {@link Path} items
* @option options.curves {Boolean} hit-test the curves of path items,
* without taking the stroke color or width into account
* @option options.handles {Boolean} hit-test for the handles ({@link
* Segment#handleIn} / {@link Segment#handleOut}) of path segments.
* @option options.ends {Boolean} only hit-test for the first or last
* segment points of open path items
* @option options.bounds {Boolean} hit-test the corners and side-centers of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.center {Boolean} hit-test the {@link Rectangle#center} of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.guides {Boolean} hit-test items that have {@link
* Item#guide} set to `true`
* @option options.selected {Boolean} only hit selected items
*
* @param {Point} point the point where the hit-test should be performed
* @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }]
* @return {HitResult} a hit result object that contains more information
* about what exactly was hit or `null` if nothing was hit
*/
// NOTE: Implementation is in Item#hitTest()
/**
* Performs a hit-test on the item and its children (if it is a {@link
* Group} or {@link Layer}) at the location of the specified point,
* returning all found hits.
*
* The options object allows you to control the specifics of the hit-
* test. See {@link #hitTest(point[, options])} for a list of all options.
*
* @name Item#hitTestAll
* @function
* @param {Point} point the point where the hit-test should be performed
* @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }]
* @return {HitResult[]} hit result objects for all hits, describing what
* exactly was hit or `null` if nothing was hit
* @see #hitTest(point[, options]);
*/
// NOTE: Implementation is in Item#hitTestAll()
/**
*
* Fetch items contained within the project whose properties match the
* criteria in the specified object.
@ -448,21 +462,27 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* matching does work for {@link Item#data}.
*
* Matching items against a rectangular area is also possible, by setting
* either `match.inside` or `match.overlapping` to a rectangle describing
* the area in which the items either have to be fully or partly contained.
* either `options.inside` or `options.overlapping` to a rectangle
* describing the area in which the items either have to be fully or partly
* contained.
*
* @option [match.recursive=true] {Boolean} whether to loop recursively
* @option [options.recursive=true] {Boolean} whether to loop recursively
* through all children, or stop at the current level
* @option match.match {Function} a match function to be called for each
* @option options.match {Function} a match function to be called for each
* item, allowing the definition of more flexible item checks that are
* not bound to properties. If no other match properties are defined,
* this function can also be passed instead of the `match` object
* @option match.class {Function} the constructor function of the item type
* to match against
* @option match.inside {Rectangle} the rectangle in which the items need to
* be fully contained
* @option match.overlapping {Rectangle} the rectangle with which the items
* need to at least partly overlap
* @option options.class {Function} the constructor function of the item
* type to match against
* @option options.inside {Rectangle} the rectangle in which the items need
* to be fully contained
* @option options.overlapping {Rectangle} the rectangle with which the
* items need to at least partly overlap
*
* @see Item#matches(options)
* @see Item#getItems(options)
* @param {Object|Function} options the criteria to match against
* @return {Item[]} the list of matching items contained in the project
*
* @example {@paperscript} // Fetch all selected path items:
* var path1 = new Path.Circle({
@ -666,14 +686,9 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* for (var i = 0; i < items.length; i++) {
* items[i].fillColor = 'red';
* }
*
* @see Item#matches(match)
* @see Item#getItems(match)
* @param {Object|Function} match the criteria to match against
* @return {Item[]} the list of matching items contained in the project
*/
getItems: function(match) {
return Item._getItems(this, match);
getItems: function(options) {
return Item._getItems(this, options);
},
/**
@ -684,13 +699,14 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* of the full object, not partial matching (e.g. only providing the x-
* coordinate to match all points with that x-value). Partial matching
* does work for {@link Item#data}.
* See {@link #getItems(match)} for a selection of illustrated examples.
*
* @param {Object|Function} match the criteria to match against
* See {@link #getItems(options)} for a selection of illustrated examples.
*
* @param {Object|Function} options the criteria to match against
* @return {Item} the first item in the project matching the given criteria
*/
getItem: function(match) {
return Item._getItems(this, match, null, null, true)[0] || null;
getItem: function(options) {
return Item._getItems(this, options, null, null, true)[0] || null;
},
/**

View file

@ -341,7 +341,7 @@ new function() { // Scope for _contains() and _hitTestSelf() code.
}
},
_hitTestSelf: function _hitTestSelf(point, options) {
_hitTestSelf: function _hitTestSelf(point, options, strokeMatrix) {
var hit = false,
style = this._style;
if (options.stroke && style.hasStroke()) {
@ -350,8 +350,7 @@ new function() { // Scope for _contains() and _hitTestSelf() code.
strokeWidth = style.getStrokeWidth(),
strokePadding = options._tolerancePadding.add(
Path._getStrokePadding(strokeWidth / 2,
!style.getStrokeScaling() &&
options._strokeMatrix));
!style.getStrokeScaling() && strokeMatrix));
if (type === 'rectangle') {
var padding = strokePadding.multiply(2),
center = getCornerCenter(this, point, padding);

View file

@ -121,8 +121,8 @@ var SymbolItem = Item.extend(/** @lends SymbolItem# */{
options);
},
_hitTestSelf: function(point, options) {
var res = this._definition._item._hitTest(point, options);
_hitTestSelf: function(point, options, strokeMatrix) {
var res = this._definition._item._hitTest(point, options, strokeMatrix);
// TODO: When the symbol's definition is a path, should hitResult
// contain information like HitResult#curve?
if (res)

View file

@ -1250,7 +1250,7 @@ statics: /** @lends Curve */{
* @return {Number} the curvature of the curve at the given location
*/
},
new function() { // // Scope to inject various curve evaluation methods
new function() { // Injection scope for various curve evaluation methods
var methods = ['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent',
'getWeightedNormal', 'getCurvature'];
return Base.each(methods,

View file

@ -1548,7 +1548,7 @@ var Path = PathItem.extend(/** @lends Path# */{
toPath: '#clone',
_hitTestSelf: function(point, options) {
_hitTestSelf: function(point, options, strokeMatrix) {
var that = this,
style = this.getStyle(),
segments = this._segments,
@ -1579,7 +1579,7 @@ var Path = PathItem.extend(/** @lends Path# */{
// #strokeScaling into account through _getStrokeMatrix().
strokePadding = tolerancePadding.add(
Path._getStrokePadding(strokeRadius,
!style.getStrokeScaling() && options._strokeMatrix));
!style.getStrokeScaling() && strokeMatrix));
} else {
join = cap = 'round';
}

View file

@ -132,7 +132,8 @@ QUnit.jsDump.setParser('object', function (obj, stack) {
var compareProperties = function(actual, expected, properties, message, options) {
for (var i = 0, l = properties.length; i < l; i++) {
var key = properties[i];
equals(actual[key], expected[key], message + '.' + key, options);
equals(actual[key], expected[key],
message + ' (#' + key + ')', options);
}
};
@ -234,14 +235,14 @@ var compareItem = function(actual, expected, message, options, properties) {
} else {
if (options.cloned)
QUnit.notStrictEqual(actual.id, expected.id,
'not ' + message + '.id');
message + ' (not #id)');
QUnit.strictEqual(actual.constructor, expected.constructor,
message + '.constructor');
message + ' (#constructor)');
// When item is cloned and has a name, the name will be versioned:
equals(actual.name,
options.cloned && expected.name
? expected.name + ' 1' : expected.name,
message + '.name');
message + ' (#name)');
compareProperties(actual, expected, ['children', 'bounds', 'position',
'matrix', 'data', 'opacity', 'locked', 'visible', 'blendMode',
'selected', 'fullySelected', 'clipMask', 'guide'],
@ -255,7 +256,7 @@ var compareItem = function(actual, expected, message, options, properties) {
if (expected instanceof TextItem)
styles.push('fontSize', 'font', 'leading', 'justification');
compareProperties(actual.style, expected.style, styles,
message + '.style', options);
message + ' (#style)', options);
}
};
@ -275,19 +276,19 @@ var comparators = {
// expected element, and compare values even if they may be inherited.
// This is to handle styling values on SVGElement items more flexibly.
equals(actual && actual.tagName, expected.tagName,
(message || '') + '.tagName', options);
(message || '') + ' (#tagName)', options);
for (var i = 0; i < expected.attributes.length; i++) {
var attr = expected.attributes[i];
if (attr.specified) {
equals(actual && actual.getAttribute(attr.name), attr.value,
(message || '') + '.' + attr.name, options);
(message || '') + ' (#' + attr.name + ')', options);
}
}
for (var i = 0; i < actual && actual.attributes.length; i++) {
var attr = actual.attributes[i];
if (attr.specified) {
equals(attr.value, expected.getAttribute(attr.name)
(message || '') + '.' + attr.name, options);
(message || '') + ' #(' + attr.name + ')', options);
}
}
},
@ -304,7 +305,8 @@ var comparators = {
},
Array: function(actual, expected, message, options) {
QUnit.strictEqual(actual.length, expected.length, message + '.length');
QUnit.strictEqual(actual.length, expected.length, message
+ ' (#length)');
for (var i = 0, l = actual.length; i < l; i++) {
equals(actual[i], expected[i], (message || '') + '[' + i + ']',
options);
@ -312,15 +314,15 @@ var comparators = {
},
Point: function(actual, expected, message, options) {
comparators.Number(actual.x, expected.x, message + '.x', options);
comparators.Number(actual.y, expected.y, message + '.y', options);
comparators.Number(actual.x, expected.x, message + ' (#x)', options);
comparators.Number(actual.y, expected.y, message + ' (#y)', options);
},
Size: function(actual, expected, message, options) {
comparators.Number(actual.width, expected.width, message + '.width',
options);
comparators.Number(actual.height, expected.height, message + '.height',
options);
comparators.Number(actual.width, expected.width,
message + ' (#width)', options);
comparators.Number(actual.height, expected.height,
message + ' (#height)', options);
},
Rectangle: function(actual, expected, message, options) {
@ -334,10 +336,10 @@ var comparators = {
Color: function(actual, expected, message, options) {
if (actual && expected) {
equals(actual.type, expected.type, message + '.type', options);
equals(actual.type, expected.type, message + ' (#type)', options);
// NOTE: This also compares gradients, with identity checks and all.
equals(actual.components, expected.components,
message + '.components', options);
message + ' (#components)', options);
} else {
QUnit.strictEqual(actual, expected, message);
}
@ -351,7 +353,7 @@ var comparators = {
SegmentPoint: function(actual, expected, message, options) {
comparators.Point(actual, expected, message, options);
comparators.Boolean(actual.selected, expected.selected,
message + '.selected', options);
message + ' (#selected)', options);
},
Item: compareItem,
@ -367,7 +369,7 @@ var comparators = {
QUnit.push(sharedProject ? sameProject : !sameProject,
actual.project,
sharedProject ? expected.project : 'not ' + expected.project,
message + '.project');
message + ' (#project)');
},
Path: function(actual, expected, message, options) {
@ -389,7 +391,7 @@ var comparators = {
comparePixels(actual, expected, message, options);
} else {
equals(actual.toDataURL(), expected.toDataURL(),
message + '.toDataUrl()');
message + ' (#toDataUrl())');
}
},
@ -414,8 +416,8 @@ var comparators = {
},
SymbolDefinition: function(actual, expected, message, options) {
equals(actual.definition, expected.definition, message + '.definition',
options);
equals(actual.definition, expected.definition,
message + ' (#definition)', options);
},
Project: function(actual, expected, message, options) {

View file

@ -727,5 +727,111 @@ test('hit-testing clipped items', function() {
}, true);
});
test('hit-testing with a match function', function() {
var point = new Point(100, 100),
red = new Color('red'),
green = new Color('green'),
blue = new Color('blue');
var c1 = new Path.Circle({
center: point,
radius: 50,
fillColor: red
});
var c2 = new Path.Circle({
center: point,
radius: 50,
fillColor: green
});
var c3 = new Path.Circle({
center: point,
radius: 50,
fillColor: blue
});
equals(function() {
var result = paper.project.hitTest(point, {
fill: true,
match: function(res) {
return res.item.fillColor == red;
}
});
return result && result.item === c1;
}, true);
equals(function() {
var result = paper.project.hitTest(point, {
fill: true,
match: function(res) {
return res.item.fillColor == green;
}
});
return result && result.item === c2;
}, true);
equals(function() {
var result = paper.project.hitTest(point, {
fill: true,
match: function(res) {
return res.item.fillColor == blue;
}
});
return result && result.item === c3;
}, true);
});
test('hit-testing for all items', function() {
var c1 = new Path.Circle({
center: [100, 100],
radius: 40,
fillColor: 'red'
});
var c2 = new Path.Circle({
center: [120, 120],
radius: 40,
fillColor: 'green'
});
var c3 = new Path.Circle({
center: [140, 140],
radius: 40,
fillColor: 'blue'
});
equals(function() {
var result = paper.project.hitTestAll([60, 60]);
return result.length === 0;
}, true);
equals(function() {
var result = paper.project.hitTestAll([80, 80]);
return result.length === 1 && result[0].item === c1;
}, true);
equals(function() {
var result = paper.project.hitTestAll([100, 100]);
return result.length === 2 && result[0].item === c2
&& result[1].item === c1;
}, true);
equals(function() {
var result = paper.project.hitTestAll([120, 120]);
return result.length === 3 && result[0].item === c3
&& result[1].item === c2
&& result[2].item === c1;
}, true);
equals(function() {
var result = paper.project.hitTestAll([140, 140]);
return result.length === 2 && result[0].item === c3
&& result[1].item === c2;
}, true);
equals(function() {
var result = paper.project.hitTestAll([160, 160]);
return result.length === 1 && result[0].item === c3;
}, true);
equals(function() {
var result = paper.project.hitTestAll([180, 180]);
return result.length === 0;
}, true);
});
// TODO: project.hitTest(point, {type: AnItemType});