From 69a7d0bfd49d5158ce44b84e2228e8f27edd0c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 7 Jul 2011 22:14:58 +0200 Subject: [PATCH] Implement a first version of Item#hitTest(), so far working only for Path items. Work in progress. --- src/item/HitResult.js | 77 +++++++++++++++++++++++++++++++++++++++ src/item/Item.js | 29 +++++++++++++-- src/load.js | 4 +- src/paper.js | 3 +- src/path/CurveLocation.js | 12 +++++- src/path/Path.js | 26 +++++++++++++ src/project/Project.js | 10 +++++ 7 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/item/HitResult.js diff --git a/src/item/HitResult.js b/src/item/HitResult.js new file mode 100644 index 00000000..532c2c73 --- /dev/null +++ b/src/item/HitResult.js @@ -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); + } + } +}); diff --git a/src/item/Item.js b/src/item/Item.js index dd81967c..3f4ab185 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -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() { diff --git a/src/load.js b/src/load.js index 5e64759a..78e6f2d2 100644 --- a/src/load.js +++ b/src/load.js @@ -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', diff --git a/src/paper.js b/src/paper.js index 60994a56..2cf1f999 100644 --- a/src/paper.js +++ b/src/paper.js @@ -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" diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 85c1c66b..db0e3617 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -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(', ') + ' }'; } }); diff --git a/src/path/Path.js b/src/path/Path.js index b2b2b0f1..3b523d8f 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -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) diff --git a/src/project/Project.js b/src/project/Project.js index 3f19a319..964eb108 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -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} *