mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2024-12-29 09:22:22 -05:00
Implement #hitTestAll() on Item and Project
Along with unit tests and documentation. Closes #536
This commit is contained in:
parent
3ee46ffc5c
commit
4a947317fb
9 changed files with 371 additions and 172 deletions
|
@ -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, ...
|
||||
|
|
211
src/item/Item.js
211
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;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
Loading…
Reference in a new issue