From 3965dd9b77c5949ac7bc5ec8d3ab44b17e05984d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 9 Feb 2016 14:13:30 +0100 Subject: [PATCH] Implement CompoundPath#flatten(), #simplify(), #smooth() And improve documentation for PathItem#simplify(). Closes #920 Relates to #727 --- src/path/CompoundPath.js | 98 ++++++++++++++++++---------------------- src/path/Curve.js | 2 + src/path/Path.js | 71 +---------------------------- src/path/PathItem.js | 93 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 123 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 41252c25..3e119f62 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -134,15 +134,6 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ return items; }, - /** - * Reverses the orientation of all nested paths. - */ - reverse: function() { - var children = this._children; - for (var i = 0, l = children.length; i < l; i++) - children[i].reverse(); - }, - // DOCS: reduce() // TEST: reduce() reduce: function reduce(options) { @@ -162,13 +153,6 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ return reduce.base.call(this); }, - // NOTE: Documentation is in PathItem.js - smooth: function(options) { - var children = this._children; - for (var i = 0, l = children.length; i < l; i++) - children[i].smooth(options); - }, - /** * Specifies whether the compound path is oriented clock-wise. * @@ -331,42 +315,50 @@ new function() { // Injection scope for PostScript-like drawing functions return children[children.length - 1]; } - var fields = { - // NOTE: Documentation for these methods is found in PathItem, as they - // are considered abstract methods of PathItem and need to be defined in - // all implementing classes. - moveTo: function(/* point */) { - var current = getCurrentPath(this), - // Reuse current path if nothing was added yet - path = current && current.isEmpty() ? current - : new Path(Item.NO_INSERT); - if (path !== current) - this.addChild(path); - path.moveTo.apply(path, arguments); - }, - - moveBy: function(/* point */) { - var current = getCurrentPath(this, true), - last = current && current.getLastSegment(), - point = Point.read(arguments); - this.moveTo(last ? point.add(last._point) : point); - }, - - closePath: function(join) { - getCurrentPath(this, true).closePath(join); - } - }; - // Redirect all other drawing commands to the current path - Base.each(['lineTo', 'cubicCurveTo', 'quadraticCurveTo', 'curveTo', 'arcTo', - 'lineBy', 'cubicCurveBy', 'quadraticCurveBy', 'curveBy', 'arcBy'], - function(key) { - fields[key] = function() { - var path = getCurrentPath(this, true); - path[key].apply(path, arguments); - }; - } - ); + return Base.each(['lineTo', 'cubicCurveTo', 'quadraticCurveTo', 'curveTo', + 'arcTo', 'lineBy', 'cubicCurveBy', 'quadraticCurveBy', 'curveBy', + 'arcBy'], + function(key) { + this[key] = function() { + var path = getCurrentPath(this, true); + path[key].apply(path, arguments); + }; + }, { + // NOTE: Documentation for these methods is found in PathItem, as + // they are considered abstract methods of PathItem and need to be + // defined in all implementing classes. + moveTo: function(/* point */) { + var current = getCurrentPath(this), + // Reuse current path if nothing was added yet + path = current && current.isEmpty() ? current + : new Path(Item.NO_INSERT); + if (path !== current) + this.addChild(path); + path.moveTo.apply(path, arguments); + }, - return fields; -}); + moveBy: function(/* point */) { + var current = getCurrentPath(this, true), + last = current && current.getLastSegment(), + point = Point.read(arguments); + this.moveTo(last ? point.add(last._point) : point); + }, + + closePath: function(join) { + getCurrentPath(this, true).closePath(join); + } + } + ); +}, Base.each(['reverse', 'flatten', 'simplify', 'smooth'], function(key) { + // Injection scope for methods forwarded to the child paths. + // NOTE: Documentation is in PathItem + this[key] = function(param) { + var children = this._children, + res; + for (var i = 0, l = children.length; i < l; i++) { + res = children[i][key](param) || res; + } + return res; + }; +}, {})); diff --git a/src/path/Curve.js b/src/path/Curve.js index 54dac0d8..68524c0c 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1070,6 +1070,8 @@ statics: { /** * Returns the curve-time parameter of the specified point if it lies on the * curve, `null` otherwise. + * Note that if there is more than one possible solution in a + * self-intersecting curve, the first found result is returned. * * @param {Point} point the point on the curve * @return {Number} the curve-time parameter of the specified point diff --git a/src/path/Path.js b/src/path/Path.js index dd3e9303..b5a8720d 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1202,9 +1202,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return this; }, - /** - * Reverses the orientation of the path, by reversing all its segments. - */ reverse: function() { this._segments.reverse(); // Reverse the handles: @@ -1223,33 +1220,6 @@ var Path = PathItem.extend(/** @lends Path# */{ this._changed(/*#=*/Change.GEOMETRY); }, - /** - * Converts the curves in a path to straight lines with an even distribution - * of points. The distance between the produced segments is as close as - * possible to the value specified by the `maxDistance` parameter. - * - * @param {Number} maxDistance the maximum distance between the points - * - * @example {@paperscript} - * // Flattening a circle shaped path: - * - * // Create a circle shaped path at { x: 80, y: 50 } - * // with a radius of 35: - * var path = new Path.Circle({ - * center: new Size(80, 50), - * radius: 35 - * }); - * - * // Select the path, so we can inspect its segments: - * path.selected = true; - * - * // Create a copy of the path and move it 150 points to the right: - * var copy = path.clone(); - * copy.position.x += 150; - * - * // Convert its curves to points, with a max distance of 20: - * copy.flatten(20); - */ flatten: function(maxDistance) { var iterator = new PathIterator(this, 64, 0.1), pos = 0, @@ -1291,46 +1261,7 @@ var Path = PathItem.extend(/** @lends Path# */{ return this; }, - /** - * Smooths a path by simplifying it. The {@link Path#segments} array is - * analyzed and replaced by a more optimal set of segments, reducing memory - * usage and speeding up drawing. - * - * @param {Number} [tolerance=2.5] - * - * @example {@paperscript height=300} - * // Click and drag below to draw to draw a line, when you release the - * // mouse, the is made smooth using path.simplify(): - * - * var path; - * function onMouseDown(event) { - * // If we already made a path before, deselect it: - * if (path) { - * path.selected = false; - * } - * - * // Create a new path and add the position of the mouse - * // as its first segment. Select it, so we can see the - * // segment points: - * path = new Path({ - * segments: [event.point], - * strokeColor: 'black', - * selected: true - * }); - * } - * - * function onMouseDrag(event) { - * // On every drag event, add a segment to the path - * // at the position of the mouse: - * path.add(event.point); - * } - * - * function onMouseUp(event) { - * // When the mouse is released, simplify the path: - * path.simplify(); - * path.selected = true; - * } - */ + // NOTE: Documentation is in PathItem#simplify() simplify: function(tolerance) { var segments = new PathFitter(this).fit(tolerance || 2.5); if (segments) diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 130db607..1faa072d 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -317,6 +317,46 @@ var PathItem = Item.extend(/** @lends PathItem# */{ /*#*/ } // !__options.nativeContains && __options.booleanOperations }, + /** + * Reverses the orientation of the path item. When called on + * {@link CompoundPath} items, each of the nested paths is reversed. On + * {@link Path} items, the sequence of {@link Path#segments} is reversed. + * + * @name PathItem#reverse + * @function + */ + + /** + * Converts the curves in a path to straight lines with an even distribution + * of points. The distance between the produced segments is as close as + * possible to the value specified by the `maxDistance` parameter. + * + * @name PathItem#flatten + * @function + * + * @param {Number} maxDistance the maximum distance between the points + * + * @example {@paperscript} + * // Flattening a circle shaped path: + * + * // Create a circle shaped path at { x: 80, y: 50 } + * // with a radius of 35: + * var path = new Path.Circle({ + * center: new Size(80, 50), + * radius: 35 + * }); + * + * // Select the path, so we can inspect its segments: + * path.selected = true; + * + * // Create a copy of the path and move it 150 points to the right: + * var copy = path.clone(); + * copy.position.x += 150; + * + * // Convert its curves to points, with a max distance of 20: + * copy.flatten(20); + */ + // TODO: Write about negative indices, and add an example for ranges. /** * Smooths the path item without changing the amount of segments in the path @@ -467,6 +507,59 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * paths[4].smooth({ type: 'continuous', from: -1, to: 1 }); */ + /** + * Fits a sequence of as few curves as possible through the path's anchor + * points, ignoring the path items's curve-handles, with an allowed maximum + * error. When called on {@link CompoundPath} items, each of the nested + * paths is simplified. On {@link Path} items, the {@link Path#segments} + * array is processed and replaced by the resulting sequence of fitted + * curves. + * + * This method can be used to process and simplify the point data received + * from a mouse or touch device. + * + * @name PathItem#simplify + * @function + * + * @param {Number} [tolerance=2.5] the allowed maximum error when fitting + * the curves through the segment points + * @return {Boolean} {@true if the method was capable of fitting curves + * through the path's segment points} + * + * @example {@paperscript height=300} + * // Click and drag below to draw to draw a line, when you release the + * // mouse, the is made smooth using path.simplify(): + * + * var path; + * function onMouseDown(event) { + * // If we already made a path before, deselect it: + * if (path) { + * path.selected = false; + * } + * + * // Create a new path and add the position of the mouse + * // as its first segment. Select it, so we can see the + * // segment points: + * path = new Path({ + * segments: [event.point], + * strokeColor: 'black', + * selected: true + * }); + * } + * + * function onMouseDrag(event) { + * // On every drag event, add a segment to the path + * // at the position of the mouse: + * path.add(event.point); + * } + * + * function onMouseUp(event) { + * // When the mouse is released, simplify the path: + * path.simplify(); + * path.selected = true; + * } + */ + /** * Interpolates between the two specified path items and uses the result * as the geometry for this path item. The number of children and