diff --git a/src/item/HitResult.js b/src/item/HitResult.js index f59f6975..cc5c3f66 100644 --- a/src/item/HitResult.js +++ b/src/item/HitResult.js @@ -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, ... diff --git a/src/item/Item.js b/src/item/Item.js index 75598c7d..07820b38 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -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; diff --git a/src/item/Project.js b/src/item/Project.js index f8df8bad..bb8b6bf5 100644 --- a/src/item/Project.js +++ b/src/item/Project.js @@ -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; }, /** diff --git a/src/item/Shape.js b/src/item/Shape.js index 392a66f1..9135bf35 100644 --- a/src/item/Shape.js +++ b/src/item/Shape.js @@ -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); diff --git a/src/item/SymbolItem.js b/src/item/SymbolItem.js index cd7f6df2..28616af1 100644 --- a/src/item/SymbolItem.js +++ b/src/item/SymbolItem.js @@ -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) diff --git a/src/path/Curve.js b/src/path/Curve.js index 20a28eb0..0b5ee0c7 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -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, diff --git a/src/path/Path.js b/src/path/Path.js index ed38d9ea..b8b11db4 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -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'; } diff --git a/test/helpers.js b/test/helpers.js index 9322bff6..358a819b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -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) { diff --git a/test/tests/HitResult.js b/test/tests/HitResult.js index 5dd1c86e..2140f3e0 100644 --- a/test/tests/HitResult.js +++ b/test/tests/HitResult.js @@ -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});