Merge branch 'develop'

This commit is contained in:
Jürg Lehni 2017-04-19 19:55:26 +02:00
commit 836210bb86
64 changed files with 3237 additions and 1112 deletions

View file

@ -1,23 +1,15 @@
# This file is for unifying the coding style for different editors and IDEs.
# More information at http://editorconfig.org
root = true root = true
[*] [*]
indent_style = space
end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true indent_style = space
insert_final_newline = true
[*.{js,html,css}]
indent_size = 4 indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.json] [*.json]
indent_size = 2 indent_size = 2
[*.md] [*.md]
indent_size = 4
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.sh]
indent_size = 4

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/node_modules/ /node_modules/
/dist/*/ /dist/*/
/.nvmrc

6
.gitmodules vendored
View file

@ -1,3 +1,9 @@
[submodule "jsdoc"] [submodule "jsdoc"]
path = gulp/jsdoc path = gulp/jsdoc
url = https://github.com/paperjs/jsdoc.git url = https://github.com/paperjs/jsdoc.git
[submodule "paper-jsdom"]
path = packages/paper-jsdom
url = https://github.com/paperjs/paper-jsdom.git
[submodule "paper-jsdom-canvas"]
path = packages/paper-jsdom-canvas
url = https://github.com/paperjs/paper-jsdom-canvas.git

View file

@ -7,6 +7,7 @@
"supernew": true, "supernew": true,
"laxbreak": true, "laxbreak": true,
"eqeqeq": false, "eqeqeq": false,
"-W041": false,
"eqnull": true, "eqnull": true,
"loopfunc": true, "loopfunc": true,
"boss": true, "boss": true,

View file

@ -1,6 +1,55 @@
# Change Log # Change Log
## `0.10.3` (Unreleased) ## `0.10.4`
### Changed
- Separate `paper` module on NPM into: `paper`, `paper-jsdom` and
`paper-jsdom-canvas` (#1252):
- `paper` is the main library, and can be used directly in a browser
context, e.g. a web browser or worker.
- `paper-jsdom` is a shim module for Node.js, offering headless use with SVG
importing / exporting.
- `paper-jsdom-canvas` is a shim module for Node.js, offering rendering
through Node Canvas.
### Added
- PaperScript: Support newer, external versions of Acorn.js for PaperScript
parsing, opening the doors to ES 2015 (#1183, #1275).
- Hit Tests: Implement `options.position` to hit `Item#position` (#1249).
- Split `Item#copyTo()` into `#addTo()` and `#copyTo()`.
### Fixed
- Intersections: Bring back special handling of curve end-points (#1284).
- Intersections: Correctly handle `Item#applyMatrix = false` (#1289).
- Boolean: Bring back on-path winding handling (#1281).
- Boolean: Pass on options in `PathItem#subtract(path, options)` (#1221).
- Boolean: Implement `options.trace` as a way to perform boolean operations on
the strokes / traces instead of the fills / areas of the involved paths
(#1221).
- Boolean: Always return `CompoundPath items (#1221).
- Style: Fix handling of gradient matrices when `Item#applyMatrix = false`
(#1238).
- Style: Prevent cleaning pre-existing styles when setting `Item#style` to an
object (#1277).
- Mouse Events: Only handle dragItem if the hitItem responds to `mousedrag`
events (#1247, #1286).
- Bounds: Clear parent's bounds cache when item's visibility changes (#1248).
- Bounds: Fix calculation of internal bounds with children and
`Item#applyMatrix = false` (#1250).
- Hit-Tests: Fix issue with non-invertible matrices ( #1271).
- SVG Import: Improve handling of sizes in percent (#1242).
- Rectangle: Improve handling of dimension properties, dealing better with
`left` / `top` / `right` / `bottom` / `center` values (#1147).
- Scene Graph: Do not allow inserting same item as child multiple times.
- Path: Fix handling of `insert = false` in `new Path.Constructor()`
initialization (#1305).
- PaperScript: Fix positive unary operator.
- Docs: Fix parameter sequence in Matrix constructor (#1273).
- Docs: Add documentation for options.bound and options.matrix in `#exportSVG()`
(#1254).
- Docs: Fix wrong `@link` references to bean properties.
## `0.10.3`
### Changed ### Changed
- Node.js: Support v7, and keep testing v4 up to v7 in Travis CI. - Node.js: Support v7, and keep testing v4 up to v7 in Travis CI.
@ -77,6 +126,8 @@
- Hit Tests: Fix stroke hit-testing for rounded shape items (#1207). - Hit Tests: Fix stroke hit-testing for rounded shape items (#1207).
- Fix matrix cloning for groups with `#applyMatrix = false` ( #1225). - Fix matrix cloning for groups with `#applyMatrix = false` ( #1225).
- Correctly handle offset in `Curve#divideAt(offset)` (#1230). - Correctly handle offset in `Curve#divideAt(offset)` (#1230).
- Fix issue with `Curve#isStraight()` where handles were checked incorrectly
for collinearity (#1269).
- Fix `Line#getSide()` imprecisions when points are on the line. - Fix `Line#getSide()` imprecisions when points are on the line.
- Docs: Fix documentation of `Project#hitTestAll()` (#536). - Docs: Fix documentation of `Project#hitTestAll()` (#536).
- Docs: Improve description of `option.class` value in `Project#hitTest()` - Docs: Improve description of `option.class` value in `Project#hitTest()`

View file

@ -78,7 +78,7 @@ And from there onwards, you should be able to use Bower like this:
bower search paperjs bower search paperjs
### Installing Paper.js for Node.js through NPM ### Installing Paper.js for Node.js
NPM is used to install Paper.js for use in Node.js. But before installing, you NPM is used to install Paper.js for use in Node.js. But before installing, you
need the Cairo Graphics library installed, see: <http://cairographics.org/>. need the Cairo Graphics library installed, see: <http://cairographics.org/>.
@ -125,6 +125,17 @@ You should now be able to install the Paper.js module from NPM:
npm install paper npm install paper
### Installing Paper.js for Electron
[Node-Canvas](https://github.com/Automattic/node-canvas) is a native dependency.
In order to build it for use in Electron, which is likely to use a different
version of V8 than the Node binary installed in your system, you need to
manually specify the location of Electrons headers. Follow these steps to do
so:
[Electron — Using Native Node
Modules](https://electron.atom.io/docs/tutorial/using-native-node-modules/)
## Development ## Development
The main Paper.js source tree is hosted on The main Paper.js source tree is hosted on

View file

@ -18,6 +18,7 @@
"gulpfile.js", "gulpfile.js",
"gulp", "gulp",
"node_modules", "node_modules",
"packages",
"projects", "projects",
"src", "src",
"test", "test",
@ -37,6 +38,7 @@
"canvas", "canvas",
"svg", "svg",
"paper", "paper",
"paper.js" "paper.js",
"paperjs"
] ]
} }

471
dist/paper-core.js vendored

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

1817
dist/paper-full.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -11,18 +11,23 @@
*/ */
var gulp = require('gulp'), var gulp = require('gulp'),
bump = require('gulp-bump'), jsonEditor = require('gulp-json-editor'),
git = require('gulp-git-streamed'), git = require('gulp-git-streamed'),
run = require('run-sequence'), run = require('run-sequence'),
shell = require('gulp-shell'), shell = require('gulp-shell'),
options = require('../utils/options.js'); options = require('../utils/options.js');
var jsonOptions = {
end_with_newline: true
};
gulp.task('publish', function() { gulp.task('publish', function() {
if (options.branch !== 'develop') { if (options.branch !== 'develop') {
throw new Error('Publishing is only allowed on the develop branch.'); throw new Error('Publishing is only allowed on the develop branch.');
} }
return run( return run(
'publish:version', 'publish:version',
'publish:packages',
'publish:dist', 'publish:dist',
'publish:commit', 'publish:commit',
'publish:release', 'publish:release',
@ -35,10 +40,24 @@ gulp.task('publish:version', function() {
// but we don't wan the published version suffixed with '-develop'. // but we don't wan the published version suffixed with '-develop'.
options.resetVersion(); options.resetVersion();
return gulp.src(['package.json']) return gulp.src(['package.json'])
.pipe(bump({ version: options.version })) .pipe(jsonEditor({
version: options.version
}, jsonOptions))
.pipe(gulp.dest('.')); .pipe(gulp.dest('.'));
}); });
gulp.task('publish:packages', function() {
options.resetVersion(); // See 'publish:version'
return gulp.src(['packages/**/*.json'])
.pipe(jsonEditor({
version: options.version,
dependencies: {
paper: options.version
}
}, jsonOptions))
.pipe(gulp.dest('packages'));
});
gulp.task('publish:dist', ['dist']); gulp.task('publish:dist', ['dist']);
gulp.task('publish:commit', function() { gulp.task('publish:commit', function() {

View file

@ -1,6 +1,6 @@
{ {
"name": "paper", "name": "paper",
"version": "0.10.3", "version": "0.10.4",
"description": "The Swiss Army Knife of Vector Graphics Scripting", "description": "The Swiss Army Knife of Vector Graphics Scripting",
"license": "MIT", "license": "MIT",
"homepage": "http://paperjs.org", "homepage": "http://paperjs.org",
@ -36,21 +36,15 @@
"engines": { "engines": {
"node": ">=4.0.0 <8.0.0" "node": ">=4.0.0 <8.0.0"
}, },
"dependencies": {
"jsdom": "^9.4.0",
"source-map-support": "^0.4.0"
},
"optionalDependencies": {
"canvas": "^1.3.5"
},
"devDependencies": { "devDependencies": {
"acorn": "~0.5.0", "acorn": "~0.5.0",
"canvas": "^1.3.5",
"del": "^2.2.1", "del": "^2.2.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-bump": "^2.2.0",
"gulp-cached": "^1.1.0", "gulp-cached": "^1.1.0",
"gulp-git-streamed": "^1.8.0", "gulp-git-streamed": "^1.8.0",
"gulp-jshint": "^2.0.0", "gulp-jshint": "^2.0.0",
"gulp-json-editor": "^2.2.1",
"gulp-prepro": "^2.4.0", "gulp-prepro": "^2.4.0",
"gulp-qunits": "^2.1.1", "gulp-qunits": "^2.1.1",
"gulp-rename": "^1.2.2", "gulp-rename": "^1.2.2",
@ -63,6 +57,7 @@
"gulp-whitespace": "^0.1.0", "gulp-whitespace": "^0.1.0",
"gulp-zip": "^3.2.0", "gulp-zip": "^3.2.0",
"husky": "^0.11.4", "husky": "^0.11.4",
"jsdom": "^9.4.0",
"jshint": "^2.9.2", "jshint": "^2.9.2",
"jshint-summary": "^0.4.0", "jshint-summary": "^0.4.0",
"merge-stream": "^1.0.0", "merge-stream": "^1.0.0",
@ -72,15 +67,12 @@
"require-dir": "^0.3.0", "require-dir": "^0.3.0",
"resemblejs": "^2.2.1", "resemblejs": "^2.2.1",
"run-sequence": "^1.2.2", "run-sequence": "^1.2.2",
"source-map-support": "^0.4.0",
"stats.js": "0.16.0", "stats.js": "0.16.0",
"straps": "^2.1.0" "straps": "^2.1.0"
}, },
"browser": { "browser": {
"canvas": false, "./dist/node/self.js": false,
"jsdom": false,
"jsdom/lib/jsdom/living/generated/utils": false,
"source-map-support": false,
"./dist/node/window.js": false,
"./dist/node/extend.js": false "./dist/node/extend.js": false
}, },
"keywords": [ "keywords": [
@ -97,6 +89,7 @@
"canvas", "canvas",
"svg", "svg",
"paper", "paper",
"paper.js" "paper.js",
"paperjs"
] ]
} }

1
packages/paper-jsdom Submodule

@ -0,0 +1 @@
Subproject commit 2bed95b950805864a91999afeaed469ac4403338

@ -0,0 +1 @@
Subproject commit 807961a794f920e107c483ce5301867af8c0cb27

View file

@ -14,6 +14,9 @@
"path": "../gulp", "path": "../gulp",
"folder_exclude_patterns": ["jsdoc"] "folder_exclude_patterns": ["jsdoc"]
}, },
{
"path": "../packages",
},
{ {
"path": "../travis", "path": "../travis",
}, },

View file

@ -108,9 +108,17 @@ var Line = Base.extend(/** @lends Line# */{
* @return {Number} * @return {Number}
*/ */
getDistance: function(point) { getDistance: function(point) {
return Math.abs(Line.getSignedDistance( return Math.abs(this.getSignedDistance(point));
this._px, this._py, this._vx, this._vy, },
point.x, point.y, true));
// DOCS: document Line#getSignedDistance(point)
/**
* @param {Point} point
* @return {Number}
*/
getSignedDistance: function(point) {
return Line.getSignedDistance(this._px, this._py, this._vx, this._vy,
point.x, point.y, true);
}, },
isCollinear: function(line) { isCollinear: function(line) {

View file

@ -52,8 +52,8 @@ var Matrix = Base.extend(/** @lends Matrix# */{
* *
* @name Matrix#initialize * @name Matrix#initialize
* @param {Number} a the a property of the transform * @param {Number} a the a property of the transform
* @param {Number} c the c property of the transform
* @param {Number} b the b property of the transform * @param {Number} b the b property of the transform
* @param {Number} c the c property of the transform
* @param {Number} d the d property of the transform * @param {Number} d the d property of the transform
* @param {Number} tx the tx property of the transform * @param {Number} tx the tx property of the transform
* @param {Number} ty the ty property of the transform * @param {Number} ty the ty property of the transform

View file

@ -267,23 +267,34 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
return new ctor(this.width, this.height, this, 'setSize'); return new ctor(this.width, this.height, this, 'setSize');
}, },
// properties to keep track of fix-width / height: They are on by default,
// and switched off once properties are used that change the outside of the
// rectangle, so combinations of: left / top / right / bottom.
_fw: 1,
_fh: 1,
setSize: function(/* size */) { setSize: function(/* size */) {
var size = Size.read(arguments); var size = Size.read(arguments),
// Keep track of how dimensions were specified through this._fix* sx = this._sx,
sy = this._sy,
w = size.width,
h = size.height;
// Keep track of how dimensions were specified through this._s*
// attributes. // attributes.
// _fixX / Y can either be 0 (l), 0.5 (center) or 1 (r), and is used as // _sx / _sy can either be 0 (left), 0.5 (center) or 1 (right), and is
// direct factors to calculate the x / y adujstments from the size // used as direct factors to calculate the x / y adjustments from the
// differences. // size differences.
// _fixW / H is either 0 (off) or 1 (on), and is used to protect // _fw / _fh can either be 0 (off) or 1 (on), and is used to protect
// widht / height values against changes. // width / height values against changes.
if (this._fixX) if (sx) {
this.x += (this.width - size.width) * this._fixX; this.x += (this.width - w) * sx;
if (this._fixY) }
this.y += (this.height - size.height) * this._fixY; if (sy) {
this.width = size.width; this.y += (this.height - h) * sy;
this.height = size.height; }
this._fixW = 1; this.width = w;
this._fixH = 1; this.height = h;
this._fw = this._fh = 1;
}, },
/** /**
@ -300,10 +311,12 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
}, },
setLeft: function(left) { setLeft: function(left) {
if (!this._fixW) if (!this._fw) {
this.width -= left - this.x; var amount = left - this.x;
this.width -= this._sx === 0.5 ? amount * 2 : amount;
}
this.x = left; this.x = left;
this._fixX = 0; this._sx = this._fw = 0;
}, },
/** /**
@ -318,10 +331,12 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
}, },
setTop: function(top) { setTop: function(top) {
if (!this._fixH) if (!this._fh) {
this.height -= top - this.y; var amount = top - this.y;
this.height -= this._sy === 0.5 ? amount * 2 : amount;
}
this.y = top; this.y = top;
this._fixY = 0; this._sy = this._fh = 0;
}, },
/** /**
@ -336,14 +351,13 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
}, },
setRight: function(right) { setRight: function(right) {
// Turn _fixW off if we specify two _fixX values if (!this._fw) {
if (this._fixX !== undefined && this._fixX !== 1) var amount = right - this.x;
this._fixW = 0; this.width = this._sx === 0.5 ? amount * 2 : amount;
if (this._fixW) }
this.x = right - this.width; this.x = right - this.width;
else this._sx = 1;
this.width = right - this.x; this._fw = 0;
this._fixX = 1;
}, },
/** /**
@ -358,14 +372,13 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
}, },
setBottom: function(bottom) { setBottom: function(bottom) {
// Turn _fixH off if we specify two _fixY values if (!this._fh) {
if (this._fixY !== undefined && this._fixY !== 1) var amount = bottom - this.y;
this._fixH = 0; this.height = this._sy === 0.5 ? amount * 2 : amount;
if (this._fixH) }
this.y = bottom - this.height; this.y = bottom - this.height;
else this._sy = 1;
this.height = bottom - this.y; this._fh = 0;
this._fixY = 1;
}, },
/** /**
@ -376,12 +389,22 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
* @ignore * @ignore
*/ */
getCenterX: function() { getCenterX: function() {
return this.x + this.width * 0.5; return this.x + this.width / 2;
}, },
setCenterX: function(x) { setCenterX: function(x) {
this.x = x - this.width * 0.5; // If we're asked to fix the width or if _sx is already in center mode,
this._fixX = 0.5; // just keep moving the center.
if (this._fw || this._sx === 0.5) {
this.x = x - this.width / 2;
} else {
if (this._sx) {
this.x += (x - this.x) * 2 * this._sx;
}
this.width = (x - this.x) * 2;
}
this._sx = 0.5;
this._fw = 0;
}, },
/** /**
@ -392,12 +415,22 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
* @ignore * @ignore
*/ */
getCenterY: function() { getCenterY: function() {
return this.y + this.height * 0.5; return this.y + this.height / 2;
}, },
setCenterY: function(y) { setCenterY: function(y) {
this.y = y - this.height * 0.5; // If we're asked to fix the height or if _sy is already in center mode,
this._fixY = 0.5; // just keep moving the center.
if (this._fh || this._sy === 0.5) {
this.y = y - this.height / 2;
} else {
if (this._sy) {
this.y += (y - this.y) * 2 * this._sy;
}
this.height = (y - this.y) * 2;
}
this._sy = 0.5;
this._fh = 0;
}, },
/** /**
@ -599,10 +632,13 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
/** /**
* Tests if the interior of this rectangle intersects the interior of * Tests if the interior of this rectangle intersects the interior of
* another rectangle. Rectangles just touching each other are considered * another rectangle. Rectangles just touching each other are considered as
* as non-intersecting. * non-intersecting, except if a `epsilon` value is specified by which this
* rectangle's dimensions are increased before comparing.
* *
* @param {Rectangle} rect the specified rectangle * @param {Rectangle} rect the specified rectangle
* @param {Number} [epsilon=0] the epsilon against which to compare the
* rectangle's dimensions
* @return {Boolean} {@true if the rectangle and the specified rectangle * @return {Boolean} {@true if the rectangle and the specified rectangle
* intersect each other} * intersect each other}
* *
@ -641,20 +677,13 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
* } * }
* } * }
*/ */
intersects: function(/* rect */) { intersects: function(/* rect, epsilon */) {
var rect = Rectangle.read(arguments); var rect = Rectangle.read(arguments),
return rect.x + rect.width > this.x epsilon = Base.read(arguments) || 0;
&& rect.y + rect.height > this.y return rect.x + rect.width > this.x - epsilon
&& rect.x < this.x + this.width && rect.y + rect.height > this.y - epsilon
&& rect.y < this.y + this.height; && rect.x < this.x + this.width + epsilon
}, && rect.y < this.y + this.height + epsilon;
touches: function(/* rect */) {
var rect = Rectangle.read(arguments);
return rect.x + rect.width >= this.x
&& rect.y + rect.height >= this.y
&& rect.x <= this.x + this.width
&& rect.y <= this.y + this.height;
}, },
/** /**

View file

@ -528,7 +528,7 @@ Base.inject(/** @lends Base# */{
exportJSON: function(obj, options) { exportJSON: function(obj, options) {
var json = Base.serialize(obj, options); var json = Base.serialize(obj, options);
return options && options.asString === false return options && options.asString == false
? json ? json
: JSON.stringify(json); : JSON.stringify(json);
}, },

View file

@ -90,7 +90,7 @@ var Emitter = {
if (setTarget) if (setTarget)
event.currentTarget = this; event.currentTarget = this;
for (var i = 0, l = handlers.length; i < l; i++) { for (var i = 0, l = handlers.length; i < l; i++) {
if (handlers[i].apply(this, args) === false) { if (handlers[i].apply(this, args) == false) {
// If the handler returns false, prevent the default behavior // If the handler returns false, prevent the default behavior
// and stop propagation of the event by calling stop() // and stop propagation of the event by calling stop()
if (event && event.stop) if (event && event.stop)

View file

@ -57,7 +57,6 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
this.project = null; this.project = null;
this.projects = []; this.projects = [];
this.tools = []; this.tools = [];
this.palettes = [];
// Assign a unique id to each scope . // Assign a unique id to each scope .
this._id = PaperScope._id++; this._id = PaperScope._id++;
PaperScope._scopes[this._id] = this; PaperScope._scopes[this._id] = this;
@ -124,10 +123,10 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
* *
* @option [settings.insertItems=true] {Boolean} controls whether newly * @option [settings.insertItems=true] {Boolean} controls whether newly
* created items are automatically inserted into the scene graph, by * created items are automatically inserted into the scene graph, by
* adding them to {@link Project#getActiveLayer()} * adding them to {@link Project#activeLayer}
* @option [settings.applyMatrix=true] {Boolean} controls what value newly * @option [settings.applyMatrix=true] {Boolean} controls what value newly
* created items have their {@link Item#getApplyMatrix()} property set * created items have their {@link Item#applyMatrix} property set to
* to (Note that not all items can set this to `false`) * (Note that not all items can set this to `false`)
* @option [settings.handleSize=4] {Number} the size of the curve handles * @option [settings.handleSize=4] {Number} the size of the curve handles
* when drawing selections * when drawing selections
* @option [settings.hitTolerance=0] {Number} the default tolerance for hit- * @option [settings.hitTolerance=0] {Number} the default tolerance for hit-
@ -277,14 +276,11 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
// Remove all projects, views and tools. // Remove all projects, views and tools.
// This also removes the installed event handlers. // This also removes the installed event handlers.
var projects = this.projects, var projects = this.projects,
tools = this.tools, tools = this.tools;
palettes = this.palettes;
for (var i = projects.length - 1; i >= 0; i--) for (var i = projects.length - 1; i >= 0; i--)
projects[i].remove(); projects[i].remove();
for (var i = tools.length - 1; i >= 0; i--) for (var i = tools.length - 1; i >= 0; i--)
tools[i].remove(); tools[i].remove();
for (var i = palettes.length - 1; i >= 0; i--)
palettes[i].remove();
}, },
remove: function() { remove: function() {

View file

@ -15,12 +15,33 @@
* @namespace * @namespace
*/ */
Base.exports.PaperScript = function() { Base.exports.PaperScript = function() {
// Locally turn of exports and define for inlined acorn. // `this` == global scope, as the function is called with `.call(this);`
// Just declaring the local vars is enough, as they will be undefined. var global = this,
var exports, define, // See if there is a global Acorn in the browser already.
// The scope into which the library is loaded. acorn = global.acorn;
scope = this; // Also try importing an outside version of Acorn.
/*#*/ include('../../node_modules/acorn/acorn.min.js', { exports: false }); if (!acorn && typeof require !== 'undefined') {
try { acorn = require('acorn'); } catch(e) {}
}
// If no Acorn was found, load the bundled version.
if (!acorn) {
// Provide our own local exports and module object so that Acorn gets
// assigned to it and ends up in the local acorn object.
var exports, module;
acorn = exports = module = {};
/*#*/ include('../../node_modules/acorn/acorn.js', { exports: false });
// Clear object again if it wasn't loaded here; for load.js, see below.
if (!acorn.version)
acorn = null;
}
function parse(code, options) {
// NOTE: When using load.js, Acorn will end up in global.acorn and will
// not be immediately available, so we need to check for it here again.
// We also give global.acorn the preference over the bundled one, so
// people can load their own preferred version in sketch.paperjs.org
return (global.acorn || acorn).parse(code, options);
}
// Operators to overload // Operators to overload
@ -37,7 +58,7 @@ Base.exports.PaperScript = function() {
var unaryOperators = { var unaryOperators = {
'-': '__negate', '-': '__negate',
'+': null '+': '__self'
}; };
// Inject underscored math methods as aliases to Point, Size and Color. // Inject underscored math methods as aliases to Point, Size and Color.
@ -48,7 +69,12 @@ Base.exports.PaperScript = function() {
// classes using Straps.js' #inject() // classes using Straps.js' #inject()
this['__' + name] = '#' + name; this['__' + name] = '#' + name;
}, },
{} {
// Needed for '+' unary operator:
__self: function() {
return this;
}
}
); );
Point.inject(fields); Point.inject(fields);
Size.inject(fields); Size.inject(fields);
@ -79,7 +105,7 @@ Base.exports.PaperScript = function() {
// Unary Operator Handler // Unary Operator Handler
function $__(operator, value) { function $__(operator, value) {
var handler = unaryOperators[operator]; var handler = unaryOperators[operator];
if (handler && value && value[handler]) if (value && value[handler])
return value[handler](); return value[handler]();
switch (operator) { switch (operator) {
case '+': return +value; case '+': return +value;
@ -89,10 +115,6 @@ Base.exports.PaperScript = function() {
// AST Helpers // AST Helpers
function parse(code, options) {
return scope.acorn.parse(code, options);
}
/** /**
* Compiles PaperScript code into JavaScript code. * Compiles PaperScript code into JavaScript code.
* *
@ -146,7 +168,7 @@ Base.exports.PaperScript = function() {
// Returns the code between two nodes, e.g. an operator and white-space. // Returns the code between two nodes, e.g. an operator and white-space.
function getBetween(left, right) { function getBetween(left, right) {
return code.substring(getOffset(left.range[1]), return code.substring(getOffset(left.range[1]),
getOffset(right.range[0])); getOffset(right.range[0]) - 1);
} }
// Replaces the node's code with a new version and keeps insertions // Replaces the node's code with a new version and keeps insertions
@ -342,7 +364,7 @@ Base.exports.PaperScript = function() {
}; };
} }
// Now do the parsing magic // Now do the parsing magic
walkAST(parse(code, { ranges: true })); walkAST(parse(code, { ranges: true, preserveParens: true }));
if (map) { if (map) {
if (offsetCode) { if (offsetCode) {
// Adjust the line offset of the resulting code if required. // Adjust the line offset of the resulting code if required.

View file

@ -35,8 +35,9 @@ paper = new (PaperScope.inject(Base.exports, {
// If we're on node, require some additional functionality now before finishing: // If we're on node, require some additional functionality now before finishing:
// - PaperScript support in require() with sourceMaps // - PaperScript support in require() with sourceMaps
// - exportFrames / exportImage on CanvasView // - exportFrames / exportImage on CanvasView
if (paper.agent.node) if (paper.agent.node) {
require('./node/extend.js')(paper); require('./node/extend.js')(paper);
}
// https://github.com/umdjs/umd // https://github.com/umdjs/umd
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {

View file

@ -17,14 +17,19 @@
// their shared scope. // their shared scope.
/* global document:true, window:true */ /* global document:true, window:true */
// Create a window variable valid in the paper.js scope, that points to the // Set up a local `window` variable valid across the full the paper.js scope,
// native window in browsers and the emulated JSDom one in node.js // pointing to the native window in browsers and the one provided by JSDom in
// In workers, and on Node.js when no Canvas is present, `window` is null (but // Node.js
// `self` is defined), so we can use the validity of the local window object to // In workers and on Node.js, the global `window` variable is null. In workers,
// detect a worker-like context in the library. // `self` is defined as a `WorkerGlobalScope` object, while in Node.js, `self`
// Make sure `self` always points to a window object, also on Node.js. // is null.
self = self || require('./node/window.js'); // When `self` is null (Node.js only). './node/self.js' is required to provide
// a window object through JSDom and assigned it to `self`.
// When `self.window` and therefore the local `window` is still null after that,
// we know that we are in a worker-like context. This check is used all across
// the library, see for example `View.create()`.
// NOTE: We're not modifying the global `self` here. We receive its value passed // NOTE: We're not modifying the global `self` here. We receive its value passed
// to the paper.js function scope, and this is the one that is modified here. // to the paper.js function scope, and this is the one that is modified here.
self = self || require('./node/self.js');
var window = self.window, var window = self.window,
document = self.document; document = self.document;

View file

@ -127,6 +127,8 @@ var HitResult = Base.extend(/** @lends HitResult# */{
// Only first or last segment hits on path (mutually exclusive // Only first or last segment hits on path (mutually exclusive
// with segments: true) // with segments: true)
ends: false, ends: false,
// Hit test the item position
position: false,
// Hit test the center of the bounds // Hit test the center of the bounds
center: false, center: false,
// Hit test the corners and side-centers of the bounding box // Hit test the corners and side-centers of the bounding box

View file

@ -154,7 +154,7 @@ new function() { // Injection scope for various item event handlers
this._style = new Style(project._currentStyle, this, project); this._style = new Style(project._currentStyle, this, project);
// Do not add to the project if it's an internal path, or if // Do not add to the project if it's an internal path, or if
// props.insert or settings.isnertItems is false. // props.insert or settings.isnertItems is false.
if (internal || hasProps && props.insert === false if (internal || hasProps && props.insert == false
|| !settings.insertItems && !(hasProps && props.insert === true)) { || !settings.insertItems && !(hasProps && props.insert === true)) {
this._setProject(project); this._setProject(project);
} else { } else {
@ -407,16 +407,20 @@ new function() { // Injection scope for various item event handlers
// call _changed() if a property was modified. // call _changed() if a property was modified.
function(name) { function(name) {
var part = Base.capitalize(name), var part = Base.capitalize(name),
name = '_' + name; key = '_' + name,
flags = {
// #locked does not change appearance, all others do:
locked: /*#=*/ChangeFlag.ATTRIBUTE,
// #visible changes apperance
visible: /*#=*/(Change.ATTRIBUTE | Change.GEOMETRY)
};
this['get' + part] = function() { this['get' + part] = function() {
return this[name]; return this[key];
}; };
this['set' + part] = function(value) { this['set' + part] = function(value) {
if (value != this[name]) { if (value != this[key]) {
this[name] = value; this[key] = value;
// #locked does not change appearance, all others do: this._changed(flags[name] || /*#=*/Change.ATTRIBUTE);
this._changed(name === '_locked'
? /*#=*/ChangeFlag.ATTRIBUTE : /*#=*/Change.ATTRIBUTE);
} }
}; };
}, },
@ -889,14 +893,14 @@ new function() { // Injection scope for various item event handlers
* Private method that deals with the calling of _getBounds, recursive * Private method that deals with the calling of _getBounds, recursive
* matrix concatenation and handles all the complicated caching mechanisms. * matrix concatenation and handles all the complicated caching mechanisms.
*/ */
_getCachedBounds: function(matrix, options) { _getCachedBounds: function(matrix, options, noInternal) {
// See if we can cache these bounds. We only cache the bounds // See if we can cache these bounds. We only cache the bounds
// transformed with the internally stored _matrix, (the default if no // transformed with the internally stored _matrix, (the default if no
// matrix is passed). // matrix is passed).
matrix = matrix && matrix._orNullIfIdentity(); matrix = matrix && matrix._orNullIfIdentity();
// Do not transform by the internal matrix for internal, untransformed // Do not transform by the internal matrix for internal, untransformed
// bounds. // bounds.
var internal = options.internal, var internal = options.internal && !noInternal,
cacheItem = options.cacheItem, cacheItem = options.cacheItem,
_matrix = internal ? null : this._matrix._orNullIfIdentity(), _matrix = internal ? null : this._matrix._orNullIfIdentity(),
// Create a key for caching, reflecting all bounds options. // Create a key for caching, reflecting all bounds options.
@ -919,7 +923,7 @@ new function() { // Injection scope for various item event handlers
var cached = this._bounds[cacheKey] = { var cached = this._bounds[cacheKey] = {
rect: bounds.clone(), rect: bounds.clone(),
// Mark as internal, so Item#transform() won't transform it // Mark as internal, so Item#transform() won't transform it
internal: options.internal internal: internal
}; };
} }
return bounds; return bounds;
@ -1006,8 +1010,11 @@ new function() { // Injection scope for various item event handlers
for (var i = 0, l = items.length; i < l; i++) { for (var i = 0, l = items.length; i < l; i++) {
var item = items[i]; var item = items[i];
if (item._visible && !item.isEmpty()) { if (item._visible && !item.isEmpty()) {
// Pass true for noInternal, since even when getting
// internal bounds for this item, we need to apply the
// matrices to its children.
var rect = item._getCachedBounds( var rect = item._getCachedBounds(
matrix && matrix.appended(item._matrix), options); matrix && matrix.appended(item._matrix), options, true);
x1 = Math.min(rect.x, x1); x1 = Math.min(rect.x, x1);
y1 = Math.min(rect.y, y1); y1 = Math.min(rect.y, y1);
x2 = Math.max(rect.x + rect.width, x2); x2 = Math.max(rect.x + rect.width, x2);
@ -1833,10 +1840,12 @@ new function() { // Injection scope for hit-test functions shared with project
* Segment#handleIn} / {@link Segment#handleOut}) of path segments. * Segment#handleIn} / {@link Segment#handleOut}) of path segments.
* @option options.ends {Boolean} only hit-test for the first or last * @option options.ends {Boolean} only hit-test for the first or last
* segment points of open path items * segment points of open path items
* @option options.bounds {Boolean} hit-test the corners and side-centers of * @option options.position {Boolean} hit-test the {@link Item#position} of
* the bounding rectangle of items ({@link Item#bounds}) * of items, which depends on the setting of {@link Item#pivot}
* @option options.center {Boolean} hit-test the {@link Rectangle#center} of * @option options.center {Boolean} hit-test the {@link Rectangle#center} of
* the bounding rectangle of items ({@link Item#bounds}) * the bounding rectangle of items ({@link Item#bounds})
* @option options.bounds {Boolean} hit-test the corners and side-centers of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.guides {Boolean} hit-test items that have {@link * @option options.guides {Boolean} hit-test items that have {@link
* Item#guide} set to `true` * Item#guide} set to `true`
* @option options.selected {Boolean} only hit selected items * @option options.selected {Boolean} only hit selected items
@ -1892,7 +1901,7 @@ new function() { // Injection scope for hit-test functions shared with project
// need to apply the inverted item matrix. // need to apply the inverted item matrix.
tolerancePadding = options._tolerancePadding = new Size( tolerancePadding = options._tolerancePadding = new Size(
Path._getStrokePadding(tolerance, Path._getStrokePadding(tolerance,
matrix.inverted()._shiftless())); matrix._shiftless().invert()));
// Transform point to local coordinates. // Transform point to local coordinates.
point = matrix._inverseTransform(point); point = matrix._inverseTransform(point);
// If the matrix is non-reversible, point will now be `null`: // If the matrix is non-reversible, point will now be `null`:
@ -1924,32 +1933,39 @@ new function() { // Injection scope for hit-test functions shared with project
return hit; return hit;
} }
function checkBounds(type, part) { function checkPoint(type, part) {
var pt = bounds['get' + part](); var pt = part ? bounds['get' + part]() : that.getPosition();
// Since there are transformations, we cannot simply use a numerical // Since there are transformations, we cannot simply use a numerical
// tolerance value. Instead, we divide by a padding size, see above. // 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, return new HitResult(type, that, {
{ name: Base.hyphenate(part), point: pt }); name: part ? Base.hyphenate(part) : type,
point: pt
});
} }
} }
var checkPosition = options.position,
checkCenter = options.center,
checkBounds = options.bounds;
// Ignore top level layers by checking for _parent: // Ignore top level layers by checking for _parent:
if (checkSelf && (options.center || options.bounds) && this._parent) { if (checkSelf && this._parent
// Don't get the transformed bounds, check against transformed && (checkPosition || checkCenter || checkBounds)) {
// points instead if (checkCenter || checkBounds) {
// Get the internal, untransformed bounds, as we check against
// transformed points.
bounds = this.getInternalBounds(); bounds = this.getInternalBounds();
if (options.center) {
res = checkBounds('center', 'Center');
} }
if (!res && options.bounds) { res = checkPosition && checkPoint('position') ||
// TODO: Move these into a private scope checkCenter && checkPoint('center', 'Center');
if (!res && checkBounds) {
// TODO: Move these into a static property on Rectangle?
var points = [ var points = [
'TopLeft', 'TopRight', 'BottomLeft', 'BottomRight', 'TopLeft', 'TopRight', 'BottomLeft', 'BottomRight',
'LeftCenter', 'TopCenter', 'RightCenter', 'BottomCenter' '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 = checkPoint('bounds', points[i]);
} }
} }
res = filter(res); res = filter(res);
@ -1964,7 +1980,7 @@ new function() { // Injection scope for hit-test functions shared with project
// If the item has a non-scaling stroke, we need to // If the item has a non-scaling stroke, we need to
// apply the inverted viewMatrix to stroke dimensions. // apply the inverted viewMatrix to stroke dimensions.
this.getStrokeScaling() ? null this.getStrokeScaling() ? null
: viewMatrix.inverted()._shiftless())) : viewMatrix._shiftless().invert()))
|| null; || null;
} }
// Transform the point back to the outer coordinate system. // Transform the point back to the outer coordinate system.
@ -2243,6 +2259,14 @@ new function() { // Injection scope for hit-test functions shared with project
* @name Item#exportSVG * @name Item#exportSVG
* @function * @function
* *
* @option [options.bounds='view'] {String|Rectangle} the bounds of the area
* to export, either as a string ({@values 'view', content'}), or a
* {@link Rectangle} object: `'view'` uses the view bounds,
* `'content'` uses the stroke bounds of all content
* @option [options.matrix=paper.view.matrix] {Matrix} the matrix with which
* to transform the exported content: If `options.bounds` is set to
* `'view'`, `paper.view.matrix` is used, for all other settings of
* `options.bounds` the identity matrix is used.
* @option [options.asString=false] {Boolean} whether a SVG node or a * @option [options.asString=false] {Boolean} whether a SVG node or a
* `String` is to be returned * `String` is to be returned
* @option [options.precision=5] {Number} the amount of fractional digits in * @option [options.precision=5] {Number} the amount of fractional digits in
@ -2365,14 +2389,19 @@ new function() { // Injection scope for hit-test functions shared with project
// Remove the items from their parents first, since they might be // Remove the items from their parents first, since they might be
// inserted into their own parents, affecting indices. // inserted into their own parents, affecting indices.
// Use the loop also to filter invalid items. // Use the loop also to filter invalid items.
var inserted = {};
for (var i = items.length - 1; i >= 0; i--) { for (var i = items.length - 1; i >= 0; i--) {
var item = items[i]; var item = items[i],
if (!item) { id = item && item._id;
// If an item was inserted already, it must be included multiple
// times in the items array. Only insert once.
if (!item || inserted[id]) {
items.splice(i, 1); items.splice(i, 1);
} else { } else {
// Notify parent of change. Don't notify item itself yet, // Notify parent of change. Don't notify item itself yet,
// as we're doing so when adding it to the new owner below. // as we're doing so when adding it to the new owner below.
item._remove(false, true); item._remove(false, true);
inserted[id] = true;
} }
} }
Base.splice(children, items, index, 0); Base.splice(children, items, index, 0);
@ -2389,7 +2418,7 @@ new function() { // Injection scope for hit-test functions shared with project
if (name) if (name)
item.setName(name); item.setName(name);
if (notifySelf) if (notifySelf)
this._changed(/*#=*/Change.INSERTION); item._changed(/*#=*/Change.INSERTION);
} }
this._changed(/*#=*/Change.CHILDREN); this._changed(/*#=*/Change.CHILDREN);
} else { } else {
@ -2506,17 +2535,27 @@ new function() { // Injection scope for hit-test functions shared with project
moveBelow: '#insertBelow', moveBelow: '#insertBelow',
/** /**
* When passed a project, copies the item to the project, * Adds it to the specified owner, which can be either a {@link Item} or a
* or duplicates it within the same project. When passed an item, * {@link Project}.
* copies the item into the specified item. *
* @param {Project|Layer|Group|CompoundPath} owner the item or project to
* add the item to
* @return {Item} the item itself, if it was successfully added
*/
addTo: function(owner) {
return owner._insertItem(undefined, this);
},
/**
* Clones the item and adds it to the specified owner, which can be either
* a {@link Item} or a {@link Project}.
* *
* @param {Project|Layer|Group|CompoundPath} owner the item or project to * @param {Project|Layer|Group|CompoundPath} owner the item or project to
* copy the item to * copy the item to
* @return {Item} the new copy of the item * @return {Item} the new copy of the item, if it was successfully added
*/ */
copyTo: function(owner) { copyTo: function(owner) {
// Pass false for insert, since we're inserting at a specific location. return this.clone(false).addTo(owner);
return owner._insertItem(undefined, this.clone(false));
}, },
/** /**
@ -3342,18 +3381,19 @@ new function() { // Injection scope for hit-test functions shared with project
if (matrix && matrix.isIdentity()) if (matrix && matrix.isIdentity())
matrix = null; matrix = null;
var _matrix = this._matrix, var _matrix = this._matrix,
transform = matrix && !matrix.isIdentity(),
applyMatrix = (_applyMatrix || this._applyMatrix) applyMatrix = (_applyMatrix || this._applyMatrix)
// Don't apply _matrix if the result of concatenating with // Don't apply _matrix if the result of concatenating with
// matrix would be identity. // matrix would be identity.
&& ((!_matrix.isIdentity() || matrix) && ((!_matrix.isIdentity() || transform)
// Even if it's an identity matrix, we still need to // Even if it's an identity matrix, we still need to
// recursively apply the matrix to children. // recursively apply the matrix to children.
|| _applyMatrix && _applyRecursively && this._children); || _applyMatrix && _applyRecursively && this._children);
// Bail out if there is nothing to do. // Bail out if there is nothing to do.
if (!matrix && !applyMatrix) if (!transform && !applyMatrix)
return this; return this;
// Simply prepend the internal matrix with the passed one: // Simply prepend the internal matrix with the passed one:
if (matrix) { if (transform) {
// Keep a backup of the last valid state before the matrix becomes // Keep a backup of the last valid state before the matrix becomes
// non-invertible. This is then used again in setBounds to restore. // non-invertible. This is then used again in setBounds to restore.
if (!matrix.isInvertible() && _matrix.isInvertible()) if (!matrix.isInvertible() && _matrix.isInvertible())
@ -3364,28 +3404,39 @@ new function() { // Injection scope for hit-test functions shared with project
// internal _matrix transformations to the item's content. // internal _matrix transformations to the item's content.
// Application is not possible on Raster, PointText, SymbolItem, since // Application is not possible on Raster, PointText, SymbolItem, since
// the matrix is where the actual transformation state is stored. // the matrix is where the actual transformation state is stored.
if (applyMatrix = applyMatrix && this._transformContent(_matrix, if (applyMatrix) {
_applyRecursively, _setApplyMatrix)) { if (this._transformContent(_matrix, _applyRecursively,
// When the _matrix could be applied, we also need to transform _setApplyMatrix)) {
// color styles (only gradients so far) and pivot point: var pivot = this._pivot;
var pivot = this._pivot, if (pivot)
style = this._style, _matrix._transformPoint(pivot, pivot, true);
// pass true for _dontMerge so we don't recursively transform // Reset the internal matrix to the identity transformation if
// it was possible to apply it.
_matrix.reset(true);
// Set the internal _applyMatrix flag to true if we're told to
// do so
if (_setApplyMatrix && this._canApplyMatrix)
this._applyMatrix = true;
} else {
applyMatrix = transform = false;
}
}
if (transform) {
// When a new matrix was applied, we also need to transform gradient
// color points. These always need transforming, regardless of
// #applyMatrix, as they are defined in the parent's coordinate
// system.
// TODO: Introduce options to control whether fills should be
// transformed or not.
var style = this._style,
// Pass true for _dontMerge so we don't recursively transform
// styles on groups' children. // styles on groups' children.
fillColor = style.getFillColor(true), fillColor = style.getFillColor(true),
strokeColor = style.getStrokeColor(true); strokeColor = style.getStrokeColor(true);
if (pivot)
_matrix._transformPoint(pivot, pivot, true);
if (fillColor) if (fillColor)
fillColor.transform(_matrix); fillColor.transform(matrix);
if (strokeColor) if (strokeColor)
strokeColor.transform(_matrix); strokeColor.transform(matrix);
// Reset the internal matrix to the identity transformation if it
// was possible to apply it.
_matrix.reset(true);
// Set the internal _applyMatrix flag to true if we're told to do so
if (_setApplyMatrix && this._canApplyMatrix)
this._applyMatrix = true;
} }
// Calling _changed will clear _bounds and _position, but depending // Calling _changed will clear _bounds and _position, but depending
// on matrix we can calculate and set them again, so preserve them. // on matrix we can calculate and set them again, so preserve them.
@ -4078,12 +4129,13 @@ new function() { // Injection scope for hit-test functions shared with project
_setStyles: function(ctx, param, viewMatrix) { _setStyles: function(ctx, param, viewMatrix) {
// We can access internal properties since we're only using this on // We can access internal properties since we're only using this on
// items without children, where styles would be merged. // items without children, where styles would be merged.
var style = this._style; var style = this._style,
matrix = this._matrix;
if (style.hasFill()) { if (style.hasFill()) {
ctx.fillStyle = style.getFillColor().toCanvasStyle(ctx); ctx.fillStyle = style.getFillColor().toCanvasStyle(ctx, matrix);
} }
if (style.hasStroke()) { if (style.hasStroke()) {
ctx.strokeStyle = style.getStrokeColor().toCanvasStyle(ctx); ctx.strokeStyle = style.getStrokeColor().toCanvasStyle(ctx, matrix);
ctx.lineWidth = style.getStrokeWidth(); ctx.lineWidth = style.getStrokeWidth();
var strokeJoin = style.getStrokeJoin(), var strokeJoin = style.getStrokeJoin(),
strokeCap = style.getStrokeCap(), strokeCap = style.getStrokeCap(),
@ -4229,8 +4281,8 @@ new function() { // Injection scope for hit-test functions shared with project
// on the temporary canvas. // on the temporary canvas.
ctx.translate(-itemOffset.x, -itemOffset.y); ctx.translate(-itemOffset.x, -itemOffset.y);
} }
// Apply globalMatrix when drawing into temporary canvas.
if (transform) { if (transform) {
// Apply viewMatrix when drawing into temporary canvas.
(direct ? matrix : viewMatrix).applyToContext(ctx); (direct ? matrix : viewMatrix).applyToContext(ctx);
} }
if (clip) { if (clip) {

View file

@ -268,7 +268,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
/** /**
* @bean * @bean
* @deprecated use {@link #getSymbolDefinitions()} instead. * @deprecated use {@link #symbolDefinitions} instead.
*/ */
getSymbols: 'getSymbolDefinitions', getSymbols: 'getSymbolDefinitions',
@ -287,7 +287,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
for (var id in selectionItems) { for (var id in selectionItems) {
var item = selectionItems[id], var item = selectionItems[id],
selection = item._selection; selection = item._selection;
if (selection & /*#=*/ItemSelection.ITEM && item.isInserted()) { if ((selection & /*#=*/ItemSelection.ITEM) && item.isInserted()) {
items.push(item); items.push(item);
} else if (!selection) { } else if (!selection) {
this._updateSelection(item); this._updateSelection(item);
@ -424,10 +424,12 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* Segment#handleIn} / {@link Segment#handleOut}) of path segments. * Segment#handleIn} / {@link Segment#handleOut}) of path segments.
* @option options.ends {Boolean} only hit-test for the first or last * @option options.ends {Boolean} only hit-test for the first or last
* segment points of open path items * segment points of open path items
* @option options.bounds {Boolean} hit-test the corners and side-centers of * @option options.position {Boolean} hit-test the {@link Item#position} of
* the bounding rectangle of items ({@link Item#bounds}) * of items, which depends on the setting of {@link Item#pivot}
* @option options.center {Boolean} hit-test the {@link Rectangle#center} of * @option options.center {Boolean} hit-test the {@link Rectangle#center} of
* the bounding rectangle of items ({@link Item#bounds}) * the bounding rectangle of items ({@link Item#bounds})
* @option options.bounds {Boolean} hit-test the corners and side-centers of
* the bounding rectangle of items ({@link Item#bounds})
* @option options.guides {Boolean} hit-test items that have {@link * @option options.guides {Boolean} hit-test items that have {@link
* Item#guide} set to `true` * Item#guide} set to `true`
* @option options.selected {Boolean} only hit selected items * @option options.selected {Boolean} only hit selected items
@ -758,6 +760,14 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* @name Project#exportSVG * @name Project#exportSVG
* @function * @function
* *
* @option [options.bounds='view'] {String|Rectangle} the bounds of the area
* to export, either as a string ({@values 'view', content'}), or a
* {@link Rectangle} object: `'view'` uses the view bounds,
* `'content'` uses the stroke bounds of all content
* @option [options.matrix=paper.view.matrix] {Matrix} the matrix with which
* to transform the exported content: If `options.bounds` is set to
* `'view'`, `paper.view.matrix` is used, for all other settings of
* `options.bounds` the identity matrix is used.
* @option [options.asString=false] {Boolean} whether a SVG node or a * @option [options.asString=false] {Boolean} whether a SVG node or a
* `String` is to be returned * `String` is to be returned
* @option [options.precision=5] {Number} the amount of fractional digits in * @option [options.precision=5] {Number} the amount of fractional digits in

View file

@ -219,7 +219,7 @@ var Raster = Item.extend(/** @lends Raster# */{
/** /**
* @private * @private
* @bean * @bean
* @deprecated use {@link #getResolution()} instead. * @deprecated use {@link #resolution} instead.
*/ */
getPpi: '#getResolution', getPpi: '#getResolution',

View file

@ -28,8 +28,8 @@ var Shape = Item.extend(/** @lends Shape# */{
radius: null radius: null
}, },
initialize: function Shape(props) { initialize: function Shape(props, point) {
this._initialize(props); this._initialize(props, point);
}, },
_equals: function(item) { _equals: function(item) {
@ -63,7 +63,7 @@ var Shape = Item.extend(/** @lends Shape# */{
/** /**
* @private * @private
* @bean * @bean
* @deprecated use {@link #getType()} instead. * @deprecated use {@link #type} instead.
*/ */
getShape: '#getType', getShape: '#getType',
setShape: '#setType', setShape: '#setType',
@ -187,6 +187,10 @@ var Shape = Item.extend(/** @lends Shape# */{
toShape: '#clone', toShape: '#clone',
_asPathItem: function() {
return this.toPath(false);
},
_draw: function(ctx, param, viewMatrix, strokeMatrix) { _draw: function(ctx, param, viewMatrix, strokeMatrix) {
var style = this._style, var style = this._style,
hasFill = style.hasFill(), hasFill = style.hasFill(),
@ -386,11 +390,11 @@ new function() { // Scope for _contains() and _hitTestSelf() code.
// Mess with indentation in order to get more line-space below: // Mess with indentation in order to get more line-space below:
statics: new function() { statics: new function() {
function createShape(type, point, size, radius, args) { function createShape(type, point, size, radius, args) {
var item = new Shape(Base.getNamed(args)); var item = new Shape(Base.getNamed(args), point);
item._type = type; item._type = type;
item._size = size; item._size = size;
item._radius = radius; item._radius = radius;
return item.translate(point); return item;
} }
return /** @lends Shape */{ return /** @lends Shape */{

View file

@ -122,7 +122,7 @@ var SymbolDefinition = Base.extend(/** @lends SymbolDefinition# */{
/** /**
* @bean * @bean
* @deprecated use {@link #getItem()} instead. * @deprecated use {@link #item} instead.
*/ */
getDefinition: '#getItem', getDefinition: '#getItem',
setDefinition: '#setItem', setDefinition: '#setItem',

View file

@ -103,7 +103,7 @@ var SymbolItem = Item.extend(/** @lends SymbolItem# */{
/** /**
* @bean * @bean
* @deprecated use {@link #getDefinition()} instead. * @deprecated use {@link #definition} instead.
*/ */
getSymbol: '#getDefinition', getSymbol: '#getDefinition',
setSymbol: '#setDefinition', setSymbol: '#setDefinition',
@ -118,7 +118,7 @@ var SymbolItem = Item.extend(/** @lends SymbolItem# */{
return item._getCachedBounds(item._matrix.prepended(matrix), options); return item._getCachedBounds(item._matrix.prepended(matrix), options);
}, },
_hitTestSelf: function(point, options, viewMatrix, strokeMatrix) { _hitTestSelf: function(point, options, viewMatrix) {
var res = this._definition._item._hitTest(point, options, viewMatrix); var res = this._definition._item._hitTest(point, options, viewMatrix);
// TODO: When the symbol's definition is a path, should hitResult // TODO: When the symbol's definition is a path, should hitResult
// contain information like HitResult#curve? // contain information like HitResult#curve?

View file

@ -43,7 +43,7 @@ if (typeof window === 'object') {
include('../node_modules/stats.js/build/stats.min.js'); include('../node_modules/stats.js/build/stats.min.js');
include('paper.js'); include('paper.js');
} }
} else { } else if (typeof require !== 'undefined') {
// Node.js based loading through Prepro.js: // Node.js based loading through Prepro.js:
var prepro = require('prepro/lib/node'), var prepro = require('prepro/lib/node'),
// Load the default browser-based options for further amendments. // Load the default browser-based options for further amendments.

View file

@ -15,7 +15,7 @@
// - Various Node Canvas methods, routed through from HTMLCanvasElement: // - Various Node Canvas methods, routed through from HTMLCanvasElement:
// toBuffer, pngStream, createPNGStream, jpgStream, createJPGStream // toBuffer, pngStream, createPNGStream, jpgStream, createJPGStream
module.exports = function(window) { module.exports = function(self, requireName) {
var Canvas; var Canvas;
try { try {
Canvas = require('canvas'); Canvas = require('canvas');
@ -25,14 +25,17 @@ module.exports = function(window) {
// - On the browser, this corresponds to a worker context. // - On the browser, this corresponds to a worker context.
// - On Node.js, it basically means the canvas is missing or not working // - On Node.js, it basically means the canvas is missing or not working
// which can be treated the same way. // which can be treated the same way.
delete window.window; delete self.window;
console.info( // Check the required module's name to see if it contains canvas, and
'Unable to load Canvas module. Running in a headless context.'); // only complain about its lack if the module requires it.
if (/\bcanvas\b/.test(requireName)) {
throw new Error('Unable to load canvas module.');
}
return; return;
} }
var idlUtils = require('jsdom/lib/jsdom/living/generated/utils'), var HTMLCanvasElement = self.HTMLCanvasElement,
HTMLCanvasElement = window.HTMLCanvasElement; idlUtils = require('jsdom/lib/jsdom/living/generated/utils');
// Add fake HTMLCanvasElement#type property: // Add fake HTMLCanvasElement#type property:
Object.defineProperty(HTMLCanvasElement.prototype, 'type', { Object.defineProperty(HTMLCanvasElement.prototype, 'type', {

View file

@ -57,8 +57,8 @@ module.exports = function(paper) {
paper.PaperScope.inject({ paper.PaperScope.inject({
createCanvas: function(width, height, type) { createCanvas: function(width, height, type) {
// Do not use CanvasProvider.getCanvas(), since we may be changing // Do not use CanvasProvider.getCanvas(), since we may be changing
// the underlying node-canvas and don't want to release it after // the underlying node-canvas when requesting PDF support, and don't
// back into the pool. // want to release it after back into the pool.
var canvas = paper.document.createElement('canvas'); var canvas = paper.document.createElement('canvas');
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;

60
src/node/self.js Normal file
View file

@ -0,0 +1,60 @@
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
// Node.js emulation layer of browser environment, based on jsdom with node-
// canvas integration.
var path = require('path');
// Determine the name by which name the module was required (either 'paper',
// 'paper-jsdom' or 'paper-jsdom-canvas'), and use this to determine if error
// exceptions should be thrown or if loading should fail silently.
var parent = module.parent.parent,
requireName = parent && path.basename(path.dirname(parent.filename));
requireName = /^paper/.test(requireName) ? requireName : 'paper';
var jsdom,
self;
try {
jsdom = require('jsdom');
} catch(e) {
// Check the required module's name to see if it contains jsdom, and only
// complain about its lack if the module requires it.
if (/\bjsdom\b/.test(requireName)) {
throw new Error('Unable to load jsdom module.');
}
}
if (jsdom) {
// Create our document and window objects through jsdom.
/* global document:true, window:true */
var document = jsdom.jsdom('<html><body></body></html>', {
// Use the current working directory as the document's origin, so
// requests to local files work correctly with CORS.
url: 'file://' + process.cwd() + '/',
features: {
FetchExternalResources: ['img', 'script']
}
});
self = document.defaultView;
require('./canvas.js')(self, requireName);
require('./xml.js')(self);
} else {
self = {
navigator: {
userAgent: 'Node.js (' + process.platform + '; U; rv:' +
process.version + ')'
}
};
}
module.exports = self;

View file

@ -1,57 +0,0 @@
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
// Node.js emulation layer of browser environment, based on jsdom with node-
// canvas integration.
var jsdom = require('jsdom');
// Create our document and window objects through jsdom.
/* global document:true, window:true */
var document = jsdom.jsdom('<html><body></body></html>', {
// Use the current working directory as the document's origin, so
// requests to local files work correctly with CORS.
url: 'file://' + process.cwd() + '/',
features: {
FetchExternalResources: ['img', 'script']
}
}),
window = document.defaultView;
require('./canvas.js')(window);
// Define XMLSerializer and DOMParser shims, to emulate browser behavior.
// Effort to bring this to jsdom: https://github.com/tmpvar/jsdom/issues/1368
function XMLSerializer() {
}
XMLSerializer.prototype.serializeToString = function(node) {
if (!node)
return '';
var text = node.outerHTML;
// Fix a jsdom issue where all SVG tagNames are lowercased:
// https://github.com/tmpvar/jsdom/issues/620
var tagNames = ['linearGradient', 'radialGradient', 'clipPath', 'textPath'];
for (var i = 0, l = tagNames.length; i < l; i++) {
var tagName = tagNames[i];
text = text.replace(
new RegExp('(<|</)' + tagName.toLowerCase() + '\\b', 'g'),
function(match, start) {
return start + tagName;
});
}
return text;
};
window.XMLSerializer = XMLSerializer;
module.exports = window;

40
src/node/xml.js Normal file
View file

@ -0,0 +1,40 @@
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
* http://paperjs.org/
*
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
* http://scratchdisk.com/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
module.exports = function(self) {
// Define XMLSerializer shim, to emulate browser behavior.
// Effort to bring XMLSerializer to jsdom:
// https://github.com/tmpvar/jsdom/issues/1368
self.XMLSerializer = function XMLSerializer() {
};
self.XMLSerializer.prototype = {
serializeToString: function(node) {
if (!node)
return '';
// Fix a jsdom issue where all SVG tagNames are lowercased:
// https://github.com/tmpvar/jsdom/issues/620
var text = node.outerHTML,
tagNames = ['linearGradient', 'radialGradient', 'clipPath',
'textPath'];
for (var i = 0, l = tagNames.length; i < l; i++) {
var tagName = tagNames[i];
text = text.replace(
new RegExp('(<|</)' + tagName.toLowerCase() + '\\b', 'g'),
function(match, start) {
return start + tagName;
});
}
return text;
}
};
};

View file

@ -17,7 +17,7 @@
// The paper.js version. // The paper.js version.
// NOTE: Adjust value here before calling `gulp publish`, which then updates and // NOTE: Adjust value here before calling `gulp publish`, which then updates and
// publishes the various JSON package files automatically. // publishes the various JSON package files automatically.
var version = '0.10.3'; var version = '0.10.4';
// If this file is loaded in the browser, we're in load.js mode. // If this file is loaded in the browser, we're in load.js mode.
var load = typeof window === 'object'; var load = typeof window === 'object';

View file

@ -181,7 +181,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The first Segment contained within the compound-path, a short-cut to * The first Segment contained within the compound-path, a short-cut to
* calling {@link Path#getFirstSegment()} on {@link Item#getFirstChild()}. * calling {@link Path#firstSegment} on {@link Item#firstChild}.
* *
* @bean * @bean
* @type Segment * @type Segment
@ -193,7 +193,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The last Segment contained within the compound-path, a short-cut to * The last Segment contained within the compound-path, a short-cut to
* calling {@link Path#getLastSegment()} on {@link Item#getLastChild()}. * calling {@link Path#lastChild} on {@link Item#lastChild}.
* *
* @bean * @bean
* @type Segment * @type Segment
@ -220,7 +220,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The first Curve contained within the compound-path, a short-cut to * The first Curve contained within the compound-path, a short-cut to
* calling {@link Path#getFirstCurve()} on {@link Item#getFirstChild()}. * calling {@link Path#firstCurve} on {@link Item#firstChild}.
* *
* @bean * @bean
* @type Curve * @type Curve
@ -232,7 +232,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The last Curve contained within the compound-path, a short-cut to * The last Curve contained within the compound-path, a short-cut to
* calling {@link Path#getLastCurve()} on {@link Item#getLastChild()}. * calling {@link Path#lastCurve} on {@link Item#lastChild}.
* *
* @bean * @bean
* @type Curve * @type Curve
@ -244,7 +244,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The area that the compound-path's geometry is covering, calculated by * The area that the compound-path's geometry is covering, calculated by
* getting th e{@link Path#getArea()} of each sub-path and it adding up. * getting the {@link Path#area} of each sub-path and it adding up.
* Note that self-intersecting paths and sub-paths of different orientation * Note that self-intersecting paths and sub-paths of different orientation
* can result in areas that cancel each other out. * can result in areas that cancel each other out.
* *
@ -261,7 +261,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
/** /**
* The total length of all sub-paths in this compound-path, calculated by * The total length of all sub-paths in this compound-path, calculated by
* getting the {@link Path#getLength()} of each sub-path and it adding up. * getting the {@link Path#length} of each sub-path and it adding up.
* *
* @bean * @bean
* @type Number * @type Number

View file

@ -632,10 +632,9 @@ statics: /** @lends Curve */{
* Splits the specified curve values into curves that are monotone in the * Splits the specified curve values into curves that are monotone in the
* specified coordinate direction. * specified coordinate direction.
* *
* @param {Number[]} v the curve values, as returned by * @param {Number[]} v the curve values, as returned by {@link Curve#values}
* {@link Curve#getValues()} * @param {Boolean} [dir=false] the direction in which the curves should be
* @param {Number} [dir=0] the direction in which the curves should be * monotone, `false`: in x-direction, `true`: in y-direction
* monotone, `0`: monotone in x-direction, `1`: monotone in y-direction
* @return {Number[][]} an array of curve value arrays of the resulting * @return {Number[][]} an array of curve value arrays of the resulting
* monotone curve. If the original curve was already monotone, an array * monotone curve. If the original curve was already monotone, an array
* only containing its values are returned. * only containing its values are returned.
@ -1716,17 +1715,18 @@ new function() { // Scope for methods that require private functions
}, },
new function() { // Scope for bezier intersection using fat-line clipping new function() { // Scope for bezier intersection using fat-line clipping
function addLocation(locations, include, c1, t1, p1, c2, t2, p2, overlap) { function addLocation(locations, include, c1, t1, c2, t2, overlap) {
// Determine if locations at the beginning / end of the curves should be // Determine if locations at the beginning / end of the curves should be
// excluded, in case the two curves are neighbors, but do not exclude // excluded, in case the two curves are neighbors, but do not exclude
// connecting points between two curves if they were part of overlap // connecting points between two curves if they were part of overlap
// checks, as they could be self-overlapping. // checks, as they could be self-overlapping.
// NOTE: We don't pass p1 and p2, because v1 and v2 may be transformed
// by their path.matrix, while c1 and c2 are untransformed. Passing null
// for point in CurveLocation() will do the right thing.
var excludeStart = !overlap && c1.getPrevious() === c2, var excludeStart = !overlap && c1.getPrevious() === c2,
excludeEnd = !overlap && c1 !== c2 && c1.getNext() === c2, excludeEnd = !overlap && c1 !== c2 && c1.getNext() === c2,
tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
if (t1 == null)
t1 = c1.getTimeOf(p1);
// Check t1 and t2 against correct bounds, based on excludeStart/End: // Check t1 and t2 against correct bounds, based on excludeStart/End:
// - excludeStart means the start of c1 connects to the end of c2 // - excludeStart means the start of c1 connects to the end of c2
// - excludeEnd means the end of c1 connects to the start of c2 // - excludeEnd means the end of c1 connects to the start of c2
@ -1736,14 +1736,10 @@ new function() { // Scope for bezier intersection using fat-line clipping
// the beginning, and would be added twice otherwise. // the beginning, and would be added twice otherwise.
if (t1 !== null && t1 >= (excludeStart ? tMin : 0) && if (t1 !== null && t1 >= (excludeStart ? tMin : 0) &&
t1 <= (excludeEnd ? tMax : 1)) { t1 <= (excludeEnd ? tMax : 1)) {
if (t2 == null)
t2 = c2.getTimeOf(p2);
if (t2 !== null && t2 >= (excludeEnd ? tMin : 0) && if (t2 !== null && t2 >= (excludeEnd ? tMin : 0) &&
t2 <= (excludeStart ? tMax : 1)) { t2 <= (excludeStart ? tMax : 1)) {
var loc1 = new CurveLocation(c1, t1, var loc1 = new CurveLocation(c1, t1, null, overlap),
p1 || c1.getPointAtTime(t1), overlap), loc2 = new CurveLocation(c2, t2, null, overlap);
loc2 = new CurveLocation(c2, t2,
p2 || c2.getPointAtTime(t2), overlap);
// Link the two locations to each other. // Link the two locations to each other.
loc1._intersection = loc2; loc1._intersection = loc2;
loc2._intersection = loc1; loc2._intersection = loc1;
@ -1806,8 +1802,8 @@ new function() { // Scope for bezier intersection using fat-line clipping
var t = (tMinNew + tMaxNew) / 2, var t = (tMinNew + tMaxNew) / 2,
u = (uMin + uMax) / 2; u = (uMin + uMax) / 2;
addLocation(locations, include, addLocation(locations, include,
flip ? c2 : c1, flip ? u : t, null, flip ? c2 : c1, flip ? u : t,
flip ? c1 : c2, flip ? t : u, null); flip ? c1 : c2, flip ? t : u);
} else { } else {
// Apply the result of the clipping to curve 1: // Apply the result of the clipping to curve 1:
v1 = Curve.getPart(v1, tMinClip, tMaxClip); v1 = Curve.getPart(v1, tMinClip, tMaxClip);
@ -1989,12 +1985,11 @@ new function() { // Scope for bezier intersection using fat-line clipping
p1 = Curve.getPoint(v1, t1), p1 = Curve.getPoint(v1, t1),
t2 = Curve.getTimeOf(v2, p1); t2 = Curve.getTimeOf(v2, p1);
if (t2 !== null) { if (t2 !== null) {
var p2 = Curve.getPoint(v2, t2);
// Only use the time values if there was no recursion, and let // Only use the time values if there was no recursion, and let
// addLocation() figure out the actual time values otherwise. // addLocation() figure out the actual time values otherwise.
addLocation(locations, include, addLocation(locations, include,
flip ? c2 : c1, flip ? t2 : t1, flip ? p2 : p1, flip ? c2 : c1, flip ? t2 : t1,
flip ? c1 : c2, flip ? t1 : t2, flip ? p1 : p2); flip ? c1 : c2, flip ? t1 : t2);
} }
} }
} }
@ -2004,7 +1999,9 @@ new function() { // Scope for bezier intersection using fat-line clipping
v1[0], v1[1], v1[6], v1[7], v1[0], v1[1], v1[6], v1[7],
v2[0], v2[1], v2[6], v2[7]); v2[0], v2[1], v2[6], v2[7]);
if (pt) { if (pt) {
addLocation(locations, include, c1, null, pt, c2, null, pt); addLocation(locations, include,
c1, Curve.getTimeOf(v1, pt),
c2, Curve.getTimeOf(v2, pt));
} }
} }
@ -2028,14 +2025,15 @@ new function() { // Scope for bezier intersection using fat-line clipping
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
var overlap = overlaps[i]; var overlap = overlaps[i];
addLocation(locations, include, addLocation(locations, include,
c1, overlap[0], null, c1, overlap[0],
c2, overlap[1], null, true); c2, overlap[1], true);
} }
} else { } else {
var straight1 = Curve.isStraight(v1), var straight1 = Curve.isStraight(v1),
straight2 = Curve.isStraight(v2), straight2 = Curve.isStraight(v2),
straight = straight1 && straight2, straight = straight1 && straight2,
flip = straight1 && !straight2; flip = straight1 && !straight2,
before = locations.length;
// Determine the correct intersection method based on whether // Determine the correct intersection method based on whether
// one or curves are straight lines: // one or curves are straight lines:
(straight (straight
@ -2050,6 +2048,25 @@ new function() { // Scope for bezier intersection using fat-line clipping
// addCurveIntersections(): // addCurveIntersections():
// recursion, calls, tMin, tMax, uMin, uMax // recursion, calls, tMin, tMax, uMin, uMax
0, 0, 0, 1, 0, 1); 0, 0, 0, 1, 0, 1);
// Handle the special case where the first curve's start- / end-
// point overlaps with the second curve's start- / end-point,
// but only if haven't found a line-line intersection already:
// #805#issuecomment-148503018
if (!straight || locations.length === before) {
for (var i = 0; i < 4; i++) {
var t1 = i >> 1, // 0, 0, 1, 1
t2 = i & 1, // 0, 1, 0, 1
i1 = t1 * 6,
i2 = t2 * 6,
p1 = new Point(v1[i1], v1[i1 + 1]),
p2 = new Point(v2[i2], v2[i2 + 1]);
if (p1.isClose(p2, epsilon)) {
addLocation(locations, include,
c1, t1,
c2, t2);
}
}
}
} }
} }
return locations; return locations;
@ -2060,8 +2077,8 @@ new function() { // Scope for bezier intersection using fat-line clipping
if (info.type === 'loop') { if (info.type === 'loop') {
var roots = info.roots; var roots = info.roots;
addLocation(locations, include, addLocation(locations, include,
c1, roots[0], null, c1, roots[0],
c1, roots[1], null); c1, roots[1]);
} }
return locations; return locations;
} }

View file

@ -185,7 +185,7 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{
/** /**
* @private * @private
* @bean * @bean
* @deprecated use {@link #getTime()} instead. * @deprecated use {@link #time} instead.
*/ */
getParameter: '#getTime', getParameter: '#getTime',

View file

@ -22,13 +22,13 @@ Path.inject({ statics: new function() {
function createPath(segments, closed, args) { function createPath(segments, closed, args) {
var props = Base.getNamed(args), var props = Base.getNamed(args),
path = new Path(props && props.insert === false && Item.NO_INSERT); path = new Path(props && props.insert == false && Item.NO_INSERT);
path._add(segments); path._add(segments);
// No need to use setter for _closed since _add() called _changed(). // No need to use setter for _closed since _add() called _changed().
path._closed = closed; path._closed = closed;
// Set named arguments at the end, since some depend on geometry to be // Set named arguments at the end, since some depend on geometry to be
// defined (e.g. #clockwise) // defined (e.g. #clockwise)
return path.set(props); return path.set(props, { insert: true });
} }
function createEllipse(center, radius, args) { function createEllipse(center, radius, args) {
@ -335,7 +335,7 @@ Path.inject({ statics: new function() {
to = Point.readNamed(arguments, 'to'), to = Point.readNamed(arguments, 'to'),
props = Base.getNamed(arguments), props = Base.getNamed(arguments),
// See createPath() for an explanation of the following sequence // See createPath() for an explanation of the following sequence
path = new Path(props && props.insert === false path = new Path(props && props.insert == false
&& Item.NO_INSERT); && Item.NO_INSERT);
path.moveTo(from); path.moveTo(from);
path.arcTo(through, to); path.arcTo(through, to);

View file

@ -1721,7 +1721,7 @@ var Path = PathItem.extend(/** @lends Path# */{
} }
function checkSegmentStroke(segment) { function checkSegmentStroke(segment) {
// Handle joins / caps that are not round specificelly, by // Handle joins / caps that are not round specifically, by
// hit-testing their polygon areas. // hit-testing their polygon areas.
var isJoin = closed || segment._index > 0 var isJoin = closed || segment._index > 0
&& segment._index < numSegments - 1; && segment._index < numSegments - 1;
@ -2523,7 +2523,7 @@ new function() { // PostScript-style drawing commands
extent += extent < 0 ? 360 : -360; extent += extent < 0 ? 360 : -360;
} }
} }
var epsilon = /*#=*/Numerical.EPSILON, var epsilon = /*#=*/Numerical.GEOMETRIC_EPSILON,
ext = abs(extent), ext = abs(extent),
// Calculate the amount of segments required to approximate over // Calculate the amount of segments required to approximate over
// `extend` degrees (extend / 90), but prevent ceil() from // `extend` degrees (extend / 90), but prevent ceil() from
@ -2834,19 +2834,20 @@ statics: {
// Style#strokeScaling. // Style#strokeScaling.
var point = segment._point.transform(matrix), var point = segment._point.transform(matrix),
loc = segment.getLocation(), loc = segment.getLocation(),
normal = loc.getNormal().multiply(radius).transform(strokeMatrix); // Checking loc.getTime() for 0 is to see whether this is the first
// or the last segment of the open path, in order to determine in
// which direction to flip the normal.
normal = loc.getNormal()
.multiply(loc.getTime() === 0 ? radius : -radius)
.transform(strokeMatrix);
// For square caps, we need to step away from point in the direction of // For square caps, we need to step away from point in the direction of
// the tangent, which is the rotated normal. // the tangent, which is the rotated normal.
// Checking loc.getTime() for 0 is to see whether this is the first
// or the last segment of the open path, in order to determine in which
// direction to move the point.
if (cap === 'square') { if (cap === 'square') {
if (isArea) { if (isArea) {
addPoint(point.subtract(normal)); addPoint(point.subtract(normal));
addPoint(point.add(normal)); addPoint(point.add(normal));
} }
point = point.add(normal.rotate( point = point.add(normal.rotate(-90));
loc.getTime() === 0 ? -90 : 90));
} }
addPoint(point.add(normal)); addPoint(point.add(normal));
addPoint(point.subtract(normal)); addPoint(point.subtract(normal));

View file

@ -61,13 +61,12 @@ PathItem.inject(new function() {
: res; : res;
} }
function createResult(ctor, paths, reduce, path1, path2, options) { function createResult(paths, simplify, path1, path2, options) {
var result = new ctor(Item.NO_INSERT); var result = new CompoundPath(Item.NO_INSERT);
result.addChildren(paths, true); result.addChildren(paths, true);
// See if the item can be reduced to just a simple Path. // See if the item can be reduced to just a simple Path.
if (reduce) result = result.reduce({ simplify: simplify });
result = result.reduce({ simplify: true }); if (!(options && options.insert == false)) {
if (!(options && options.insert === false)) {
// Insert the resulting path above whichever of the two paths appear // Insert the resulting path above whichever of the two paths appear
// further up in the stack. // further up in the stack.
result.insertAbove(path2 && path1.isSibling(path2) result.insertAbove(path2 && path1.isSibling(path2)
@ -78,12 +77,12 @@ PathItem.inject(new function() {
return result; return result;
} }
function computeBoolean(path1, path2, operation, options) { function traceBoolean(path1, path2, operation, options) {
// Only support subtract and intersect operations when computing stroke // Only support subtract and intersect operations when computing stroke
// based boolean operations. // based boolean operations (options.split = true).
if (options && options.stroke && if (options && (options.trace == false || options.stroke) &&
/^(subtract|intersect)$/.test(operation)) /^(subtract|intersect)$/.test(operation))
return computeStrokeBoolean(path1, path2, operation === 'subtract'); return splitBoolean(path1, path2, operation);
// We do not modify the operands themselves, but create copies instead, // We do not modify the operands themselves, but create copies instead,
// fas produced by the calls to preparePath(). // fas produced by the calls to preparePath().
// NOTE: The result paths might not belong to the same type i.e. // NOTE: The result paths might not belong to the same type i.e.
@ -157,22 +156,26 @@ PathItem.inject(new function() {
}); });
} }
return createResult(CompoundPath, paths, true, path1, path2, options); return createResult(paths, true, path1, path2, options);
} }
function computeStrokeBoolean(path1, path2, subtract) { function splitBoolean(path1, path2, operation) {
var _path1 = preparePath(path1), var _path1 = preparePath(path1),
_path2 = preparePath(path2), _path2 = preparePath(path2),
crossings = _path1.getCrossings(_path2), crossings = _path1.getCrossings(_path2),
subtract = operation === 'subtract',
divide = operation === 'divide',
added = {},
paths = []; paths = [];
function addPath(path) { function addPath(path) {
// Simple see if the point halfway across the open path is inside // Simple see if the point halfway across the open path is inside
// path2, and include / exclude the path based on the operator. // path2, and include / exclude the path based on the operator.
if (_path2.contains(path.getPointAt(path.getLength() / 2)) if (!added[path._id] && (divide ||
^ subtract) { _path2.contains(path.getPointAt(path.getLength() / 2))
^ subtract)) {
paths.unshift(path); paths.unshift(path);
return true; return added[path._id] = true;
} }
} }
@ -190,9 +193,9 @@ PathItem.inject(new function() {
_path1.getLastSegment().setHandleOut(0, 0); _path1.getLastSegment().setHandleOut(0, 0);
} }
} }
// At the end, check what's left from our path after all the splitting. // At the end, add what's left from our path after all the splitting.
addPath(_path1); addPath(_path1);
return createResult(Group, paths, false, path1, path2); return createResult(paths, false, path1, path2);
} }
/* /*
@ -450,10 +453,10 @@ PathItem.inject(new function() {
* @param {Point} point the location for which to determine the winding * @param {Point} point the location for which to determine the winding
* contribution * contribution
* @param {Curve[]} curves the curves that describe the shape against which * @param {Curve[]} curves the curves that describe the shape against which
* to check, as returned by {@link Path#getCurves()} or * to check, as returned by {@link Path#curves} or
* {@link CompoundPath#getCurves()} * {@link CompoundPath#curves}
* @param {Number} [dir=0] the direction in which to determine the * @param {Boolean} [dir=false] the direction in which to determine the
* winding contribution, `0`: in x-direction, `1`: in y-direction * winding contribution, `false`: in x-direction, `true`: in y-direction
* @param {Boolean} [closed=false] determines how areas should be closed * @param {Boolean} [closed=false] determines how areas should be closed
* when a curve is part of an open path, `false`: area is closed with a * when a curve is part of an open path, `false`: area is closed with a
* straight line, `true`: area is closed taking the handles of the first * straight line, `true`: area is closed taking the handles of the first
@ -479,7 +482,10 @@ PathItem.inject(new function() {
paR = pa + windingEpsilon, paR = pa + windingEpsilon,
windingL = 0, windingL = 0,
windingR = 0, windingR = 0,
pathWindingL = 0,
pathWindingR = 0,
onPath = false, onPath = false,
onAnyPath = false,
quality = 1, quality = 1,
roots = [], roots = [],
vPrev, vPrev,
@ -512,13 +518,17 @@ PathItem.inject(new function() {
// Bail out without updating vPrev at the end of the call. // Bail out without updating vPrev at the end of the call.
return; return;
} }
// Determine the curve-time value corresponding to the point.
var t = po === o0 ? 0 var t = po === o0 ? 0
: po === o3 ? 1 : po === o3 ? 1
// If the abscissa is outside the curve, we can use any
// value except 0 (requires special handling). Use 1, as it
// does not require additional calculations for the point.
: paL > max(a0, a1, a2, a3) || paR < min(a0, a1, a2, a3) : paL > max(a0, a1, a2, a3) || paR < min(a0, a1, a2, a3)
? 1 ? 1
: Curve.solveCubic(v, io, po, roots, 0, 1) === 1 : Curve.solveCubic(v, io, po, roots, 0, 1) > 0
? roots[0] ? roots[0]
: 0.5, : 1,
a = t === 0 ? a0 a = t === 0 ? a0
: t === 1 ? a3 : t === 1 ? a3
: Curve.getPoint(v, t)[dir ? 'y' : 'x'], : Curve.getPoint(v, t)[dir ? 'y' : 'x'],
@ -528,9 +538,9 @@ PathItem.inject(new function() {
if (po !== o0) { if (po !== o0) {
// Standard case, curve is not crossed at its starting point. // Standard case, curve is not crossed at its starting point.
if (a < paL) { if (a < paL) {
windingL += winding; pathWindingL += winding;
} else if (a > paR) { } else if (a > paR) {
windingR += winding; pathWindingR += winding;
} else { } else {
onPath = true; onPath = true;
} }
@ -541,13 +551,13 @@ PathItem.inject(new function() {
if (a > pa - qualityEpsilon && a < pa + qualityEpsilon) if (a > pa - qualityEpsilon && a < pa + qualityEpsilon)
quality /= 2; quality /= 2;
} else { } else {
// Curve is crossed at starting point.
if (winding !== windingPrev) { if (winding !== windingPrev) {
// Curve is crossed at starting point and winding changes // Winding changes from previous curve, cancel its winding.
// from previous curve. Cancel winding from previous curve.
if (a0 < paL) { if (a0 < paL) {
windingL += winding; pathWindingL += winding;
} else if (a0 > paR) { } else if (a0 > paR) {
windingR += winding; pathWindingR += winding;
} }
} else if (a0 != a3Prev) { } else if (a0 != a3Prev) {
// Handle a horizontal curve between the current and // Handle a horizontal curve between the current and
@ -555,11 +565,11 @@ PathItem.inject(new function() {
// #1261#issuecomment-282726147 for a detailed explanation: // #1261#issuecomment-282726147 for a detailed explanation:
if (a3Prev < paR && a > paR) { if (a3Prev < paR && a > paR) {
// Right winding was not added before, so add it now. // Right winding was not added before, so add it now.
windingR += winding; pathWindingR += winding;
onPath = true; onPath = true;
} else if (a3Prev > paL && a < paL) { } else if (a3Prev > paL && a < paL) {
// Left winding was not added before, so add it now. // Left winding was not added before, so add it now.
windingL += winding; pathWindingL += winding;
onPath = true; onPath = true;
} }
} }
@ -574,7 +584,7 @@ PathItem.inject(new function() {
// again with flipped direction and return that result instead. // again with flipped direction and return that result instead.
return !dontFlip && a > paL && a < paR return !dontFlip && a > paL && a < paR
&& Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0 && Curve.getTangent(v, t)[dir ? 'x' : 'y'] === 0
&& getWinding(point, curves, dir ? 0 : 1, closed, true); && getWinding(point, curves, !dir, closed, true);
} }
function handleCurve(v) { function handleCurve(v) {
@ -660,6 +670,23 @@ PathItem.inject(new function() {
// it now to treat the path as closed: // it now to treat the path as closed:
if (vClose && (res = handleCurve(vClose))) if (vClose && (res = handleCurve(vClose)))
return res; return res;
if (onPath && !pathWindingL && !pathWindingR) {
// If the point is on the path and the windings canceled
// each other, we treat the point as if it was inside the
// path. A point inside a path has a winding of [+1,-1]
// for clockwise and [-1,+1] for counter-clockwise paths.
// If the ray is cast in y direction (dir == true), the
// windings always have opposite sign.
pathWindingL = pathWindingR = path.isClockwise(closed) ^ dir
? 1 : -1;
}
windingL += pathWindingL;
windingR += pathWindingR;
pathWindingL = pathWindingR = 0;
if (onPath) {
onAnyPath = true;
onPath = false;
}
vClose = null; vClose = null;
} }
} }
@ -674,7 +701,7 @@ PathItem.inject(new function() {
windingL: windingL, windingL: windingL,
windingR: windingR, windingR: windingR,
quality: quality, quality: quality,
onPath: onPath onPath: onAnyPath
}; };
} }
@ -698,8 +725,7 @@ PathItem.inject(new function() {
// sufficient quality is found, use it. Otherwise use the winding with // sufficient quality is found, use it. Otherwise use the winding with
// the best quality. // the best quality.
var offsets = [0.5, 0.25, 0.75], var offsets = [0.5, 0.25, 0.75],
windingZero = { winding: 0, quality: 0 }, winding = { winding: 0, quality: -1 },
winding = windingZero,
tMin = /*#=*/Numerical.CURVETIME_EPSILON, tMin = /*#=*/Numerical.CURVETIME_EPSILON,
tMax = 1 - tMin; tMax = 1 - tMin;
for (var i = 0; i < offsets.length && winding.quality < 0.5; i++) { for (var i = 0; i < offsets.length && winding.quality < 0.5; i++) {
@ -711,26 +737,24 @@ PathItem.inject(new function() {
var curve = entry.curve, var curve = entry.curve,
path = curve._path, path = curve._path,
parent = path._parent, parent = path._parent,
operand = parent instanceof CompoundPath ? parent : path,
t = Numerical.clamp(curve.getTimeAt(length), tMin, tMax), t = Numerical.clamp(curve.getTimeAt(length), tMin, tMax),
pt = curve.getPointAtTime(t), pt = curve.getPointAtTime(t),
// Determine the direction in which to check the winding // Determine the direction in which to check the winding
// from the point (horizontal or vertical), based on the // from the point (horizontal or vertical), based on the
// curve's direction at that point. If tangent is less // curve's direction at that point. If tangent is less
// than 45°, cast the ray vertically, else horizontally. // than 45°, cast the ray vertically, else horizontally.
dir = abs(curve.getTangentAtTime(t).normalize().y) dir = abs(curve.getTangentAtTime(t).y) < Math.SQRT1_2;
< Math.SQRT1_2 ? 1 : 0;
if (parent instanceof CompoundPath)
path = parent;
// While subtracting, we need to omit this curve if it is // While subtracting, we need to omit this curve if it is
// contributing to the second operand and is outside the // contributing to the second operand and is outside the
// first operand. // first operand.
var wind = !(operator.subtract && path2 && ( var wind = !(operator.subtract && path2 && (
path === path1 && operand === path1 &&
path2._getWinding(pt, dir, true).winding || path2._getWinding(pt, dir, true).winding ||
path === path2 && operand === path2 &&
!path1._getWinding(pt, dir, true).winding)) !path1._getWinding(pt, dir, true).winding))
? getWinding(pt, curves, dir, true) ? getWinding(pt, curves, dir, true)
: windingZero; : { winding: 0, quality: 1 };
if (wind.quality > winding.quality) if (wind.quality > winding.quality)
winding = wind; winding = wind;
break; break;
@ -1019,7 +1043,7 @@ PathItem.inject(new function() {
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
unite: function(path, options) { unite: function(path, options) {
return computeBoolean(this, path, 'unite', options); return traceBoolean(this, path, 'unite', options);
}, },
/** /**
@ -1029,15 +1053,18 @@ PathItem.inject(new function() {
* @option [options.insert=true] {Boolean} whether the resulting item * @option [options.insert=true] {Boolean} whether the resulting item
* should be inserted back into the scene graph, above both paths * should be inserted back into the scene graph, above both paths
* involved in the operation * involved in the operation
* @option [options.stroke=false] {Boolean} whether the operation should * @option [options.trace=true] {Boolean} whether the tracing method is
* be performed on the stroke or on the fill of the first path * used, treating both paths as areas when determining which parts
* of the paths are to be kept in the result, or whether the first
* path is only to be split at intersections, keeping the parts of
* the curves that intersect with the area of the second path.
* *
* @param {PathItem} path the path to intersect with * @param {PathItem} path the path to intersect with
* @param {Object} [options] the boolean operation options * @param {Object} [options] the boolean operation options
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
intersect: function(path, options) { intersect: function(path, options) {
return computeBoolean(this, path, 'intersect', options); return traceBoolean(this, path, 'intersect', options);
}, },
/** /**
@ -1047,15 +1074,18 @@ PathItem.inject(new function() {
* @option [options.insert=true] {Boolean} whether the resulting item * @option [options.insert=true] {Boolean} whether the resulting item
* should be inserted back into the scene graph, above both paths * should be inserted back into the scene graph, above both paths
* involved in the operation * involved in the operation
* @option [options.stroke=false] {Boolean} whether the operation should * @option [options.trace=true] {Boolean} whether the tracing method is
* be performed on the stroke or on the fill of the first path * used, treating both paths as areas when determining which parts
* of the paths are to be kept in the result, or whether the first
* path is only to be split at intersections, removing the parts of
* the curves that intersect with the area of the second path.
* *
* @param {PathItem} path the path to subtract * @param {PathItem} path the path to subtract
* @param {Object} [options] the boolean operation options * @param {Object} [options] the boolean operation options
* @return {PathItem} the resulting path item * @return {PathItem} the resulting path item
*/ */
subtract: function(path) { subtract: function(path, options) {
return computeBoolean(this, path, 'subtract'); return traceBoolean(this, path, 'subtract', options);
}, },
/** /**
@ -1068,10 +1098,10 @@ PathItem.inject(new function() {
* *
* @param {PathItem} path the path to exclude the intersection of * @param {PathItem} path the path to exclude the intersection of
* @param {Object} [options] the boolean operation options * @param {Object} [options] the boolean operation options
* @return {PathItem} the resulting group item * @return {PathItem} the resulting path item
*/ */
exclude: function(path, options) { exclude: function(path, options) {
return computeBoolean(this, path, 'exclude', options); return traceBoolean(this, path, 'exclude', options);
}, },
/** /**
@ -1083,15 +1113,19 @@ PathItem.inject(new function() {
* @option [options.insert=true] {Boolean} whether the resulting item * @option [options.insert=true] {Boolean} whether the resulting item
* should be inserted back into the scene graph, above both paths * should be inserted back into the scene graph, above both paths
* involved in the operation * involved in the operation
* @option [options.stroke=false] {Boolean} whether the operation should * @option [options.trace=true] {Boolean} whether the tracing method is
* be performed on the stroke or on the fill of the first path * used, treating both paths as areas when determining which parts
* of the paths are to be kept in the result, or whether the first
* path is only to be split at intersections.
* *
* @param {PathItem} path the path to divide by * @param {PathItem} path the path to divide by
* @param {Object} [options] the boolean operation options * @param {Object} [options] the boolean operation options
* @return {Group} the resulting group item * @return {PathItem} the resulting path item
*/ */
divide: function(path, options) { divide: function(path, options) {
return createResult(Group, [ return options && (options.trace == false || options.stroke)
? splitBoolean(this, path, 'divide')
: createResult([
this.subtract(path, options), this.subtract(path, options),
this.intersect(path, options) this.intersect(path, options)
], true, this, path, options); ], true, this, path, options);

View file

@ -330,7 +330,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{
: (_matrix || path._matrix)._orNullIfIdentity(); : (_matrix || path._matrix)._orNullIfIdentity();
// First check the bounds of the two paths. If they don't intersect, // First check the bounds of the two paths. If they don't intersect,
// we don't need to iterate through their curves. // we don't need to iterate through their curves.
return self || this.getBounds(matrix1).touches(path.getBounds(matrix2)) return self || this.getBounds(matrix1).intersects(
path.getBounds(matrix2), /*#=*/Numerical.EPSILON)
? Curve.getIntersections( ? Curve.getIntersections(
this.getCurves(), !self && path.getCurves(), include, this.getCurves(), !self && path.getCurves(), include,
matrix1, matrix2, _returnFirst) matrix1, matrix2, _returnFirst)

View file

@ -834,7 +834,7 @@ var Color = Base.extend(new function() {
+ components.join(',') + ')'; + components.join(',') + ')';
}, },
toCanvasStyle: function(ctx) { toCanvasStyle: function(ctx, matrix) {
if (this._canvasStyle) if (this._canvasStyle)
return this._canvasStyle; return this._canvasStyle;
// Normal colors are simply represented by their CSS string. // Normal colors are simply represented by their CSS string.
@ -846,10 +846,20 @@ var Color = Base.extend(new function() {
stops = gradient._stops, stops = gradient._stops,
origin = components[1], origin = components[1],
destination = components[2], destination = components[2],
highlight = components[3],
inverse = matrix && matrix.inverted(),
canvasGradient; canvasGradient;
// If the item's content is transformed by a matrix, we need to
// inverse transform the gradient points, as they are defined in
// item's parent coordinate system.
if (inverse) {
origin = inverse._transformPoint(origin);
destination = inverse._transformPoint(destination);
if (highlight)
highlight = inverse._transformPoint(highlight);
}
if (gradient._radial) { if (gradient._radial) {
var radius = destination.getDistance(origin), var radius = destination.getDistance(origin);
highlight = components[3];
if (highlight) { if (highlight) {
var vector = highlight.subtract(origin); var vector = highlight.subtract(origin);
if (vector.getLength() > radius) if (vector.getLength() > radius)

View file

@ -69,9 +69,9 @@ var GradientStop = Base.extend(/** @lends GradientStop# */{
* Called by various setters whenever a value changes * Called by various setters whenever a value changes
*/ */
_changed: function() { _changed: function() {
// Loop through the gradients that use this stop and notify them about // Notify the graident that uses this stop about the change, so it can
// the change, so they can notify their gradient colors, which in turn // notify its gradient colors, which in turn will notify the items they
// will notify the items they are used in: // are used in:
if (this._owner) if (this._owner)
this._owner._changed(/*#=*/Change.STYLE); this._owner._changed(/*#=*/Change.STYLE);
}, },
@ -127,7 +127,7 @@ var GradientStop = Base.extend(/** @lends GradientStop# */{
/** /**
* @private * @private
* @bean * @bean
* @deprecated use {@link #getOffset()} instead. * @deprecated use {@link #offset} instead.
*/ */
getRampPoint: '#getOffset', getRampPoint: '#getOffset',
setRampPoint: '#setOffset', setRampPoint: '#setOffset',

View file

@ -264,8 +264,7 @@ var Style = Base.extend(new function() {
return fields; return fields;
}, /** @lends Style# */{ }, /** @lends Style# */{
set: function(style) { set: function(style) {
this._values = {}; // Reset already set styles. // If the passed style object is also a Style, clone its cloneable
// If the passed style object is also a Style, clone its clonable
// fields rather than simply copying them. // fields rather than simply copying them.
var isStyle = style instanceof Style, var isStyle = style instanceof Style,
// Use the other stlyle's _values object for iteration // Use the other stlyle's _values object for iteration
@ -355,7 +354,7 @@ var Style = Base.extend(new function() {
/** /**
* @bean * @bean
* @private * @private
* @deprecated use {@link #getFontFamily()} instead. * @deprecated use {@link #fontFamily} instead.
*/ */
getFont: '#getFontFamily', getFont: '#getFontFamily',
setFont: '#setFontFamily', setFont: '#setFontFamily',

View file

@ -95,7 +95,7 @@ new function() {
attrs.y -= size.height / 2; attrs.y -= size.height / 2;
attrs.width = size.width; attrs.width = size.width;
attrs.height = size.height; attrs.height = size.height;
attrs.href = options.embedImages === false && image && image.src attrs.href = options.embedImages == false && image && image.src
|| item.toDataURL(); || item.toDataURL();
return SvgElement.create('image', attrs, formatter); return SvgElement.create('image', attrs, formatter);
} }

View file

@ -389,13 +389,6 @@ new function() {
.translate(bounds.getPoint()) .translate(bounds.getPoint())
.scale(bounds.getSize())); .scale(bounds.getSize()));
} }
if (item instanceof Shape) {
// When applying gradient colors to shapes, we need
// to offset the shape's initial position to get the
// same results as SVG.
color.transform(new Matrix().translate(
item.getPosition(true).negate()));
}
} }
} }
} }
@ -528,23 +521,25 @@ new function() {
* @param {Item} item the item to apply the style and attributes to * @param {Item} item the item to apply the style and attributes to
*/ */
function applyAttributes(item, node, isRoot) { function applyAttributes(item, node, isRoot) {
// SVG attributes can be set both as styles and direct node attributes, if (node.style) {
// so we need to handle both. // SVG attributes can be set both as styles and direct node
// attributes, so we need to handle both.
var parent = node.parentNode, var parent = node.parentNode,
styles = { styles = {
node: DomElement.getStyles(node) || {}, node: DomElement.getStyles(node) || {},
// Do not check for inheritance if this is root, since we want // Do not check for inheritance if this is root, to make the
// the default SVG settings to stick. Also detect defs parents, // default SVG settings stick. Also detect defs parents, of
// of which children need to explicitly inherit their styles. // which children need to explicitly inherit their styles.
parent: !isRoot && !/^defs$/i.test(parent.tagName) parent: !isRoot && !/^defs$/i.test(parent.tagName)
&& DomElement.getStyles(parent) || {} && DomElement.getStyles(parent) || {}
}; };
Base.each(attributes, function(apply, name) { Base.each(attributes, function(apply, name) {
var value = getAttribute(node, name, styles); var value = getAttribute(node, name, styles);
// 'clip-path' attribute returns a new item, support it here: // 'clip-path' attribute returns a new item, support it here:
item = value !== undefined && apply(item, value, name, node, styles) item = value !== undefined
|| item; && apply(item, value, name, node, styles) || item;
}); });
}
return item; return item;
} }
@ -578,9 +573,10 @@ new function() {
parent, parent,
next; next;
if (isRoot && isElement) { if (isRoot && isElement) {
// Set rootSize root element size, fall-back to view size. // Set rootSize to view size, as getSize() may refer to it (#1242).
rootSize = getSize(node, null, null, true) rootSize = paper.getView().getSize();
|| paper.getView().getSize(); // Now set rootSize to the root element size, and fall-back to view.
rootSize = getSize(node, null, null, true) || rootSize;
// We need to move the SVG node to the current document, so default // We need to move the SVG node to the current document, so default
// styles are correctly inherited! For this we create and insert a // styles are correctly inherited! For this we create and insert a
// temporary SVG container which is removed again at the end. This // temporary SVG container which is removed again at the end. This

View file

@ -158,7 +158,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{
/** /**
* @bean * @bean
* @private * @private
* @deprecated use {@link #getStyle()} instead. * @deprecated use {@link #style} instead.
*/ */
getCharacterStyle: '#getStyle', getCharacterStyle: '#getStyle',
setCharacterStyle: '#setStyle', setCharacterStyle: '#setStyle',
@ -166,7 +166,7 @@ var TextItem = Item.extend(/** @lends TextItem# */{
/** /**
* @bean * @bean
* @private * @private
* @deprecated use {@link #getStyle()} instead. * @deprecated use {@link #style} instead.
*/ */
getParagraphStyle: '#getStyle', getParagraphStyle: '#getStyle',
setParagraphStyle: '#setStyle' setParagraphStyle: '#setStyle'

View file

@ -608,7 +608,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @name View#rotate * @name View#rotate
* @function * @function
* @param {Number} angle the rotation angle * @param {Number} angle the rotation angle
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
* @see Matrix#rotate(angle[, center]) * @see Matrix#rotate(angle[, center])
*/ */
@ -619,7 +619,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @name View#scale * @name View#scale
* @function * @function
* @param {Number} scale the scale factor * @param {Number} scale the scale factor
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
*/ */
/** /**
* Scales the view by the given values from its center point, or optionally * Scales the view by the given values from its center point, or optionally
@ -629,7 +629,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @function * @function
* @param {Number} hor the horizontal scale factor * @param {Number} hor the horizontal scale factor
* @param {Number} ver the vertical scale factor * @param {Number} ver the vertical scale factor
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
*/ */
/** /**
@ -639,7 +639,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @name View#shear * @name View#shear
* @function * @function
* @param {Point} shear the horziontal and vertical shear factors as a point * @param {Point} shear the horziontal and vertical shear factors as a point
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
* @see Matrix#shear(shear[, center]) * @see Matrix#shear(shear[, center])
*/ */
/** /**
@ -650,7 +650,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @function * @function
* @param {Number} hor the horizontal shear factor * @param {Number} hor the horizontal shear factor
* @param {Number} ver the vertical shear factor * @param {Number} ver the vertical shear factor
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
* @see Matrix#shear(hor, ver[, center]) * @see Matrix#shear(hor, ver[, center])
*/ */
@ -661,7 +661,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @name View#skew * @name View#skew
* @function * @function
* @param {Point} skew the horziontal and vertical skew angles in degrees * @param {Point} skew the horziontal and vertical skew angles in degrees
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
* @see Matrix#shear(skew[, center]) * @see Matrix#shear(skew[, center])
*/ */
/** /**
@ -672,7 +672,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @function * @function
* @param {Number} hor the horizontal skew angle in degrees * @param {Number} hor the horizontal skew angle in degrees
* @param {Number} ver the vertical sskew angle in degrees * @param {Number} ver the vertical sskew angle in degrees
* @param {Point} [center={@link View#getCenter()}] * @param {Point} [center={@link View#center}]
* @see Matrix#shear(hor, ver[, center]) * @see Matrix#shear(hor, ver[, center])
*/ */
@ -1399,8 +1399,15 @@ new function() { // Injection scope for event handling on the browser
&& (Date.now() - clickTime < 300); && (Date.now() - clickTime < 300);
downItem = clickItem = hitItem; downItem = clickItem = hitItem;
// Only start dragging if the mousedown event has not // Only start dragging if the mousedown event has not
// prevented the default. // prevented the default, and if the hitItem or any of its
dragItem = !prevented && hitItem; // parents actually respond to mousedrag events.
if (!prevented && hitItem) {
var item = hitItem;
while (item && !item.responds('mousedrag'))
item = item._parent;
if (item)
dragItem = hitItem;
}
downPoint = point; downPoint = point;
} else if (mouse.up) { } else if (mouse.up) {
// Emulate click / doubleclick, but only on the hit-item, // Emulate click / doubleclick, but only on the hit-item,

View file

@ -144,10 +144,16 @@ var comparePixels = function(actual, expected, message, options) {
function rasterize(item, group, resolution) { function rasterize(item, group, resolution) {
var raster = null; var raster = null;
if (group) { if (group) {
var parent = item.parent,
index = item.index;
group.addChild(item); group.addChild(item);
raster = group.rasterize(resolution, false); raster = group.rasterize(resolution, false);
if (parent) {
parent.insertChild(index, item);
} else {
item.remove(); item.remove();
} }
}
return raster; return raster;
} }
@ -469,16 +475,34 @@ var compareBoolean = function(actual, expected, message, options) {
message = getFunctionMessage(actual); message = getFunctionMessage(actual);
actual = actual(); actual = actual();
} }
var style = { var parent,
index,
style = {
strokeColor: 'black', strokeColor: 'black',
fillColor: expected && fillColor: expected && (expected.closed
(expected.closed || expected.children && 'yellow') || null || expected.firstChild && expected.firstChild.closed && 'yellow')
|| null
}; };
if (actual) if (actual) {
parent = actual.parent;
index = actual.index;
// Remove it from parent already now, in case we're comparing children
// of compound-paths, so we can apply styling to them.
if (parent && parent instanceof CompoundPath) {
actual.remove();
} else {
parent = null;
}
actual.style = style; actual.style = style;
if (expected) }
if (expected) {
expected.style = style; expected.style = style;
}
equals(actual, expected, message, Base.set({ rasterize: true }, options)); equals(actual, expected, message, Base.set({ rasterize: true }, options));
if (parent) {
// Insert it back.
parent.insertChild(index, actual);
}
}; };
var createSVG = function(str, attrs) { var createSVG = function(str, attrs) {

View file

@ -248,3 +248,38 @@ test('Gradient', function() {
equals(function() { return stop3.offset; }, 1); equals(function() { return stop3.offset; }, 1);
equals(function() { return stop4.offset; }, 0.5); equals(function() { return stop4.offset; }, 0.5);
}); });
test('Gradients with applyMatrix', function() {
var topLeft = [100, 100];
var bottomRight = [400, 400];
var gradientColor = {
gradient: {
stops: ['yellow', 'red', 'blue']
},
origin: topLeft,
destination: bottomRight
}
var path = new Path.Rectangle({
topLeft: topLeft,
bottomRight: bottomRight,
fillColor: gradientColor,
applyMatrix: true
});
var shape = new Shape.Rectangle({
topLeft: topLeft,
bottomRight: bottomRight,
fillColor: gradientColor,
applyMatrix: false
});
comparePixels(path, shape);
path.scale(2);
path.rotate(45);
shape.scale(2);
shape.rotate(45);
comparePixels(path, shape);
});

View file

@ -114,7 +114,9 @@ test('group.insertChildren(0, otherGroup.children)', function() {
test('group.addChildren()', function() { test('group.addChildren()', function() {
var group = new Group(); var group = new Group();
var children = [new Path(), new Path()]; var path1 = new Path();
var path2 = new Path();
var children = [path1, path2];
group.addChildren(children); group.addChildren(children);
equals(group.children.length, 2, equals(group.children.length, 2,
'group.children.length after adding 2 children'); 'group.children.length after adding 2 children');
@ -128,4 +130,8 @@ test('group.addChildren()', function() {
equals(group.children.length, 2, equals(group.children.length, 2,
'calling group.addChildren() with an array with 3 entries, ' + 'calling group.addChildren() with an array with 3 entries, ' +
'of which 2 are valid, group.children.length should be 2'); 'of which 2 are valid, group.children.length should be 2');
}); children = [path1, path1, path2];
group.addChildren(children);
equals(group.children.length, 2,
'adding the same item twice should only add it once.');
})

View file

@ -21,6 +21,7 @@ test('Hit-testing options', function() {
segments: true, segments: true,
handles: false, handles: false,
ends: false, ends: false,
position: false,
center: false, center: false,
bounds: false, bounds: false,
guides: false, guides: false,
@ -128,31 +129,46 @@ test('hitting path segments', function() {
}); });
}); });
test('hitting the center of a path', function() { test('hitting the center and position of a path', function() {
var path = new Path([0, 0], [100, 100], [200, 0]); var path = new Path([0, 0], [100, 100], [200, 0]);
path.closed = true; path.closed = true;
var center = path.bounds.center,
position = path.position,
positionResult = {
type: 'position', item: path, point: position
},
centerResult = {
type: 'center', item: path, point: center
};
testHitResult(paper.project.hitTest(path.position, { testHitResult(paper.project.hitTest(position, {
center: true center: true
}), { }), centerResult);
type: 'center',
item: path,
point: path.position
});
});
test('hitting the center of a path with tolerance', function() {
var path = new Path([0, 0], [100, 100], [200, 0]);
path.closed = true;
var offset = new Point(1, 1); var offset = new Point(1, 1);
testHitResult(paper.project.hitTest(position.add(offset), {
testHitResult(paper.project.hitTest(path.position.add(offset), {
tolerance: offset.length, tolerance: offset.length,
center: true center: true
}), centerResult, 'position with tolerance');
testHitResult(paper.project.hitTest(position, {
position: true
}), positionResult);
testHitResult(paper.project.hitTest(center, {
position: true
}), positionResult);
path.pivot = [100, 100];
testHitResult(paper.project.hitTest(center, {
position: true
}), null, 'with pivot, the position should not be in the center');
testHitResult(paper.project.hitTest(path.position, {
position: true
}), { }), {
type: 'center', type: 'position', item: path, point: path.position
item: path,
point: path.position
}); });
}); });
@ -838,4 +854,18 @@ test('hit-testing scaled items with different settings of view.zoom and item.str
testItem(Path, 2, true); testItem(Path, 2, true);
}); });
test('hit-testing items scaled to 0', function() {
var item = new Shape.Rectangle({
point: [0, 0],
size: [100, 100],
fillColor: 'red',
selected: true
});
item.scale(0);
testHitResult(project.hitTest(item.position), null,
'should not throw an exception.');
});
// TODO: project.hitTest(point, {type: AnItemType}); // TODO: project.hitTest(point, {type: AnItemType});

View file

@ -207,7 +207,6 @@ test('path.bounds & path.strokeBounds with stroke styles', function() {
} }
var path = makePath(); var path = makePath();
path.fullySelected = true;
path.strokeColor = 'black'; path.strokeColor = 'black';
path.strokeCap = 'butt'; path.strokeCap = 'butt';
path.strokeJoin = 'round'; path.strokeJoin = 'round';
@ -730,8 +729,41 @@ test('symbolItem.bounds with strokeScaling disabled', function() {
var placed = symbol.place([100, 100]); var placed = symbol.place([100, 100]);
equals(placed.bounds, new Rectangle(85, 85, 30, 30), 'placed.bounds'); equals(placed.bounds, new Rectangle(85, 85, 30, 30), 'placed.bounds');
placed.scale(4, 2); placed.scale(4, 2);
equals(placed.bounds, new Rectangle(55, 75, 90, 50), 'placed.bounds after scaling'); equals(placed.bounds, new Rectangle(55, 75, 90, 50),
'placed.bounds after scaling');
path.strokeScaling = true; path.strokeScaling = true;
equals(placed.bounds, new Rectangle(40, 70, 120, 60), 'placed.bounds after scaling, strokeScaling enabled'); equals(placed.bounds, new Rectangle(40, 70, 120, 60),
'placed.bounds after scaling, strokeScaling enabled');
}); });
test('item.visible and item.parents.bounds (#1248)', function() {
var item = new Path.Rectangle({
point: [0, 0],
size: [50, 100],
visible: false
});
equals(item.bounds, new Rectangle(0, 0, 50, 100), 'item.bounds');
equals(item.parent.bounds, new Rectangle(0, 0, 0, 0),
'item.parent.bounds with item.visible = false');
item.visible = true;
equals(item.parent.bounds, item.bounds,
'item.parent.bounds with item.visible = true');
});
test('group.internalBounds with child and child.applyMatrix = false (#1250)', function() {
var item1 = Shape.Rectangle({
point: [100, 100],
size: [200, 200]
});
var item2 = new Path.Rectangle({
point: [0, 0],
size: [100, 100]
});
var group = new Group([item1, item2]);
equals(item1.bounds, new Rectangle(100, 100, 200, 200), 'item.bounds');
equals(group.internalBounds, new Rectangle(0, 0, 300, 300),
'group.internalBounds before scaling item1');
item1.scale(0.5);
equals(group.internalBounds, new Rectangle(0, 0, 250, 250),
'group.internalBounds after scaling item1');
});

View file

@ -177,7 +177,7 @@ test('#719', function() {
compareBoolean(result, expected); compareBoolean(result, expected);
}); });
test('#757 (path1.intersect(pat2, { stroke: true }))', function() { test('#757 (path1.intersect(pat2, { trace: false }))', function() {
var rect = new Path.Rectangle({ var rect = new Path.Rectangle({
from: [100, 250], from: [100, 250],
to: [350, 350] to: [350, 350]
@ -194,7 +194,7 @@ test('#757 (path1.intersect(pat2, { stroke: true }))', function() {
] ]
}); });
var res = line.intersect(rect, { stroke: true }); var res = line.intersect(rect, { trace: false });
var children = res.removeChildren(); var children = res.removeChildren();
var first = children[0]; var first = children[0];
@ -864,7 +864,62 @@ test('#1123', function() {
'M100,200v-100h100v100zM180,180v-60h-60v60z'); 'M100,200v-100h100v100zM180,180v-60h-60v60z');
}); });
test('#1239', function() { test('#1221', function() {
var rect1 = new Path.Rectangle({
point: [100, 100],
size: [200, 200]
});
var circle = new Path.Circle({
center: [100, 100],
radius: 100
});
compareBoolean(function() { return rect1.subtract(circle, { trace: false }); },
'M200,100h100v200h-200v-100');
compareBoolean(function() { return rect1.subtract(circle, { trace: true }); },
'M100,300v-100c55.22847,0 100,-44.77153 100,-100h100v200z');
var blob = PathItem.create("M534,273C171.7,111,60.5,117.1,30,158c-40.5,54.3,31.5,210.2,111,222c60.8,9,88-71.9,159-66c81.6,6.8,99.6,118.3,179,128c33.8,4.1,83.1-9.7,150-90")
var rect2 = new Path.Rectangle({
point: [150, 100],
size: [300, 300]
});
compareBoolean(function() { return blob.subtract(rect2, { trace: false }); },
'M534,273c-29.65069,-13.2581 -57.61955,-25.39031 -84,-36.46967M150,138.13156c-71.67127,-11.53613 -105.25987,0.10217 -120,19.86844c-40.5,54.3 31.5,210.2 111,222c3.08303,0.45637 6.07967,0.68158 9,0.69867M409.85616,400c18.87105,20.95032 39.82014,38.41763 69.14384,42c33.8,4.1 83.1,-9.7 150,-90');
compareBoolean(function() { return blob.subtract(rect2, { trace: true }); },
'M629,352c-66.9,80.3 -116.2,94.1 -150,90c-29.3237,-3.58237 -50.27279,-21.04968 -69.14384,-42h40.14384v-163.46967c26.38045,11.07937 54.34931,23.21157 84,36.46967M141,380c-79.5,-11.8 -151.5,-167.7 -111,-222c14.74013,-19.76627 48.32873,-31.40457 120,-19.86844v242.56712c-2.92033,-0.01709 -5.91697,-0.24231 -9,-0.69867z');
var rect3 = new Path.Rectangle({
point: [150, 100],
size: [300, 150]
});
compareBoolean(function() { return blob.subtract(rect3, { trace: false }); },
'M534,273c-29.65069,-13.2581 -57.61955,-25.39031 -84,-36.46967M150,138.13156c-71.67127,-11.53613 -105.25987,0.10217 -120,19.86844c-40.5,54.3 31.5,210.2 111,222c60.8,9 88,-71.9 159,-66c81.6,6.8 99.6,118.3 179,128c33.8,4.1 83.1,-9.7 150,-90');
compareBoolean(function() { return blob.subtract(rect3, { trace: true }); },
'M629,352c-66.9,80.3 -116.2,94.1 -150,90c-79.4,-9.7 -97.4,-121.2 -179,-128c-71,-5.9 -98.2,75 -159,66c-79.5,-11.8 -151.5,-167.7 -111,-222c14.74013,-19.76627 48.32873,-31.40457 120,-19.86844v111.86844h300v-13.46967c26.38045,11.07937 54.34931,23.21157 84,36.46967');
var rect4 = new Path.Rectangle({
point: [200, 200],
size: [400, 200]
});
var line = new Path.Line({
from: [400, 300],
to: [400, 600]
});
var division = line.divide(rect4, { trace: false });
equals(function() { return division.children.length; }, 2);
compareBoolean(function() { return division.children[0]; }, 'M400,300v100');
compareBoolean(function() { return division.children[1]; }, 'M400,400v200');
});
test('#1239 / #1073', function() {
var p1 = new Path([[890.91, 7.52, 0, 0, 85.40999999999997, 94.02], [1024, 351.78, 0, -127.03999999999996, 0, 127.14999999999998], [890.69, 696.28, 85.54999999999995, -94.03999999999996, 0, 0], [843.44, 653.28, 0, 0, 75.20000000000005, -82.66999999999996], [960, 351.78, 0, 111.75, 0, -111.63], [843.65, 50.51999999999998, 75.07000000000005, 82.63999999999999, 0, 0], true]); var p1 = new Path([[890.91, 7.52, 0, 0, 85.40999999999997, 94.02], [1024, 351.78, 0, -127.03999999999996, 0, 127.14999999999998], [890.69, 696.28, 85.54999999999995, -94.03999999999996, 0, 0], [843.44, 653.28, 0, 0, 75.20000000000005, -82.66999999999996], [960, 351.78, 0, 111.75, 0, -111.63], [843.65, 50.51999999999998, 75.07000000000005, 82.63999999999999, 0, 0], true]);
var p2 = new Path([[960, 352, -0.05999999999994543, 111.67999999999995, 0, 0], [1024, 352, 0, 0, -0.05999999999994543, 127.07], [890.69, 696.28, 85.5, -93.98000000000002, 0, 0], [843.44, 653.28, 0, 0, 75.14999999999998, -82.61000000000001], true]); var p2 = new Path([[960, 352, -0.05999999999994543, 111.67999999999995, 0, 0], [1024, 352, 0, 0, -0.05999999999994543, 127.07], [890.69, 696.28, 85.5, -93.98000000000002, 0, 0], [843.44, 653.28, 0, 0, 75.14999999999998, -82.61000000000001], true]);
project.activeLayer.scale(0.25); project.activeLayer.scale(0.25);
@ -945,6 +1000,47 @@ test('Selected edge-cases from @hari\'s boolean-test suite', function() {
'M283.8,324.71094l0,41.45422c1.42504,1.15843 2.79504,2.33005 4.11,3.51485c7.88,7.1 13.66333,14.91 17.35,23.43c3.68667,8.52667 4.97333,17.91 3.86,28.15c-0.36003,3.31143 -0.98987,6.71838 -1.88951,10.22084c1.02,0.27014 2.0765,0.50986 3.16951,0.71916c6.26667,1.2 13.8,1.8 22.6,1.8l8.39102,0c0.08444,-2.79841 -0.02257,-5.56508 -0.32102,-8.3c-0.78746,-7.20773 -2.77831,-14.50893 -5.97256,-21.90361c-1.74009,-0.09727 -3.3059,-0.2294 -4.69744,-0.39639c-3.33333,-0.4 -5.93333,-1.26667 -7.8,-2.6c-1.86667,-1.33333 -3.13333,-3.2 -3.8,-5.6c-0.66667,-2.4 -1,-5.6 -1,-9.6l0,-43.223z'); 'M283.8,324.71094l0,41.45422c1.42504,1.15843 2.79504,2.33005 4.11,3.51485c7.88,7.1 13.66333,14.91 17.35,23.43c3.68667,8.52667 4.97333,17.91 3.86,28.15c-0.36003,3.31143 -0.98987,6.71838 -1.88951,10.22084c1.02,0.27014 2.0765,0.50986 3.16951,0.71916c6.26667,1.2 13.8,1.8 22.6,1.8l8.39102,0c0.08444,-2.79841 -0.02257,-5.56508 -0.32102,-8.3c-0.78746,-7.20773 -2.77831,-14.50893 -5.97256,-21.90361c-1.74009,-0.09727 -3.3059,-0.2294 -4.69744,-0.39639c-3.33333,-0.4 -5.93333,-1.26667 -7.8,-2.6c-1.86667,-1.33333 -3.13333,-3.2 -3.8,-5.6c-0.66667,-2.4 -1,-5.6 -1,-9.6l0,-43.223z');
}); });
test('Selected test-cases provided by @iconexperience', function() {
// Test all of test cases in one batch.
// Read more in #784
var paths = [[
[[276.60479999999995, 265.6776, 2.6464000000000283, 5.646400000000028, 1.2208000000000538, -0.8512000000000057], [279.9632, 262.924, -1.0127999999999702, 1.0160000000000196, 0.019200000000012096, 3.302400000000034], [294.4, 269.31440000000003, -7.240000000000009, 0, 7.240000000000009, 0], [308.8384, 262.924, -0.020800000000008367, 3.302400000000034, 1.0112000000000307, 1.0160000000000196], [312.1968, 265.6776, -1.2224000000000501, -0.8512000000000057, -2.6464000000000283, 5.646400000000028], [294.4, 273.3128, 5.590400000000045, 0, -5.590399999999988, 0], true],
[[294.4, 273.3128, 0, 0, -7.240000000000009, 0], [279.9808, 266.9064, 0.014400000000023283, 3.3023999999999774, 0, 0], [279.91999999999996, 253.31279999999998, 0, 0, 9.652800000000013, 0], [308.88, 253.31279999999998, -9.652800000000013, 0, 0, 0], [308.82079999999996, 266.9064, 0, 0, -0.014865740466348143, 3.068288832262283], [295.93610344888236, 273.2670371105283, 6.683812628997487, -0.3953268817066373, 5.682599074647953, -0.3268447810676207], [312.1968, 265.6776, -2.4122653499520084, 5.146166079897625, 8.40640000000002, 5.8559999999999945], [294.4, 307.1864, 16, 0.001599999999996271, 0, 0], true]
],
[
[[512, 563.0515712, 0, 0, 0, 0], [462.4499456000001, 563.0515712, 0, 0, -3.031499856000039, 0], [456.94438400000007, 557.549450576, 0, 3.031499855999982, 0, 0], true],
[[462.4499456000001, 563.0515712, -3.031499856000039, 0, -3.0303527841790014, 0], [456.94438506256495, 557.5494505759262, 0.0018713092763391614, 3.0299308178812225, 0,0], [567.0556160000001, 564], true]
],
[
[[506.49443840000004, 600, 0, 0, 0, 0], [567.0556160000001, 623.6127488, 0, 0, 0, 3.031499856000096], [561.5500544, 629.1148694240001, 3.031499855999982, 0, 3.031499855999982, 0], [567.0556160000001, 634.620431024, 0, -3.031499855999982, 0, 0], true],
[[561.5500544, 629.1148694240001, 3.031499855999982, 0, 3.031499855999982, 0], [567.0556160000001, 634.620431024, 0, -3.0280588800000032, 0, 0], [567.0556160000001, 656.6461184, 0, 0, 0, 3.0280588800000032], true]
],
[
[[400.81546878700505, 400.81546878700505, 79.97937495065997, -79.9793749506602, -79.97937495066014, 79.97937495065997], [111.18453121299513, 400.81546878700505, 79.97937495066012, 79.97937495065997, -79.97937495066009, -79.9793749506602], [111.18453121299507, 111.18453121299507, -79.97937495066003, 79.97937495066017, 79.97937495066009, -79.97937495066007], [400.815468787005, 111.18453121299503, -79.97937495066009, -79.97937495066003, 79.97937495066003, 79.97937495066007], true],
[[400.815468787005, 400.815468787005, 79.97937495065997, -79.97937495066014, -79.97937495066014, 79.97937495065992], [111.18453121299507, 400.815468787005, 79.97937495066014, 79.97937495065992, -79.97937495066003, -79.9793749506602], [111.18453121299507, 111.18453121299504, -79.97937495066003, 79.97937495066014, 79.97937495066009, -79.97937495066006], [400.81546878700493, 111.184531212995, -79.97937495066009, -79.97937495066002, 79.97937495066003, 79.97937495066004], true]
],
[
[[512, 96, -125.89361307431193, 0, 168, 0], [736, 337.78, 0, -120.88999999999999, 0, 21.79000000000002], [729.34, 402.7, 4.319999999999936, -21.340000000000032, -25.649999999999977, 126.62999999999994], [512, 640, 95.85000000000002, 0, 0, 0], [288.04, 332.0799999999999, 100.000575152844760396, -0.000053268689669039304, 0.41767160812742077, -28.392122573051836], [299.43514767699014, 248.78534185388617, -7.421136238632187, 26.615225151165646, 23.67264213867054, -84.8997620019185], true],
[[512, 96, -125.89322163652065, 0, 168, 0], [736, 337.78, 0, -120.88999999999999, 0, 151.11], [512, 640, 112, 0, -112, 0], [288, 337.78, 0, 151.11, 0, -1.9002097681526493], [288.04172546304807, 332.07984018287976, -0.02788946906741785, 1.8997402808149104, 0.4168153242491144, -28.39210955294908], [299.4351476773096, 248.7853418527407, -7.42079681324509, 26.615144268685953, 23.671675930689275, -84.89992191301803], true]
]];
var results = [
['M276.6048,265.6776c1.22072,-0.85114 2.34544,-1.73748 3.35819,-2.75339l-0.04299,-9.61141c9.6528,0 19.3072,0 28.96,0l-0.04199,9.64131c0.0002,-0.01003 0.00032,-0.02007 0.00039,-0.03011c1.0112,1.016 2.136,1.9024 3.3584,2.7536c8.4064,5.856 -1.7968,41.5104 -17.7968,41.5088l0,-33.8736c-0.54424,0 -1.08806,-0.01754 -1.62844,-0.05139c-5.65571,-0.34382 -13.76286,-2.45483 -16.16676,-7.58381z M305.01293,271.38867c-2.95531,1.12961 -6.25621,1.70701 -8.98571,1.87282c-0.03036,0.0019 -0.06073,0.00375 -0.09111,0.00555c2.75482,-0.15845 6.09324,-0.73747 9.07682,-1.87836z'],
['M462.44995,563.05157c-3.0315,0 -5.50556,-2.47062 -5.50556,-5.50212l0,0l110.11123,6.45055c0,0 -107.63717,-0.94843 -104.60567,-0.94843z'],
['M506.49444,600l60.56118,23.61275c0,3.0315 -2.47406,5.50212 -5.50556,5.50212c3.0315,0 5.50556,2.47406 5.50556,5.50556l0,22.02569c0,2.39831 -1.552,-16.27299 -3.70373,-24.14296z'],
['M400.81547,400.81547c-79.97937,79.97937 -209.65156,79.97937 -289.63094,0c-79.97937,-79.97937 -79.97937,-209.65156 0,-289.63094c79.97937,-79.97937 209.65156,-79.97937 289.63094,0c79.97937,79.97937 79.97937,209.65156 0,289.63094z'],
['M551.12365,634.03909c-12.92155,3.89332 -26.02251,5.96091 -39.12365,5.96091c-112,0 -224,-151.11 -224,-302.22c0,-1.90016 0.01384,-3.80031 0.04172,-5.7c-0.00057,0 -0.00115,0 -0.00172,0c0.41767,-28.39212 3.97401,-56.67943 11.39515,-83.29466c23.67168,-84.89992 86.67163,-152.78534 212.56485,-152.78534c168,0 224,120.89 224,241.78c0,133.43422 -87.33052,266.86844 -184.87635,296.25909z']
];
for (var i = 0; i < paths.length; i++) {
var entry = paths[i],
result = results[i],
path1 = PathItem.create(entry[0]),
path2 = PathItem.create(entry[1]);
compareBoolean(path1.unite(path2), result[0], 'path1.unite(path2); // Test ' + (i + 1));
}
});
test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function() { test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function() {
// Test all of @iconexperience's isolated cases in one batch. // Test all of @iconexperience's isolated cases in one batch.
// Read more in #784 // Read more in #784

View file

@ -130,3 +130,20 @@ test('new Path.Line({ from: from, to: to })', function() {
}); });
equals(path.segments.toString(), '{ point: { x: 20, y: 20 } },{ point: { x: 40, y: 40 } }'); equals(path.segments.toString(), '{ point: { x: 20, y: 20 } },{ point: { x: 40, y: 40 } }');
}); });
test('new Path.Line({ insert: false })', function() {
var path = new Path.Line({
from:[50, 50],
to:[100, 100],
insert: false
});
equals(function() {
return typeof path.insert;
}, 'function');
equals(function() {
return path.parent;
}, null);
equals(function() {
return path.isInserted();
}, false);
});

View file

@ -12,13 +12,19 @@
QUnit.module('Path Intersections'); QUnit.module('Path Intersections');
function createPath(curve) {
return new Path([curve.segment1, curve.segment2]);
}
function testIntersections(intersections, results) { function testIntersections(intersections, results) {
equals(intersections.length, results.length, 'intersections.length'); equals(intersections.length, results.length, 'intersections.length');
for (var i = 0; i < results.length; i++) { for (var i = 0, l = Math.min(results.length, intersections.length); i < l; i++) {
var inter = intersections[i]; var inter = intersections[i];
var values = results[i]; var values = results[i];
var name = 'intersections[' + i + ']'; var name = 'intersections[' + i + ']';
if (values.point != null)
equals(inter.point, new Point(values.point), name + '.point'); equals(inter.point, new Point(values.point), name + '.point');
if (values.index != null)
equals(inter.index, values.index, name + '.index'); equals(inter.index, values.index, name + '.index');
if (values.time != null) if (values.time != null)
equals(inter.time, values.time, name + '.time'); equals(inter.time, values.time, name + '.time');
@ -30,9 +36,9 @@ function testIntersections(intersections, results) {
test('#565', function() { test('#565', function() {
var curve1 = new Curve(new Point(421.75945, 416.40481), new Point(-181.49299, -224.94946), new Point(44.52004, -194.13319), new Point(397.47615, 331.34712)); var curve1 = new Curve(new Point(421.75945, 416.40481), new Point(-181.49299, -224.94946), new Point(44.52004, -194.13319), new Point(397.47615, 331.34712));
var curve2 = new Curve(new Point(360.09446, 350.97254), new Point(-58.58867, -218.45806), new Point(-109.55091, -220.99561), new Point(527.83582, 416.79948)); var curve2 = new Curve(new Point(360.09446, 350.97254), new Point(-58.58867, -218.45806), new Point(-109.55091, -220.99561), new Point(527.83582, 416.79948));
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = createPath(curve1);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = createPath(curve2);
testIntersections(curve1.getIntersections(curve2), [ testIntersections(path1.getIntersections(path2), [
{ point: { x: 354.13635, y: 220.81369 }, index: 0, time: 0.46725, crossing: true }, { point: { x: 354.13635, y: 220.81369 }, index: 0, time: 0.46725, crossing: true },
{ point: { x: 390.24772, y: 224.27351 }, index: 0, time: 0.71605, crossing: true } { point: { x: 390.24772, y: 224.27351 }, index: 0, time: 0.71605, crossing: true }
]); ]);
@ -40,9 +46,9 @@ test('#565', function() {
// Alternative pair of curves that has the same issue // Alternative pair of curves that has the same issue
var curve1 = new Curve(new Point(484.9026237381622, 404.11001967731863), new Point(-265.1185871567577, -204.00749347172678), new Point(-176.7118886578828, 111.96015905588865), new Point(438.8191690435633, 429.0297837462276)); var curve1 = new Curve(new Point(484.9026237381622, 404.11001967731863), new Point(-265.1185871567577, -204.00749347172678), new Point(-176.7118886578828, 111.96015905588865), new Point(438.8191690435633, 429.0297837462276));
var curve2 = new Curve(new Point(388.25280445162207, 490.95032326877117), new Point(-194.0586572047323, -50.77360603027046), new Point(-184.71034923568368, -260.5346686206758), new Point(498.41401199810207, 455.55853731930256)); var path1 = new Path([curve1.segment1, curve1.segment2]); var curve2 = new Curve(new Point(388.25280445162207, 490.95032326877117), new Point(-194.0586572047323, -50.77360603027046), new Point(-184.71034923568368, -260.5346686206758), new Point(498.41401199810207, 455.55853731930256)); var path1 = new Path([curve1.segment1, curve1.segment2]);
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = createPath(curve1);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = createPath(curve2);
testIntersections(curve1.getIntersections(curve2), [ testIntersections(path1.getIntersections(path2), [
{ point: { x: 335.62744, y: 338.15939 }, index: 0, time: 0.26516, crossing: true } { point: { x: 335.62744, y: 338.15939 }, index: 0, time: 0.26516, crossing: true }
]); ]);
}); });
@ -50,18 +56,18 @@ test('#565', function() {
test('#568', function() { test('#568', function() {
var curve1 = new Curve(new Point(509.05465863179415, 440.1211663847789), new Point(233.6728838738054, -245.8216403145343), new Point(-270.755685120821, 53.14275110140443), new Point(514.079892472364, 481.95262297522277)); var curve1 = new Curve(new Point(509.05465863179415, 440.1211663847789), new Point(233.6728838738054, -245.8216403145343), new Point(-270.755685120821, 53.14275110140443), new Point(514.079892472364, 481.95262297522277));
var curve2 = new Curve(new Point(542.1666181180626, 451.06309361290187), new Point(179.91238399408758, 148.68241581134498), new Point(193.42650789767504, -47.97609066590667), new Point(423.66228222381324, 386.3876062911004)); var curve2 = new Curve(new Point(542.1666181180626, 451.06309361290187), new Point(179.91238399408758, 148.68241581134498), new Point(193.42650789767504, -47.97609066590667), new Point(423.66228222381324, 386.3876062911004));
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = createPath(curve1);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = createPath(curve2);
testIntersections(curve1.getIntersections(curve2), [ testIntersections(path1.getIntersections(path2), [
{ point: { x: 547.96568, y: 396.66339 }, index: 0, time: 0.07024, crossing: true }, { point: { x: 547.96568, y: 396.66339 }, index: 0, time: 0.07024, crossing: true },
{ point: { x: 504.79973, y: 383.37886 }, index: 0, time: 0.48077, crossing: true } { point: { x: 504.79973, y: 383.37886 }, index: 0, time: 0.48077, crossing: true }
]); ]);
var curve1 = new Curve(new Point(0, 0), new Point(20, 40) , new Point (-30, -50), new Point(50, 50)); var curve1 = new Curve(new Point(0, 0), new Point(20, 40) , new Point (-30, -50), new Point(50, 50));
var curve2 = new Curve(new Point(50, 50), new Point(20, 100), new Point (-30, -120), new Point(250, 250)); var curve2 = new Curve(new Point(50, 50), new Point(20, 100), new Point (-30, -120), new Point(250, 250));
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = createPath(curve1);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = createPath(curve2);
testIntersections(curve1.getIntersections(curve2), [ testIntersections(path1.getIntersections(path2), [
{ point: { x: 50, y: 50 }, index: 0, time: 1, crossing: false } { point: { x: 50, y: 50 }, index: 0, time: 1, crossing: false }
]); ]);
}); });
@ -71,8 +77,7 @@ test('#570', function() {
var curve2 = new Curve(new Point(311.16034791674826, 406.2985255840872), new Point(39.997020018940304, -8.347079462067768), new Point(-73.86292504547487, -77.47859270504358), new Point(465, 467)); var curve2 = new Curve(new Point(311.16034791674826, 406.2985255840872), new Point(39.997020018940304, -8.347079462067768), new Point(-73.86292504547487, -77.47859270504358), new Point(465, 467));
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = new Path([curve1.segment1, curve1.segment2]);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = new Path([curve2.segment1, curve2.segment2]);
var ints = curve1.getIntersections(curve2); testIntersections(path1.getIntersections(path2), [
testIntersections(curve1.getIntersections(curve2), [
{ point: { x: 311.16035, y: 406.29853 }, index: 0, time: 1, crossing: false } { point: { x: 311.16035, y: 406.29853 }, index: 0, time: 1, crossing: false }
]); ]);
}); });
@ -82,7 +87,7 @@ test('#571', function() {
var curve2 = new Curve(new Point(420.1235851920127, 275.8351912321666), new Point(-10.77224553077383, -53.21262197949682), new Point(-259.2129470250785, -258.56165821345775), new Point(465, 467)); var curve2 = new Curve(new Point(420.1235851920127, 275.8351912321666), new Point(-10.77224553077383, -53.21262197949682), new Point(-259.2129470250785, -258.56165821345775), new Point(465, 467));
var path1 = new Path([curve1.segment1, curve1.segment2]); var path1 = new Path([curve1.segment1, curve1.segment2]);
var path2 = new Path([curve2.segment1, curve2.segment2]); var path2 = new Path([curve2.segment1, curve2.segment2]);
testIntersections(curve1.getIntersections(curve2), [ testIntersections(path1.getIntersections(path2), [
{ point: { x: 352.39945, y: 330.44135 }, index: 0, time: 0.41159, crossing: true }, { point: { x: 352.39945, y: 330.44135 }, index: 0, time: 0.41159, crossing: true },
{ point: { x: 420.12359, y: 275.83519 }, index: 0, time: 1, crossing: false } { point: { x: 420.12359, y: 275.83519 }, index: 0, time: 1, crossing: false }
]); ]);
@ -106,6 +111,49 @@ test('circle and square (existing segments overlaps on curves)', function() {
]); ]);
}); });
test('intersecting paths with applyMatrix = true / false', function() {
function test(ctor, applyMatrix) {
var name = ctor.name.toLowerCase();
var item1 = new ctor.Rectangle({
point: [0, 0],
size: [200, 200],
applyMatrix: applyMatrix
});
var offset = new Point(200, 200);
item1.translate(offset);
var item2 = new ctor.Rectangle({
point: [100, 100],
size: [200, 200],
applyMatrix: applyMatrix
});
if (!applyMatrix)
offset = new Point(0, 0);
if (ctor === Path) {
testIntersections(item1.getIntersections(item2), [{
point: new Point(0, 100).add(offset), index: 0, time: 0.5,
crossing: true
}, {
point: new Point(100, 0).add(offset), index: 1, time: 0.5,
crossing: true
}]);
}
equals(item1.intersects(item2), true,
name + '1.intersects(' + name + '2);');
}
test(Path, true);
test(Path, false);
// Also tests #intersects() on Shape
test(Shape, false);
});
test('#904', function() { test('#904', function() {
var path1 = new Path([ var path1 = new Path([
[347.65684372173973, 270.4315945523045, 0, 0, 22.844385382059784, -25.115215946843847], [347.65684372173973, 270.4315945523045, 0, 0, 22.844385382059784, -25.115215946843847],
@ -224,7 +272,7 @@ test('#1197', function() {
}); });
test('#1233', function() { test('#1233', function() {
var p = new Path([ var path = new Path([
[274, 253], [274, 253],
[274.39360933873525, 252.93741452470064, 0, 0, -0.6159287834059803, 0.8638915013756937], [274.39360933873525, 252.93741452470064, 0, 0, -0.6159287834059803, 0.8638915013756937],
[271.7973629022481, 254.56035493761536, 1.0464052751698083, -0.17525065612978258, 0, 0], [271.7973629022481, 254.56035493761536, 1.0464052751698083, -0.17525065612978258, 0, 0],
@ -233,8 +281,8 @@ test('#1233', function() {
[273.5560469948133, 254.11108376712414], [273.5560469948133, 254.11108376712414],
true true
]); ]);
p.scale(100); path.scale(100);
testIntersections(p.getIntersections(), [ testIntersections(path.getIntersections(), [
{ point: { x: 366.12645, y: 320.20927 }, index: 1, time: 0, crossing: false }, { point: { x: 366.12645, y: 320.20927 }, index: 1, time: 0, crossing: false },
{ point: { x: 366.07584, y: 320.28024 }, index: 1, time: 0.00027, crossing: true }, { point: { x: 366.07584, y: 320.28024 }, index: 1, time: 0.00027, crossing: true },
{ point: { x: 366.02122, y: 320.3568 }, index: 1, time: 0.00057, crossing: true }, { point: { x: 366.02122, y: 320.3568 }, index: 1, time: 0.00057, crossing: true },
@ -270,4 +318,28 @@ test('#1270', function() {
{ point: { x: 500.36244, y: 522.99072 }, index: 0, time: 0.03613, crossing: true }, { point: { x: 500.36244, y: 522.99072 }, index: 0, time: 0.03613, crossing: true },
{ point: { x: 463.83556, y: 476.44545 }, index: 0, time: 0.52936, crossing: true } { point: { x: 463.83556, y: 476.44545 }, index: 0, time: 0.52936, crossing: true }
]); ]);
}) });
test('#1284', function() {
// Sketch 1
var curve1 = new Curve([664.0386774947868,382.45193387473927],[0.854973312711149,-17.099466254222136],[-17.09946625422214,-0.8549733127111154],[696.5480661252607,353.03867749478684]);
var curve2 = new Curve([664.0386774947868,382.4519338747394],[-0.854973312711093,17.099466254222147],[-17.09946625422214,-0.8549733127111177],[693.4519338747393,414.96132250521316]);
var expected = [
{ point: { x: 664.03868, y: 382.45193 }, time: 0 }
];
testIntersections(curve1.getIntersections(curve2), expected);
var path1 = createPath(curve1);
var path2 = createPath(curve2);
testIntersections(path1.getIntersections(path2), expected);
// Sketch 2
var curve1 = new Curve([725.9613225052132,385.5480661252606],[4.121287844621423,-82.42575689242983],[33.676217018891116,53.91523058931191],[670.1308066286094,179.78451157644233]);
var curve2 = new Curve([725.9613225052132,385.54806612526073],[1.752865851700387,-35.057317034006445],[17.232666369345022,25.982789528039472],[702.42903006491,293.5923946244755]);
var expected = [
{ point: { x: 725.96132, y: 385.54807 }, time: 0 }
];
testIntersections(curve1.getIntersections(curve2), expected);
var path1 = createPath(curve1);
var path2 = createPath(curve2);
testIntersections(path1.getIntersections(path2), expected);
});

View file

@ -12,190 +12,256 @@
QUnit.module('Rectangle'); QUnit.module('Rectangle');
test('new Rectangle(new Point(10, 20), new Size(30, 40));', function() { test('new Rectangle(Point, Size);', function() {
var rect = new Rectangle(new Point(10, 20), new Size(30, 40)); var rect = new Rectangle(new Point(10, 20), new Size(30, 40));
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
}); });
test('new Rectangle({ point: [10, 20], size: [30, 40] });', function() { test('new Rectangle({ point, size });', function() {
var rect = new Rectangle({ point: [10, 20], size: [30, 40] }); var rect = new Rectangle({ point: [10, 20], size: [30, 40] });
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
});
test('new Rectangle({ point: new Point(10, 20), size: new Size(30, 40)});', function() {
var rect = new Rectangle({ point: new Point(10, 20), size: new Size(30, 40)}); var rect = new Rectangle({ point: new Point(10, 20), size: new Size(30, 40)});
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
}); });
test('new Rectangle([10, 20], [30, 40]);', function() { test('new Rectangle(Array, Array);', function() {
var rect = new Rectangle([10, 20], [30, 40]); var rect = new Rectangle([10, 20], [30, 40]);
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
}); });
test('new Rectangle({from: [10, 20], to: [30, 40]});', function() { test('new Rectangle(Point, Point);', function() {
var rect = new Rectangle({from: [10, 20], to: [30, 40]});
equals(rect.toString(), '{ x: 10, y: 20, width: 20, height: 20 }');
});
test('new Rectangle(new Point(10, 20), new Point(30, 40));', function() {
var rect = new Rectangle(new Point(10, 20), new Point(30, 40)); var rect = new Rectangle(new Point(10, 20), new Point(30, 40));
equals(rect.toString(), '{ x: 10, y: 20, width: 20, height: 20 }'); equals(rect, new Rectangle(10, 20, 20, 20));
}); });
test('new Rectangle(10, 20, 30, 40);', function() { test('new Rectangle({ from, to });', function() {
var rect = new Rectangle({from: [10, 20], to: [30, 40]});
equals(rect, new Rectangle(10, 20, 20, 20));
});
test('new Rectangle(x, y, width, height);', function() {
var rect = new Rectangle(10, 20, 30, 40); var rect = new Rectangle(10, 20, 30, 40);
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
}); });
test('new Rectangle({x: 10, y: 20, width: 30, height: 40});', function() { test('new Rectangle({ x, y, width, height });', function() {
var rect = new Rectangle({x: 10, y: 20, width: 30, height: 40}); var rect = new Rectangle({x: 10, y: 20, width: 30, height: 40});
equals(rect.toString(), '{ x: 10, y: 20, width: 30, height: 40 }'); equals(rect, new Rectangle(10, 20, 30, 40));
}); });
test('get size', function() { test('new Rectangle(object)', function() {
var expected = new Rectangle(100, 50, 100, 200);
equals(function() {
return new Rectangle({
top: expected.top,
right: expected.right,
bottom: expected.bottom,
left: expected.left
});
}, expected);
function testProperties(key1, key2) {
var obj = {};
obj[key1] = expected[key1];
obj[key2] = expected[key2];
var rect = new Rectangle(obj);
equals(rect, expected, 'new Rectangle({ ' + key1 + ', ' + key2 + ' });');
}
var tests = [
['center', 'size'],
['topLeft', 'size'],
['topRight', 'size'],
['bottomRight', 'size'],
['bottomLeft', 'size'],
['leftCenter', 'size'],
['topCenter', 'size'],
['rightCenter', 'size'],
['bottomCenter', 'size'],
['topLeft', 'bottomRight'],
['topRight', 'bottomLeft'],
['topLeft', 'bottomCenter'],
['topLeft', 'rightCenter'],
['topRight', 'bottomCenter'],
['topRight', 'leftCenter'],
['bottomLeft', 'topCenter'],
['bottomLeft', 'rightCenter'],
['bottomRight', 'topCenter'],
['bottomRight', 'leftCenter']
];
tests.forEach(function(test) {
testProperties(test[0], test[1]);
testProperties(test[1], test[0]);
});
});
test('rect.left / rect.top VS rect.right / rect.bottom', function() {
var rect = new Rectangle({
point: [0,0],
size: [100, 100],
});
rect.left -= 10;
rect.top -= 10;
equals(rect.right, 90);
equals(rect.bottom, 90);
var rect = new Rectangle([0, 0], [100, 100]);
rect.left -= 10;
rect.top -= 10;
equals(rect.right, 90);
equals(rect.bottom, 90);
var rect = new Rectangle({
topLeft: [0,0],
bottomRight: [100, 100],
});
rect.left -= 10;
rect.top -= 10;
equals(rect.right, 100);
equals(rect.bottom, 100);
});
test('rect.size', function() {
var rect = new Rectangle(10, 10, 20, 30); var rect = new Rectangle(10, 10, 20, 30);
equals(function() { equals(function() {
return rect.size.equals([20, 30]); return rect.size.equals([20, 30]);
}, true); }, true);
rect.size = [30, 40];
equals(rect, new Rectangle(10, 10, 30, 40));
}); });
test('set size', function() { test('rect.center', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 30);
rect.size = new Size(30, 30); equals(function() {
equals(rect.toString(), '{ x: 10, y: 10, width: 30, height: 30 }'); return rect.size;
}, new Size(20, 30));
equals(function() {
return rect.center;
}, new Point(20, 25));
rect.center = [100, 100];
equals(function() {
return rect.center;
}, new Point(100, 100));
equals(function() {
return rect.size;
}, new Size(20, 30));
rect.center = [200, 200];
equals(function() {
return rect.center;
}, new Point(200, 200));
equals(function() {
return rect.size;
}, new Size(20, 30));
}); });
test('topLeft', function() { test('rect.topLeft', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.topLeft; var point = rect.topLeft;
equals(point.toString(), '{ x: 10, y: 10 }'); equals(point, { x: 10, y: 10 });
});
test('set topLeft', function() {
var rect = new Rectangle(10, 10, 20, 20);
rect.topLeft = [10, 15]; rect.topLeft = [10, 15];
var point = rect.topLeft; var point = rect.topLeft;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get topRight', function() { test('rect.topRight', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.topRight; var point = rect.topRight;
equals(point.toString(), '{ x: 30, y: 10 }'); equals(point, { x: 30, y: 10 });
});
test('set topRight', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.topRight = [10, 15]; rect.topRight = [10, 15];
var point = rect.topRight; var point = rect.topRight;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get bottomLeft', function() { test('rect.bottomLeft', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.bottomLeft; var point = rect.bottomLeft;
equals(point.toString(), '{ x: 10, y: 30 }'); equals(point, { x: 10, y: 30 });
});
test('set bottomLeft', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.bottomLeft = [10, 15]; rect.bottomLeft = [10, 15];
var point = rect.bottomLeft; var point = rect.bottomLeft;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get bottomRight', function() { test('rect.bottomRight', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.bottomRight; var point = rect.bottomRight;
equals(point.toString(), '{ x: 30, y: 30 }'); equals(point, { x: 30, y: 30 });
});
test('set bottomRight', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.bottomRight = [10, 15]; rect.bottomRight = [10, 15];
var point = rect.bottomRight; var point = rect.bottomRight;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get bottomCenter', function() { test('rect.bottomCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.bottomCenter; var point = rect.bottomCenter;
equals(point.toString(), '{ x: 20, y: 30 }'); equals(point, { x: 20, y: 30 });
});
test('set bottomCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.bottomCenter = [10, 15]; rect.bottomCenter = [10, 15];
var point = rect.bottomCenter; var point = rect.bottomCenter;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get topCenter', function() { test('rect.topCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.topCenter; var point = rect.topCenter;
equals(point.toString(), '{ x: 20, y: 10 }'); equals(point, { x: 20, y: 10 });
});
test('set topCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.topCenter = [10, 15]; rect.topCenter = [10, 15];
var point = rect.topCenter; var point = rect.topCenter;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get leftCenter', function() { test('rect.leftCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.leftCenter; var point = rect.leftCenter;
equals(point.toString(), '{ x: 10, y: 20 }'); equals(point, { x: 10, y: 20 });
});
test('set leftCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.leftCenter = [10, 15]; rect.leftCenter = [10, 15];
var point = rect.leftCenter; var point = rect.leftCenter;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('get rightCenter', function() { test('rect.rightCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
var point = rect.rightCenter; var point = rect.rightCenter;
equals(point.toString(), '{ x: 30, y: 20 }'); equals(point, { x: 30, y: 20 });
});
test('set rightCenter', function() {
var rect = new Rectangle(10, 10, 20, 20); var rect = new Rectangle(10, 10, 20, 20);
rect.rightCenter = [10, 15]; rect.rightCenter = [10, 15];
var point = rect.rightCenter; var point = rect.rightCenter;
equals(point.toString(), '{ x: 10, y: 15 }'); equals(point, { x: 10, y: 15 });
}); });
test('intersects(rect)', function() { test('rect1.intersects(rect2)', function() {
var rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); var rect1 = new Rectangle(160, 270, 20, 20);
var rect2 = { x: 195, y: 301, width: 19, height: 19 }; var rect2 = new Rectangle(195, 301, 19, 19);
equals(function() { equals(function() {
return rect1.intersects(rect2); return rect1.intersects(rect2);
}, false); }, false);
rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); rect1 = new Rectangle(160, 270, 20, 20);
rect2 = { x: 170.5, y: 280.5, width: 19, height: 19 }; rect2 = new Rectangle(170.5, 280.5, 19, 19);
equals(function() { equals(function() {
return rect1.intersects(rect2); return rect1.intersects(rect2);
}, true); }, true);
}); });
test('contains(rect)', function() { test('rect1.contains(rect2)', function() {
var rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); var rect1 = new Rectangle(160, 270, 20, 20);
var rect2 = { x: 195, y: 301, width: 19, height: 19 }; var rect2 = new Rectangle(195, 301, 19, 19);
equals(function() { equals(function() {
return rect1.contains(rect2); return rect1.contains(rect2);
}, false); }, false);
rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); rect1 = new Rectangle(160, 270, 20, 20);
rect2 = new Rectangle({ x: 170.5, y: 280.5, width: 19, height: 19 }); rect2 = new Rectangle(170.5, 280.5, 19, 19);
equals(function() { equals(function() {
return rect1.contains(rect2); return rect1.contains(rect2);
}, false); }, false);
rect1 = new Rectangle({ x: 299, y: 161, width: 137, height: 129 }); rect1 = new Rectangle(299, 161, 137, 129);
rect2 = new Rectangle({ x: 340, y: 197, width: 61, height: 61 }); rect2 = new Rectangle(340, 197, 61, 61);
equals(function() { equals(function() {
return rect1.contains(rect2); return rect1.contains(rect2);
}, true); }, true);
@ -204,8 +270,8 @@ test('contains(rect)', function() {
}, false); }, false);
}); });
test('contains(point)', function() { test('rect.contains(point)', function() {
var rect = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); var rect = new Rectangle(160, 270, 20, 20);
var point = new Point(166, 280); var point = new Point(166, 280);
equals(function() { equals(function() {
return rect.contains(point); return rect.contains(point);
@ -216,121 +282,33 @@ test('contains(point)', function() {
}, false); }, false);
}); });
test('intersect(rect)', function() { test('rect1.intersect(rect2)', function() {
var rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); var rect1 = new Rectangle(160, 270, 20, 20);
var rect2 = { x: 170.5, y: 280.5, width: 19, height: 19 }; var rect2 = new Rectangle(170.5, 280.5, 19, 19);
var intersected = rect1.intersect(rect2); var intersected = rect1.intersect(rect2);
equals(function() { equals(function() {
return intersected.equals({ x: 170.5, y: 280.5, width: 9.5, height: 9.5 }); return intersected.equals(new Rectangle(170.5, 280.5, 9.5, 9.5));
}, true); }, true);
}); });
test('unite(rect)', function() { test('rect1.unite(rect2)', function() {
var rect1 = new Rectangle({ x: 160, y: 270, width: 20, height: 20 }); var rect1 = new Rectangle(160, 270, 20, 20);
var rect2 = { x: 170.5, y: 280.5, width: 19, height: 19 }; var rect2 = new Rectangle(170.5, 280.5, 19, 19);
var united = rect1.unite(rect2); var united = rect1.unite(rect2);
equals(function() { equals(function() {
return united.equals({ x: 160, y: 270, width: 29.5, height: 29.5 }); return united.equals(new Rectangle(160, 270, 29.5, 29.5));
}, true); }, true);
}); });
test('include(point)', function() { test('rect.include(point)', function() {
var rect1 = new Rectangle({ x: 95, y: 151, width: 20, height: 20 }); var rect1 = new Rectangle(95, 151, 20, 20);
var included = rect1.include([50, 50]); var included = rect1.include([50, 50]);
equals(function() { equals(function() {
return included.equals({ x: 50, y: 50, width: 65, height: 121 }); return included.equals(new Rectangle(50, 50, 65, 121));
}, true); }, true);
}); });
test('toString()', function() { test('rect.toString()', function() {
var string = new Rectangle(10, 20, 30, 40).toString(); var string = new Rectangle(10, 20, 30, 40).toString();
equals(string, '{ x: 10, y: 20, width: 30, height: 40 }'); equals(string, '{ x: 10, y: 20, width: 30, height: 40 }');
}); });
test('new Rectangle(object)', function() {
equals(function() {
return new Rectangle({
center: [50, 100],
size: [100, 200]
}).toString();
}, '{ x: 0, y: 0, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
topLeft: [100, 50],
size: [100, 200]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
size: [100, 200],
topLeft: [100, 50]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
topRight: [200, 50],
size: [100, 200]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
size: [100, 200],
topRight: [200, 50]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
bottomRight: [200, 250],
size: [100, 200]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
size: [100, 200],
bottomRight: [200, 250]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
bottomLeft: [100, 250],
size: [100, 200]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
size: [100, 200],
bottomLeft: [100, 250]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
topRight: [200, 50],
bottomLeft: [100, 250]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
topLeft: [100, 50],
bottomRight: [200, 250]
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
equals(function() {
return new Rectangle({
top: 50,
right: 200,
bottom: 250,
left: 100
}).toString();
}, '{ x: 100, y: 50, width: 100, height: 200 }');
});

View file

@ -57,24 +57,38 @@ test('setting Project#currentStyle to an object', function() {
test('setting Path#style to an object', function() { test('setting Path#style to an object', function() {
var path = new Path(); var path = new Path();
path.strokeWidth = 10;
path.style = { path.style = {
fillColor: 'red', fillColor: 'red',
strokeColor: 'green' strokeColor: 'green'
}; };
equals(path.fillColor, new Color('red'), 'path.fillColor'); equals(path.fillColor, new Color('red'), 'path.fillColor');
equals(path.strokeColor, new Color('green'), 'path.strokeColor'); equals(path.strokeColor, new Color('green'), 'path.strokeColor');
equals(path.strokeWidth, 10,
'path.strokeWidth, set outside object should not be cleared');
equals(path.style.fillColor, new Color('red'), 'path.style.fillColor');
equals(path.style.strokeColor, new Color('green'), 'path.style.strokeColor');
equals(path.style.strokeWidth, 10,
'path.style.strokeWidth, set outside object should not be cleared');
}); });
test('setting Group#style to an object', function() { test('setting Group#style to an object', function() {
var group = new Group(); var group = new Group();
var path = new Path(); var path = new Path();
group.addChild(path); group.addChild(path);
group.strokeWidth = 10;
group.style = { group.style = {
fillColor: 'red', fillColor: 'red',
strokeColor: 'green' strokeColor: 'green'
}; };
equals(path.fillColor, new Color('red'), 'path.fillColor'); equals(path.fillColor, new Color('red'), 'path.fillColor');
equals(path.strokeColor, new Color('green'), 'path.strokeColor'); equals(path.strokeColor, new Color('green'), 'path.strokeColor');
equals(path.strokeWidth, 10,
'path.strokeWidth, set outside object should not be cleared');
equals(path.style.fillColor, new Color('red'), 'path.style.fillColor');
equals(path.style.strokeColor, new Color('green'), 'path.style.strokeColor');
equals(path.style.strokeWidth, 10,
'path.style.strokeWidth, set outside object should not be cleared');
}); });
test('getting Group#fillColor', function() { test('getting Group#fillColor', function() {

View file

@ -97,7 +97,7 @@ test('SymbolDefinition item selection', function() {
path.selected = true; path.selected = true;
var definition = new SymbolDefinition(path); var definition = new SymbolDefinition(path);
equals(function() { equals(function() {
return definition.item.selected === false; return definition.item.selected == false;
}, true); }, true);
equals(function() { equals(function() {
return paper.project.selectedItems.length === 0; return paper.project.selectedItems.length === 0;