Implement a first version of Item#hitTest(), so far working only for Path items. Work in progress.

This commit is contained in:
Jürg Lehni 2011-07-07 22:14:58 +02:00
parent de87c1f97a
commit 69a7d0bfd4
7 changed files with 154 additions and 7 deletions

77
src/item/HitResult.js Normal file
View file

@ -0,0 +1,77 @@
/*
* Paper.js
*
* This file is part of Paper.js, a JavaScript Vector Graphics Library,
* based on Scriptographer.org and designed to be largely API compatible.
* http://paperjs.org/
* http://scriptographer.org/
*
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
* http://lehni.org/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
/**
* @name HitResult
*
* @class
*
* @extends CurveLocation
*/
HitResult = CurveLocation.extend(/** @lends HitResult# */{
initialize: function(type, item) {
this._type = type;
if (item instanceof CurveLocation) {
// If a CurveLocation is passed, we can simply copy over values
// since HitResult is also an instance of CurveLocation.
this._item = item._curve._path;
Base.each(item, function(value, key) {
this[key] = value;
}, this);
} else {
this._item = item;
}
},
statics: {
/**
* Merges default options into options hash for #hitTest() calls, and
* marks as merged, to prevent repeated merging in nested calls.
*
* @private
*/
getOptions: function(options) {
// TODO: Consier moving to HitResult / HitEvent?
return options && options._merged ? options : Base.merge({
// Hit the fill of items
fill: true,
// Hit the curves of path items, taking into account the stroke
// width.
stroke: true,
// Hit the part of segments that curves pass through
// (Segment#point)
// TODO: Shall we call this points?
segments: true,
// Only first or last segment hits on path (mutually exclusive
// with segments: true)
// TODO: Shall we calls this: endPoints?
ends: false,
// Hit the parts of segments that define the curvature
handles: true,
// Hit items that are marked as guides
guides: false,
// Only hit selected objects
selected: false,
// Type of item, for instanceof check: PathItem, TexItem, etc
type: Item,
// Tolerance
tolerance: 2,
// Mark as merged, so next time Base.merge isn't called
_merged: true
}, options);
}
}
});

View file

@ -668,6 +668,27 @@ var Item = this.Item = Base.extend(/** @lends Item# */{
return raster;
},
hitTest: function(point, options, matrix) {
options = HitResult.getOptions(options);
// TODO: Support option.type even for things like CompoundPath where
// children are matched but the parent is returned.
// Filter for guides or selected items if that's required
return this._children || !(options.guides && !this._guide
|| options.selected && !this._selected)
? this._hitTest(point, options, matrix) : null;
},
_hitTest: function(point, options, matrix) {
if (this._children) {
for (var i = 0, l = this._children.length; i < l; i++) {
var res = this._children[i].hitTest(point, options, matrix);
if (res)
return res;
}
}
},
/**
* {@grouptitle Hierarchy Operations}
* Adds the specified item as a child of this item at the end of the
@ -1583,10 +1604,10 @@ var Item = this.Item = Base.extend(/** @lends Item# */{
},
/*
_transform: function(matrix, flags) {
// The code that performs the actual transformation of content,
// if defined. Item itself does not define this.
},
_transform: function(matrix, flags) {
// The code that performs the actual transformation of content,
// if defined. Item itself does not define this.
},
*/
toString: function() {

View file

@ -45,12 +45,14 @@ var sources = [
'src/item/PlacedItem.js',
'src/item/Raster.js',
'src/item/PlacedSymbol.js',
// TODO: Move to item?
'src/path/CurveLocation.js',
'src/item/HitResult.js',
'src/path/Segment.js',
'src/path/SegmentPoint.js',
'src/path/SelectionState.js',
'src/path/Curve.js',
'src/path/CurveLocation.js',
'src/path/PathItem.js',
'src/path/Path.js',
'src/path/CompoundPath.js',

View file

@ -75,12 +75,13 @@ var paper = new function() {
//#include "item/PlacedItem.js"
//#include "item/Raster.js"
//#include "item/PlacedSymbol.js"
//#include "path/CurveLocation.js"
//#include "item/HitResult.js"
//#include "path/Segment.js"
//#include "path/SegmentPoint.js"
//#include "path/SelectionState.js"
//#include "path/Curve.js"
//#include "path/CurveLocation.js"
//#include "path/PathItem.js"
//#include "path/Path.js"
//#include "path/CompoundPath.js"

View file

@ -14,6 +14,7 @@
* All rights reserved.
*/
// TODO: Consider simply naming Location?
/**
* @name CurveLocation
*
@ -87,7 +88,8 @@ CurveLocation = Base.extend(/** @lends CurveLocation# */{
* @bean
*/
getItem: function() {
return this._curve && this._curve._path;
// Support _item for HitResult
return this._item || this._curve && this._curve._path;
},
/**
@ -193,6 +195,12 @@ CurveLocation = Base.extend(/** @lends CurveLocation# */{
toString: function() {
var parts = [],
point = this.getPoint();
// Support for HitResult that inherits from CurveLocation
if (this._type)
parts.push('type: ' + this._type);
if (this._item)
parts.push('item: ' + this._item);
// Normal CurveLocation properties:
if (point)
parts.push('point: ' + point);
var index = this.getIndex();
@ -201,6 +209,8 @@ CurveLocation = Base.extend(/** @lends CurveLocation# */{
var parameter = this.getParameter();
if (parameter != null)
parts.push('parameter: ' + Base.formatNumber(parameter));
if (this._distance != null)
parts.push('distance: ' + Base.formatNumber(this._distance));
return '{ ' + parts.join(', ') + ' }';
}
});

View file

@ -1193,6 +1193,32 @@ var Path = this.Path = PathItem.extend(/** @lends Path# */{
for (var i = 0, l = curves.length; i < l; i++)
crossings += curves[i].getCrossings(point, matrix);
return (crossings & 1) == 1;
},
_hitTest: function(point, options, matrix) {
// TODO:
// segments: true,
// ends: false,
// handles: true,
var radius = (options.stroke ? this.getStrokeWidth() / 2 : 0)
+ (options.tolerance || 0),
loc;
// If we're querying for stroke, perform that before fill
if (options.stroke && radius > 0)
loc = this.getNearestLocation(point, matrix);
// Don't process loc yet, as we also need to query for stroke after fill
// in some cases. Simply skip fill query if we already have a matching
// stroke.
if (!(loc && loc._distance <= radius) && options.fill
&& this.getFillColor() && this.contains(point, matrix))
return new HitResult('fill', this);
// Now query stroke if we haven't already
if (!loc && radius > 0)
loc = this.getNearestLocation(point, matrix);
if (loc._distance <= radius)
return options.stroke
? new HitResult('stroke', loc)
: new HitResult('fill', this);
}
// TODO: intersects(item)

View file

@ -181,6 +181,16 @@ var Project = this.Project = Base.extend(/** @lends Project# */{
this._selectedItems[i].setSelected(false);
},
hitTest: function(point, options) {
options = HitResult.getOptions(options);
for (var i = 0, l = this.layers.length; i < l; i++) {
var res = this.layers[i].hitTest(point, options);
if (res)
return res;
}
return null;
},
/**
* {@grouptitle Project Hierarchy}
*