diff --git a/.gitignore b/.gitignore index b0a5c349..8dc9c779 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /node_modules/ -/dist/ +/dist/*/ diff --git a/.jshintrc b/.jshintrc index 5e8108bf..01894348 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,16 +1,17 @@ { + "esversion": 5, "browser": true, "node": true, "wsh": true, "evil": true, - "trailing": false, - "smarttabs": false, - "sub": true, "supernew": true, "laxbreak": true, "eqeqeq": false, "eqnull": true, "loopfunc": true, "boss": true, - "shadow": true + "shadow": true, + "funcscope": false, + "latedef": "nofunc", + "freeze": true } diff --git a/.travis.yml b/.travis.yml index 8ce3097f..cc943b55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,6 @@ script: - gulp jshint - gulp minify - gulp test -- gulp dist +- gulp zip after_script: - '[ "${TRAVIS_BRANCH}" = "develop" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ] && travis/deploy-prebuilt.sh' diff --git a/CHANGELOG.md b/CHANGELOG.md index f28bf76a..f57f3b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,38 @@ # Change Log -All notable changes to Paper.js shall be documented in this file, following -common [CHANGELOG](http://keepachangelog.com/) conventions. As of `0.10.0`, -Paper.js adheres to [Semantic Versioning](http://semver.org/). -## `0.10.0` (Unreleased) +## `0.10.3` (Unreleased) + +### Changed +- Loosely couple Node.js / Electron code to Canvas module, and treat its absence + like a headless web worker context in the browser (#1103). + +### Fixed +- Prevent `Path#getStrokeBounds(matrix)` from accidentally modifying segments + (#1102). +- Compatibility with JSPM (#1104). + +## `0.10.2` + +### Fixed +- Get published version to work correctly in Bower again. + +## `0.10.1` + +### Fixed +- Correct a few issues with documentation and NPM publishing that slipped + through in the `0.10.0` release. + +## `0.10.0` ### Preamble This is a huge release for Paper.js as we aim for a version `1.0.0` release -later this year. There are many items in the changelog (and many more items not -in the changelog) so here a high-level overview to frame the long list of -changes: +later this year. As of this version, all notable changes are documented in the +change-log following common [CHANGELOG](http://keepachangelog.com/) conventions. +Paper.js now also adheres to [Semantic Versioning](http://semver.org/). + +There are many items in the changelog (and many more items not in the changelog) +so here a high-level overview to frame the long list of changes: - Boolean operations have been improved and overhauled for reliability and efficiency. These include the path functions to unite, intersect, subtract, @@ -221,7 +243,7 @@ contribute to the code. invertible transformations (#558). - Scaling shadows now works correctly with browser- and view-zoom (#831). - `Path#arcTo()` correctly handles zero sizes. -- `#importSVG()` handles onLoad and onError callbacks for string inputs that +- `#importSVG()` handles `onLoad` and `onError` callbacks for string inputs that load external resources (#827). - `#importJSON()` and `#exportJSON()` now handle non-`Item` objects correctly (#392). @@ -248,6 +270,8 @@ contribute to the code. rounding (#1045). - Improve reliability of fat-line clipping for curves that are very similar (#904). +- Improve precision of `Numerical.solveQuadratic()` and + `Numerical.solveCubic()` for edge-cases (#1085). ### Removed - Canvas attributes "resize" and "data-paper-resize" no longer cause paper to diff --git a/bower.json b/bower.json index b0dd0e83..782f4801 100644 --- a/bower.json +++ b/bower.json @@ -14,14 +14,14 @@ ], "main": "dist/paper-full.js", "ignore": [ - "build", - "components", - "dist/paper-node.js", - "projects", - "node_modules", "package.json", + "gulpfile.js", + "gulp", + "node_modules", + "projects", "src", - "test" + "test", + "travis" ], "keywords": [ "vector", diff --git a/component.json b/component.json deleted file mode 100644 index 485411c8..00000000 --- a/component.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "paper", - "version": "0.9.25", - "description": "The Swiss Army Knife of Vector Graphics Scripting", - "license": "MIT", - "repo": "paperjs/paper.js", - "main": [ - "dist/paper-full.js" - ], - "scripts": [ - "dist/paper-full.js" - ], - "files": [ - "AUTHORS.md", - "LICENSE.txt", - "README.md" - ], - "keywords": [ - "vector", - "graphic", - "graphics", - "2d", - "geometry", - "bezier", - "curve", - "curves", - "path", - "paths", - "canvas", - "svg", - "paper", - "paper.js" - ] -} diff --git a/dist/paper-core.js b/dist/paper-core.js new file mode 120000 index 00000000..37e257c7 --- /dev/null +++ b/dist/paper-core.js @@ -0,0 +1 @@ +../src/load.js \ No newline at end of file diff --git a/dist/paper-full.js b/dist/paper-full.js new file mode 120000 index 00000000..37e257c7 --- /dev/null +++ b/dist/paper-full.js @@ -0,0 +1 @@ +../src/load.js \ No newline at end of file diff --git a/gulp/jsdoc b/gulp/jsdoc index 4a90c050..b6f36edb 160000 --- a/gulp/jsdoc +++ b/gulp/jsdoc @@ -1 +1 @@ -Subproject commit 4a90c0501d3d0a1f5fec2363e9dce623d0758401 +Subproject commit b6f36edba33690cee908cadcb24515ec5be97b1d diff --git a/gulp/tasks/build.js b/gulp/tasks/build.js index b0c4636b..ba2397e7 100644 --- a/gulp/tasks/build.js +++ b/gulp/tasks/build.js @@ -16,8 +16,7 @@ var gulp = require('gulp'), uncomment = require('gulp-uncomment'), whitespace = require('gulp-whitespace'), del = require('del'), - extend = require('extend'), - options = require('../utils/options.js')({ suffix: true }); + options = require('../utils/options.js'); // Options to be used in Prepro.js preprocessing through the global __options // object, merged in with the options required above. @@ -48,10 +47,10 @@ buildNames.forEach(function(name) { evaluate: ['src/constants.js'], setup: function() { // Return objects to be defined in the preprocess-scope. - // Note that this would be merge in with already existing + // Note that this would be merged in with already existing // objects. return { - __options: extend({}, options, buildOptions[name]) + __options: Object.assign({}, options, buildOptions[name]) }; } })) diff --git a/gulp/tasks/dist.js b/gulp/tasks/dist.js index 33f0dc9e..b86ce6b9 100644 --- a/gulp/tasks/dist.js +++ b/gulp/tasks/dist.js @@ -15,7 +15,9 @@ var gulp = require('gulp'), merge = require('merge-stream'), zip = require('gulp-zip'); -gulp.task('dist', ['minify', 'docs', 'clean:dist'], function() { +gulp.task('dist', ['build', 'minify', 'docs']); + +gulp.task('zip', ['clean:zip', 'dist'], function() { return merge( gulp.src([ 'dist/paper-full*.js', @@ -31,7 +33,7 @@ gulp.task('dist', ['minify', 'docs', 'clean:dist'], function() { .pipe(gulp.dest('dist')); }); -gulp.task('clean:dist', function() { +gulp.task('clean:zip', function() { return del([ 'dist/paperjs.zip' ]); diff --git a/gulp/tasks/docs.js b/gulp/tasks/docs.js index 13249a5a..217b7293 100644 --- a/gulp/tasks/docs.js +++ b/gulp/tasks/docs.js @@ -14,7 +14,7 @@ var gulp = require('gulp'), del = require('del'), rename = require('gulp-rename'), shell = require('gulp-shell'), - options = require('../utils/options.js')({ suffix: true }); + options = require('../utils/options.js'); var docOptions = { local: 'docs', // Generates the offline docs @@ -22,7 +22,7 @@ var docOptions = { }; gulp.task('docs', ['docs:local', 'build:full'], function() { - gulp.src('dist/paper-full.js') + return gulp.src('dist/paper-full.js') .pipe(rename({ basename: 'paper' })) .pipe(gulp.dest('dist/docs/assets/js/')); }); diff --git a/gulp/tasks/load.js b/gulp/tasks/load.js index f34fe48b..1d301aab 100644 --- a/gulp/tasks/load.js +++ b/gulp/tasks/load.js @@ -21,5 +21,5 @@ gulp.task('load', ['clean:load'], function() { }); gulp.task('clean:load', function() { - return del([ 'dist/paper-full.js', 'dist/paper-core.js', 'dist/node/**' ]); + return del([ 'dist/*.js', 'dist/node/**' ]); }); diff --git a/gulp/tasks/publish.js b/gulp/tasks/publish.js index 8411ce0b..5ddbe2fb 100644 --- a/gulp/tasks/publish.js +++ b/gulp/tasks/publish.js @@ -11,35 +11,57 @@ */ var gulp = require('gulp'), - addSrc = require('gulp-add-src'), bump = require('gulp-bump'), git = require('gulp-git-streamed'), + run = require('run-sequence'), shell = require('gulp-shell'), - options = require('../utils/options.js')({ suffix: false }); + options = require('../utils/options.js'); -gulp.task('publish', ['publish:bump', 'publish:release']); - -gulp.task('publish:bump', function() { - return gulp.src([ 'package.json', 'component.json' ]) - .pipe(bump({ version: options.version })) - .pipe(gulp.dest('./')) - .pipe(addSrc('src/options.js')) - .pipe(git.add()); -}); - -gulp.task('publish:release', function() { +gulp.task('publish', function() { if (options.branch !== 'develop') { throw new Error('Publishing is only allowed on the develop branch.'); } + return run( + 'publish:version', + 'publish:dist', + 'publish:commit', + 'publish:release', + 'publish:load' + ); +}); + +gulp.task('publish:version', function() { + // Reset the version value since we're executing this on the develop branch, + // but we don't wan the published version suffixed with '-develop'. + options.resetVersion(); + return gulp.src([ 'package.json' ]) + .pipe(bump({ version: options.version })) + .pipe(gulp.dest('.')); +}); + +gulp.task('publish:dist', ['dist']); + +gulp.task('publish:commit', function() { var message = 'Release version ' + options.version; return gulp.src('.') + .pipe(git.checkout('develop')) + .pipe(git.add()) .pipe(git.commit(message)) - .pipe(git.tag('v' + options.version, message)) + .pipe(git.tag('v' + options.version, message)); +}); + +gulp.task('publish:release', function() { + return gulp.src('.') .pipe(git.checkout('master')) .pipe(git.merge('develop', { args: '-X theirs' })) - .pipe(git.push('origin', 'master' )) - .pipe(git.push('origin', 'develop' )) - .pipe(git.push(null, null, { args: '--tags' } )) - .pipe(shell('npm publish')) - .pipe(git.checkout('develop')); + .pipe(git.push('origin', ['master', 'develop'], { args: '--tags' })) + .pipe(shell('npm publish')); +}); + +gulp.task('publish:load', ['load'], function() { + return gulp.src('dist') + .pipe(git.checkout('develop')) + .pipe(git.add()) + .pipe(git.commit('Switch back to load.js versions on develop branch.')) + .pipe(git.push('origin', 'develop')); }); diff --git a/gulp/tasks/test.js b/gulp/tasks/test.js index 22ebb275..79f1adbb 100644 --- a/gulp/tasks/test.js +++ b/gulp/tasks/test.js @@ -11,8 +11,8 @@ */ var gulp = require('gulp'), - qunits = require('gulp-qunits'), gutil = require('gulp-util'), + qunits = require('gulp-qunits'), webserver = require('gulp-webserver'); gulp.task('test', ['test:phantom', 'test:node']); diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js index ce138803..5864e895 100644 --- a/gulp/tasks/watch.js +++ b/gulp/tasks/watch.js @@ -14,7 +14,6 @@ var gulp = require('gulp'), path = require('path'), gutil = require('gulp-util'); - gulp.task('watch', function () { gulp.watch('src/**/*.js', ['jshint']) .on('change', function(event) { diff --git a/gulp/utils/options.js b/gulp/utils/options.js index 1b6b9111..bfd3763e 100644 --- a/gulp/utils/options.js +++ b/gulp/utils/options.js @@ -11,7 +11,6 @@ */ var execSync = require('child_process').execSync, - extend = require('extend'), // Require the __options object, so we have access to the version number and // make amendments, e.g. the release date. options = require('../../src/options.js'); @@ -20,19 +19,20 @@ function git(command) { return execSync('git ' + command).toString().trim(); } +options.date = git('log -1 --pretty=format:%ad'); +options.branch = git('rev-parse --abbrev-ref HEAD'); + // Get the date of the last commit from this branch for release date: -var date = git('log -1 --pretty=format:%ad'), - branch = git('rev-parse --abbrev-ref HEAD'); +var version = options.version, + branch = options.branch; -extend(options, { - date: date, - branch: branch, - // If we're not on the master branch, use the branch name as a suffix: - suffix: branch === 'master' ? '' : '-' + branch -}); +// If we're not on the master branch, use the branch name as a suffix: +if (branch !== 'master') + options.version += '-' + branch; -module.exports = function(opts) { - return extend({}, options, opts && opts.suffix && { - version: options.version + options.suffix - }); -}; +// Allow the removal of the suffix again, as needed by the publish task. +options.resetVersion = function() { + options.version = version; +} + +module.exports = options; diff --git a/package.json b/package.json index 21faf4a7..0612ca4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paper", - "version": "0.9.25", + "version": "0.10.2", "description": "The Swiss Army Knife of Vector Graphics Scripting", "license": "MIT", "homepage": "http://paperjs.org", @@ -15,12 +15,19 @@ ], "main": "dist/paper-full.js", "scripts": { - "lint": "jshint src", - "prepublish": "gulp minify", + "precommit": "gulp jshint", + "prepush": "gulp test", + "build": "gulp build", + "dist": "gulp dist", + "zip": "gulp zip", + "docs": "gulp docs", + "load": "gulp load", + "jshint": "gulp jshint", "test": "gulp test" }, "files": [ "AUTHORS.md", + "CHANGELOG.md", "dist/", "examples/", "LICENSE.txt", @@ -30,7 +37,7 @@ "node": ">=4.0.0 <7.0.0" }, "dependencies": { - "jsdom": "^8.3.0", + "jsdom": "^9.4.0", "source-map-support": "^0.4.0" }, "optionalDependencies": { @@ -38,38 +45,39 @@ }, "devDependencies": { "acorn": "~0.5.0", - "del": "^2.2.0", - "extend": "^3.0.0", - "gulp": "^3.9.0", - "gulp-add-src": "^0.2.0", - "gulp-bump": "^1.0.0", + "del": "^2.2.1", + "gulp": "^3.9.1", + "gulp-bump": "^2.2.0", "gulp-cached": "^1.1.0", - "gulp-git-streamed": "^1.0.0", + "gulp-git-streamed": "^1.8.0", "gulp-jshint": "^2.0.0", "gulp-prepro": "^2.4.0", - "gulp-qunits": "^2.0.1", + "gulp-qunits": "^2.1.0", "gulp-rename": "^1.2.2", "gulp-shell": "^0.5.2", - "gulp-symlink": "^2.1.3", - "gulp-uglify": "^1.5.1", + "gulp-symlink": "^2.1.4", + "gulp-uglify": "^1.5.4", "gulp-uncomment": "^0.3.0", - "gulp-util": "^3.0.0", + "gulp-util": "^3.0.7", "gulp-webserver": "^0.9.1", "gulp-whitespace": "^0.1.0", - "gulp-zip": "^3.0.2", - "jshint": "2.8.x", + "gulp-zip": "^3.2.0", + "husky": "^0.11.4", + "jshint": "^2.9.2", "jshint-summary": "^0.4.0", "merge-stream": "^1.0.0", "prepro": "^2.4.0", - "qunitjs": "^1.20.0", + "qunitjs": "^1.23.0", "require-dir": "^0.3.0", - "resemblejs": "^2.1.0", - "stats.js": "0.0.14-master", + "resemblejs": "^2.2.1", + "run-sequence": "^1.2.2", + "stats.js": "0.16.0", "straps": "^1.9.0" }, "browser": { "canvas": false, "jsdom": false, + "jsdom/lib/jsdom/living/generated/utils": false, "source-map-support": false, "./dist/node/window.js": false, "./dist/node/extend.js": false diff --git a/src/core/Emitter.js b/src/core/Emitter.js index a83275ba..69ffbfc9 100644 --- a/src/core/Emitter.js +++ b/src/core/Emitter.js @@ -80,10 +80,15 @@ var Emitter = { var handlers = this._callbacks && this._callbacks[type]; if (!handlers) return false; - var args = [].slice.call(arguments, 1); + var args = [].slice.call(arguments, 1), + // Set the current target to `this` if the event object defines + // #target but not #currentTarget. + setTarget = event && event.target && !event.currentTarget; // Create a clone of the handlers list so changes caused by on / off // won't throw us off track here: handlers = handlers.slice(); + if (setTarget) + event.currentTarget = this; for (var i = 0, l = handlers.length; i < l; i++) { if (handlers[i].apply(this, args) === false) { // If the handler returns false, prevent the default behavior @@ -94,6 +99,8 @@ var Emitter = { break; } } + if (setTarget) + delete event.currentTarget; return true; }, diff --git a/src/core/PaperScript.js b/src/core/PaperScript.js index bf29a2e2..560094d0 100644 --- a/src/core/PaperScript.js +++ b/src/core/PaperScript.js @@ -14,7 +14,7 @@ * @name PaperScript * @namespace */ -Base.exports.PaperScript = (function() { +Base.exports.PaperScript = function() { // Locally turn of exports and define for inlined acorn. // Just declaring the local vars is enough, as they will be undefined. var exports, define, @@ -347,7 +347,7 @@ Base.exports.PaperScript = (function() { } if (/^(inline|both)$/.test(sourceMaps)) { code += "\n//# sourceMappingURL=data:application/json;base64," - + window.btoa(unescape(encodeURIComponent( + + self.btoa(unescape(encodeURIComponent( JSON.stringify(map)))); } code += "\n//# sourceURL=" + (url || 'paperscript'); @@ -590,4 +590,4 @@ Base.exports.PaperScript = (function() { }; // Pass on `this` as the binding object, so we can reference Acorn both in // development and in the built library. -}).call(this); +}.call(this); diff --git a/src/event/Key.js b/src/event/Key.js index 9b0d51cf..6b7b0d85 100644 --- a/src/event/Key.js +++ b/src/event/Key.js @@ -45,7 +45,7 @@ var Key = new function() { keyMap = {}, // Map for currently pressed keys charMap = {}, // key -> char mappings for pressed keys metaFixMap, // Keys that will not receive keyup events due to Mac bug - downKey; // The key from the keydown event, if it wasn't handled already + downKey, // The key from the keydown event, if it wasn't handled already // Use new Base() to convert into a Base object, for #toString() modifiers = new Base({ diff --git a/src/event/MouseEvent.js b/src/event/MouseEvent.js index 2e047898..0a3ca604 100644 --- a/src/event/MouseEvent.js +++ b/src/event/MouseEvent.js @@ -30,7 +30,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ this.type = type; this.event = event; this.point = point; - this._target = target; + this.target = target; this.delta = delta; }, @@ -51,19 +51,24 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ * @type Point */ - // DOCS: document MouseEvent#target /** + * The item that dispatched the event. It is different from + * {@link #currentTarget} when the event handler is called during + * the bubbling phase of the event. + * * @name MouseEvent#target * @type Item */ - getTarget: function() { - // #_target may be a hitTest() function, in which case we need to - // execute and override it the first time #target is requested. - var target = this._target; - if (typeof target === 'function') - target = this._target = target(); - return target; - }, + + /** + * The current target for the event, as the event traverses the scene graph. + * It always refers to the element the event handler has been attached to as + * opposed to {@link #target} which identifies the element on + * which the event occurred. + * + * @name MouseEvent#currentTarget + * @type Item + */ // DOCS: document MouseEvent#delta /** @@ -77,7 +82,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ toString: function() { return "{ type: '" + this.type + "', point: " + this.point - + ', target: ' + this.getTarget() + + ', target: ' + this.target + (this.delta ? ', delta: ' + this.delta : '') + ', modifiers: ' + this.getModifiers() + ' }'; diff --git a/src/export.js b/src/export.js index 60a61a5c..6b9358ea 100644 --- a/src/export.js +++ b/src/export.js @@ -36,7 +36,7 @@ paper = new (PaperScope.inject(Base.exports, { // - PaperScript support in require() with sourceMaps // - exportFrames / exportImage on CanvasView if (paper.agent.node) - require('./node/extend')(paper); + require('./node/extend.js')(paper); // https://github.com/umdjs/umd if (typeof define === 'function' && define.amd) { diff --git a/src/init.js b/src/init.js index dec12155..de416b51 100644 --- a/src/init.js +++ b/src/init.js @@ -19,11 +19,12 @@ /* global document:true, window:true */ // Create a window variable valid in the paper.js scope, that points to the // native window in browsers and the emulated JSDom one in node.js -// In workers, window is null (but self is defined), so we can use the validity -// of the local window object to detect a worker-like context in the library. -var window = self ? self.window : require('./node/window'), - document = window && window.document; -// Make sure 'self' always points to a window object, also on Node.js. +// In workers, and on Node.js when no Canvas is present, `window` is null (but +// `self` is defined), so we can use the validity of the local window object to +// detect a worker-like context in the library. +// Make sure `self` always points to a window object, also on Node.js. +self = self || require('./node/window.js'); // 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. -self = self || window; +var window = self.window, + document = self.document; diff --git a/src/item/Item.js b/src/item/Item.js index 5e526262..8c56ba7c 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1893,6 +1893,7 @@ new function() { // Injection scope for hit-test functions shared with project || options.class && !(this instanceof options.class)), callback = options.match, that = this, + bounds, res; function match(hit) { @@ -1913,7 +1914,7 @@ new function() { // Injection scope for hit-test functions shared with project if (checkSelf && (options.center || options.bounds) && this._parent) { // Don't get the transformed bounds, check against transformed // points instead - var bounds = this.getInternalBounds(); + bounds = this.getInternalBounds(); if (options.center) { res = checkBounds('center', 'Center'); } diff --git a/src/item/Raster.js b/src/item/Raster.js index 8b8fba05..a7355af2 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -379,7 +379,7 @@ var Raster = Item.extend(/** @lends Raster# */{ }, setSource: function(src) { - var image = new window.Image(), + var image = new self.Image(), crossOrigin = this._crossOrigin; if (crossOrigin) image.crossOrigin = crossOrigin; diff --git a/src/net/Http.js b/src/net/Http.js index e5a304af..012f6b92 100644 --- a/src/net/Http.js +++ b/src/net/Http.js @@ -13,7 +13,7 @@ var Http = { request: function(options) { // Code borrowed from Coffee Script and extended: - var xhr = new window.XMLHttpRequest(); + var xhr = new self.XMLHttpRequest(); xhr.open((options.method || 'get').toUpperCase(), options.url, Base.pick(options.async, true)); if (options.mimeType) diff --git a/src/node/canvas.js b/src/node/canvas.js index 94dd232a..df4e8880 100644 --- a/src/node/canvas.js +++ b/src/node/canvas.js @@ -10,35 +10,41 @@ * All rights reserved. */ -var Canvas = require('canvas'), - idlUtils = require('jsdom/lib/jsdom/living/generated/utils'); - // Add some useful extensions to HTMLCanvasElement: // - HTMLCanvasElement#type, so we can switch to a PDF canvas // - Various Node Canvas methods, routed through from HTMLCanvasElement: // toBuffer, pngStream, createPNGStream, jpgStream, createJPGStream module.exports = function(window) { - var HTMLCanvasElement = window.HTMLCanvasElement; - - function getImplementation(obj) { - // Try implForWrapper() first, fall back on obj. This appears to be - // necessary on v7.2.2, but not anymore once we can switch to 8.0.0 - var impl = idlUtils.implForWrapper(obj); - return impl && impl._canvas ? impl : obj; + var Canvas; + try { + Canvas = require('canvas'); + } catch(e) { + // Remove `self.window`, so we still have the global `self` reference, + // but no `window` object: + // - On the browser, this corresponds to a worker context. + // - On Node.js, it basically means the canvas is missing or not working + // which can be treated the same way. + delete window.window; + console.info( + 'Unable to load Canvas module. Running in a headless context.'); + return; } + var idlUtils = require('jsdom/lib/jsdom/living/generated/utils'), + HTMLCanvasElement = window.HTMLCanvasElement; + // Add fake HTMLCanvasElement#type property: Object.defineProperty(HTMLCanvasElement.prototype, 'type', { get: function() { - var canvas = getImplementation(this)._canvas; + var canvas = idlUtils.implForWrapper(this)._canvas; return canvas && canvas.type || 'image'; }, set: function(type) { // Allow replacement of internal node-canvas, so we can switch to a // PDF canvas. - var impl = getImplementation(this), + var impl = idlUtils.implForWrapper(this), size = impl._canvas || impl; impl._canvas = new Canvas(size.width, size.height, type); impl._context = null; @@ -49,7 +55,7 @@ module.exports = function(window) { ['toBuffer', 'pngStream', 'createPNGStream', 'jpgStream', 'createJPGStream'] .forEach(function(key) { HTMLCanvasElement.prototype[key] = function() { - var canvas = getImplementation(this)._canvas; + var canvas = idlUtils.implForWrapper(this)._canvas; return canvas[key].apply(canvas, arguments); }; }); diff --git a/src/node/window.js b/src/node/window.js index 4d66ef84..5987e4f5 100644 --- a/src/node/window.js +++ b/src/node/window.js @@ -27,7 +27,7 @@ var document = jsdom.jsdom('', { }), window = document.defaultView; -require('./canvas')(window); +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 @@ -52,25 +52,6 @@ XMLSerializer.prototype.serializeToString = function(node) { return text; }; -function DOMParser() { -} - -DOMParser.prototype.parseFromString = function(string, contentType) { - // Create a new document, since we're supposed to always return one. - var doc = document.implementation.createHTMLDocument(''), - body = doc.body, - last; - // Set the body's HTML, then change the DOM according the specs. - body.innerHTML = string; - // Remove all top-level children () - while (last = doc.lastChild) - doc.removeChild(last); - // Insert the first child of the body at the top. - doc.appendChild(body.firstChild); - return doc; -}; - window.XMLSerializer = XMLSerializer; -window.DOMParser = DOMParser; module.exports = window; diff --git a/src/options.js b/src/options.js index e7d5fcf2..9947b2f1 100644 --- a/src/options.js +++ b/src/options.js @@ -17,7 +17,7 @@ // The paper.js version. // NOTE: Adjust value here before calling `gulp publish`, which then updates and // publishes the various JSON package files automatically. -var version = '0.9.25'; +var version = '0.10.2'; // If this file is loaded in the browser, we're in load.js mode. var load = typeof window === 'object'; diff --git a/src/paper.js b/src/paper.js index 7b44d3a3..7d30d91f 100644 --- a/src/paper.js +++ b/src/paper.js @@ -123,4 +123,4 @@ var paper = function(self, undefined) { /*#*/ include('export.js'); return paper; -}(typeof self === 'object' ? self : null); +}.call(this, typeof self === 'object' ? self : null); diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index d367fde6..03a65b04 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -170,7 +170,32 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The first Segment contained within the path. + * Specifies whether the compound-path is fully closed, meaning all its + * contained sub-paths are closed path. + * + * @bean + * @type Boolean + * @see Path#isClosed() + */ + isClosed: function() { + var children = this._children; + for (var i = 0, l = children.length; i < l; i++) { + if (!children[i]._closed) + return false; + } + return true; + }, + + setClosed: function(closed) { + var children = this._children; + for (var i = 0, l = children.length; i < l; i++) { + children[i].setClosed(closed); + } + }, + + /** + * The first Segment contained within the compound-path, a short-cut to + * calling {@link Path#getFirstSegment()} on {@link Item#getFirstChild()}. * * @bean * @type Segment @@ -181,7 +206,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The last Segment contained within the path. + * The last Segment contained within the compound-path, a short-cut to + * calling {@link Path#getLastSegment()} on {@link Item#getLastChild()}. * * @bean * @type Segment @@ -207,7 +233,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The first Curve contained within the path. + * The first Curve contained within the compound-path, a short-cut to + * calling {@link Path#getFirstCurve()} on {@link Item#getFirstChild()}. * * @bean * @type Curve @@ -218,14 +245,15 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ }, /** - * The last Curve contained within the path. + * The last Curve contained within the compound-path, a short-cut to + * calling {@link Path#getLastCurve()} on {@link Item#getLastChild()}. * * @bean * @type Curve */ getLastCurve: function() { var last = this.getLastChild(); - return last && last.getFirstCurve(); + return last && last.getLastCurve(); }, /** diff --git a/src/path/Curve.js b/src/path/Curve.js index 63de4dce..5238035b 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -680,10 +680,16 @@ statics: /** @lends Curve */{ c1 = v[coord + 2], c2 = v[coord + 4], p2 = v[coord + 6], - c = 3 * (c1 - p1), - b = 3 * (c2 - c1) - c, - a = p2 - p1 - c - b; - return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); + res = 0; + // If val is outside the curve values, no solution is possible. + if ( !(p1 < val && p2 < val && c1 < val && c2 < val || + p1 > val && p2 > val && c1 > val && c2 > val)) { + var c = 3 * (c1 - p1), + b = 3 * (c2 - c1) - c, + a = p2 - p1 - c - b; + res = Numerical.solveCubic(a, b, c, p1 - val, roots, min, max); + } + return res; }, getTimeOf: function(v, point) { @@ -835,7 +841,6 @@ statics: /** @lends Curve */{ * NOTE: padding is only used for Path.getBounds(). */ _addBounds: function(v0, v1, v2, v3, coord, padding, min, max, roots) { - padding /= 2; // strokePadding is in width, not radius // Code ported and further optimised from: // http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html function add(value, padding) { @@ -846,32 +851,49 @@ statics: /** @lends Curve */{ if (right > max[coord]) max[coord] = right; } - // Calculate derivative of our bezier polynomial, divided by 3. - // Doing so allows for simpler calculations of a, b, c and leads to the - // same quadratic roots. - var a = 3 * (v1 - v2) - v0 + v3, - b = 2 * (v0 + v2) - 4 * v1, - c = v1 - v0, - count = Numerical.solveQuadratic(a, b, c, roots), - // Add some tolerance for good roots, as t = 0, 1 are added - // separately anyhow, and we don't want joins to be added with radii - // in getStrokeBounds() - tMin = /*#=*/Numerical.CURVETIME_EPSILON, - tMax = 1 - tMin; - // Only add strokeWidth to bounds for points which lie within 0 < t < 1 - // The corner cases for cap and join are handled in getStrokeBounds() - add(v3, 0); - for (var i = 0; i < count; i++) { - var t = roots[i], - u = 1 - t; - // Test for good roots and only add to bounds if good. - if (tMin < t && t < tMax) - // Calculate bezier polynomial at t. - add(u * u * u * v0 - + 3 * u * u * t * v1 - + 3 * u * t * t * v2 - + t * t * t * v3, - padding); + + padding /= 2; // strokePadding is in width, not radius + var minPad = min[coord] - padding, + maxPad = max[coord] + padding; + // Perform a rough bounds checking first: The curve can only extend the + // current bounds if at least one value is outside the min-max range. + if ( v0 < minPad || v1 < minPad || v2 < minPad || v3 < minPad || + v0 > maxPad || v1 > maxPad || v2 > maxPad || v3 > maxPad) { + if (v1 < v0 != v1 < v3 && v2 < v0 != v2 < v3) { + // If the values of a curve are sorted, the extrema are simply + // the start and end point. + add(v0, padding); + add(v3, padding); + } else { + // Calculate derivative of our bezier polynomial, divided by 3. + // Doing so allows for simpler calculations of a, b, c and leads + // to the same quadratic roots. + var a = 3 * (v1 - v2) - v0 + v3, + b = 2 * (v0 + v2) - 4 * v1, + c = v1 - v0, + count = Numerical.solveQuadratic(a, b, c, roots), + // Add some tolerance for good roots, as t = 0, 1 are added + // separately anyhow, and we don't want joins to be added + // with radii in getStrokeBounds() + tMin = /*#=*/Numerical.CURVETIME_EPSILON, + tMax = 1 - tMin; + // Only add strokeWidth to bounds for points which lie within 0 + // < t < 1 The corner cases for cap and join are handled in + // getStrokeBounds() + add(v3, 0); + for (var i = 0; i < count; i++) { + var t = roots[i], + u = 1 - t; + // Test for good roots and only add to bounds if good. + if (tMin < t && t < tMax) + // Calculate bezier polynomial at t. + add(u * u * u * v0 + + 3 * u * u * t * v1 + + 3 * u * t * t * v2 + + t * t * t * v3, + padding); + } + } } } }}, Base.each( diff --git a/src/path/Path.js b/src/path/Path.js index 2beab9cf..8afbbcec 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1284,6 +1284,13 @@ var Path = PathItem.extend(/** @lends Path# */{ // NOTE: Documentation is in PathItem#smooth() smooth: function(options) { + var that = this, + opts = options || {}, + type = opts.type || 'asymmetric', + segments = this._segments, + length = segments.length, + closed = this._closed; + // Helper method to pick the right from / to indices. // Supports numbers and segment objects. // For numbers, the `to` index is exclusive, while for segments and @@ -1312,15 +1319,10 @@ var Path = PathItem.extend(/** @lends Path# */{ : index < 0 ? index + length : index, length - 1); } - var that = this, - opts = options || {}, - type = opts.type || 'asymmetric', - segments = this._segments, - length = segments.length, - closed = this._closed, - loop = closed && opts.from === undefined && opts.to === undefined, + var loop = closed && opts.from === undefined && opts.to === undefined, from = getIndex(opts.from, 0), to = getIndex(opts.to, length - 1); + if (from > to) { if (closed) { from -= length; @@ -2047,7 +2049,9 @@ new function() { // Scope for drawing // performance. function drawHandles(ctx, segments, matrix, size) { - var half = size / 2; + var half = size / 2, + coords = new Array(6), + pX, pY; function drawHandle(index) { var hX = coords[index], @@ -2063,13 +2067,12 @@ new function() { // Scope for drawing } } - var coords = new Array(6); for (var i = 0, l = segments.length; i < l; i++) { - var segment = segments[i]; + var segment = segments[i], + selection = segment._selection; segment._transformCoordinates(matrix, coords); - var selection = segment._selection, - pX = coords[0], - pY = coords[1]; + pX = coords[0]; + pY = coords[1]; if (selection & /*#=*/SegmentSelection.HANDLE_IN) drawHandle(2); if (selection & /*#=*/SegmentSelection.HANDLE_OUT) @@ -2699,22 +2702,19 @@ statics: { _addBevelJoin: function(segment, join, radius, miterLimit, matrix, strokeMatrix, addPoint, isArea) { - // Handles both 'bevel' and 'miter' joins, as they share a lot of code. + // Handles both 'bevel' and 'miter' joins, as they share a lot of code, + // using different matrices to transform segment points and stroke + // vectors to support Style#strokeScaling. var curve2 = segment.getCurve(), curve1 = curve2.getPrevious(), - point = curve2.getPointAtTime(0), - normal1 = curve1.getNormalAtTime(1), - normal2 = curve2.getNormalAtTime(0), - step = normal1.getDirectedAngle(normal2) < 0 ? -radius : radius; - normal1.setLength(step); - normal2.setLength(step); - // use different matrices to transform segment points and stroke vectors - // to support Style#strokeScaling. - if (matrix) - matrix._transformPoint(point, point); - if (strokeMatrix) { - strokeMatrix._transformPoint(normal1, normal1); - strokeMatrix._transformPoint(normal2, normal2); + point = curve2.getPoint1().transform(matrix), + normal1 = curve1.getNormalAtTime(1).multiply(radius) + .transform(strokeMatrix), + normal2 = curve2.getNormalAtTime(0).multiply(radius) + .transform(strokeMatrix); + if (normal1.getDirectedAngle(normal2) < 0) { + normal1 = normal1.negate(); + normal2 = normal2.negate(); } if (isArea) { addPoint(point); @@ -2744,16 +2744,13 @@ statics: { _addSquareCap: function(segment, cap, radius, matrix, strokeMatrix, addPoint, isArea) { // Handles both 'square' and 'butt' caps, as they share a lot of code. - // Calculate the corner points of butt and square caps - var point = segment._point, + // Calculate the corner points of butt and square caps, using different + // matrices to transform segment points and stroke vectors to support + // Style#strokeScaling. + var point = segment._point.transform(matrix), loc = segment.getLocation(), - normal = loc.getNormal().multiply(radius); // normal is normalized - // use different matrices to transform segment points and stroke vectors - // to support Style#strokeScaling. - if (matrix) - matrix._transformPoint(point, point); - if (strokeMatrix) - strokeMatrix._transformPoint(normal, normal); + // NOTE: normal is normalized, so multiply instead of normalize. + normal = loc.getNormal().multiply(radius).transform(strokeMatrix); if (isArea) { addPoint(point.subtract(normal)); addPoint(point.add(normal)); diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index 19712831..28ea08fb 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -47,10 +47,12 @@ PathItem.inject(new function() { * remove empty curves, #resolveCrossings() to resolve self-intersection * make sure all paths have correct winding direction. */ - function preparePath(path, resolve) { + function preparePath(path, closed) { var res = path.clone(false).reduce({ simplify: true }) .transform(null, true, true); - return resolve ? res.resolveCrossings() : res; + if (closed) + res.setClosed(true); + return closed ? res.resolveCrossings() : res; } function createResult(ctor, paths, reduce, path1, path2) { @@ -74,8 +76,10 @@ PathItem.inject(new function() { // Add a simple boolean property to check for a given operation, // e.g. `if (operator.unite)` operator[operation] = true; - // If path1 is open, delegate to computeOpenBoolean() - if (!path1._children && !path1._closed) + // If path1 is open, delegate to computeOpenBoolean(). + // NOTE: Do not access private _closed property here, since path1 may + // be a CompoundPath. + if (!path1.isClosed()) return computeOpenBoolean(path1, path2, operator); // We do not modify the operands themselves, but create copies instead, // fas produced by the calls to preparePath(). @@ -139,11 +143,11 @@ PathItem.inject(new function() { function computeOpenBoolean(path1, path2, operator) { // Only support subtract and intersect operations between an open - // and a closed path. Assume that compound-paths are closed. - // TODO: Should we complain about not supported operations? - if (!path2 || !path2._children && !path2._closed - || !operator.subtract && !operator.intersect) - return null; + // and a closed path. + if (!path2 || !operator.subtract && !operator.intersect) { + throw new Error('Boolean operations on open paths only support ' + + 'subtraction and intersection with another path.'); + } var _path1 = preparePath(path1, false), _path2 = preparePath(path2, false), crossings = _path1.getCrossings(_path2), @@ -612,7 +616,7 @@ PathItem.inject(new function() { // are bringing us back to the beginning, and are both valid, // meaning they are part of the boolean result. if (seg !== exclude && (isStart(seg) || isStart(nextSeg) - || !seg._visited && !nextSeg._visited + || nextSeg && !seg._visited && !nextSeg._visited // Self-intersections (!operator) don't need isValid() calls && (!operator || isValid(seg) && (isValid(nextSeg) // If the next segment isn't valid, its intersection diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 61a1e896..5fbef743 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -99,8 +99,10 @@ var PathItem = Item.extend(/** @lends PathItem# */{ coords = part.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g); var length = coords && coords.length; relative = command === lower; + // Fix issues with z in the middle of SVG path data, not followed by + // a m command, see #413: if (previous === 'z' && !/[mz]/.test(lower)) - this.moveTo(current = start); + this.moveTo(current); switch (lower) { case 'm': case 'l': @@ -170,6 +172,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ // Merge first and last segment with Numerical.EPSILON tolerance // to address imprecisions in relative SVG data. this.closePath(/*#=*/Numerical.EPSILON); + // Correctly handle relative m commands, see #1101: + current = start; break; } previous = lower; @@ -184,7 +188,7 @@ var PathItem = Item.extend(/** @lends PathItem# */{ _contains: function(point) { // NOTE: point is reverse transformed by _matrix, so we don't need to - // apply here. + // apply the matrix here. /*#*/ if (__options.nativeContains || !__options.booleanOperations) { // To compare with native canvas approach: var ctx = CanvasProvider.getContext(1, 1); diff --git a/src/path/Segment.js b/src/path/Segment.js index 70b8b923..d1cbb837 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -130,7 +130,7 @@ var Segment = Base.extend(/** @lends Segment# */{ } else { point = arg0; } - } else if (typeof arg0 === 'object') { + } else if (arg0 == null || typeof arg0 === 'object') { // It doesn't matter if all of these arguments exist. // new SegmentPoint() produces creates points with (0, 0) otherwise. point = arg0; diff --git a/src/style/Style.js b/src/style/Style.js index 00f97daf..61386a52 100644 --- a/src/style/Style.js +++ b/src/style/Style.js @@ -171,7 +171,9 @@ var Style = Base.extend(new function() { var old = this._values[key]; if (old !== value) { if (isColor) { - if (old) + // The old value may be a native string or other color + // description that wasn't coerced to a color object yet + if (old && old._owner !== undefined) old._owner = undefined; if (value && value.constructor === Color) { // Clone color if it already has an owner. diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 15808d73..ecf19f23 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -369,7 +369,7 @@ new function() { definitions = null; } return options.asString - ? new window.XMLSerializer().serializeToString(svg) + ? new self.XMLSerializer().serializeToString(svg) : svg; } diff --git a/src/svg/SvgImport.js b/src/svg/SvgImport.js index 7a8141e5..96f96739 100644 --- a/src/svg/SvgImport.js +++ b/src/svg/SvgImport.js @@ -19,7 +19,8 @@ new function() { // objects, dealing with baseVal, and item lists. // index is option, and if passed, causes a lookup in a list. - var rootSize; + var definitions = {}, + rootSize; function getValue(node, name, isString, allowNull, allowPercent) { // Interpret value as number. Never return NaN, but 0 instead. @@ -547,16 +548,17 @@ new function() { return item; } - var definitions = {}; function getDefinition(value) { // When url() comes from a style property, '#'' seems to be missing on // WebKit. We also get variations of quotes or no quotes, single or // double, so handle it all with one regular expression: var match = value && value.match(/\((?:["'#]*)([^"')]+)/), - res = match && definitions[match[1] - // This is required by Firefox, which can produce absolute urls - // for local gradients, see #1001: - .replace(window.location.href.split('#')[0] + '#', '')]; + name = match && match[1], + res = name && definitions[window + // This is required by Firefox, which can produce absolute + // urls for local gradients, see #1001: + ? name.replace(window.location.href.split('#')[0] + '#', '') + : name]; // Patch in support for SVG's gradientUnits="objectBoundingBox" through // Color#_scaleToBounds if (res && res._scaleToBounds) { @@ -659,7 +661,7 @@ new function() { function onLoad(svg) { try { - var node = typeof svg === 'object' ? svg : new window.DOMParser() + var node = typeof svg === 'object' ? svg : new self.DOMParser() .parseFromString(svg, 'image/svg+xml'); if (!node.nodeName) { node = null; diff --git a/src/util/Numerical.js b/src/util/Numerical.js index a0f92a78..fe6cd0dc 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -60,6 +60,11 @@ var Numerical = new function() { var abs = Math.abs, sqrt = Math.sqrt, pow = Math.pow, + // Fallback to polyfill: + log2 = Math.log2 || function(x) { + return Math.log(x) * Math.LOG2E; + }, + // Constants EPSILON = 1e-12, MACHINE_EPSILON = 1.12e-16; @@ -67,6 +72,44 @@ var Numerical = new function() { return value < min ? min : value > max ? max : value; } + function getDiscriminant(a, b, c) { + // Ported from @hkrish's polysolve.c + function split(v) { + var x = v * 134217729, + y = v - x, + hi = y + x, // Don't optimize y away! + lo = v - hi; + return [hi, lo]; + } + + var D = b * b - a * c, + E = b * b + a * c; + if (abs(D) * 3 < E) { + var ad = split(a), + bd = split(b), + cd = split(c), + p = b * b, + dp = (bd[0] * bd[0] - p + 2 * bd[0] * bd[1]) + bd[1] * bd[1], + q = a * c, + dq = (ad[0] * cd[0] - q + ad[0] * cd[1] + ad[1] * cd[0]) + + ad[1] * cd[1]; + D = (p - q) + (dp - dq); // Don’t omit parentheses! + } + return D; + } + + function getNormalizationFactor() { + // Normalize coefficients à la Jenkins & Traub's RPOLY. + // Normalization is done by scaling coefficients with a power of 2, so + // that all the bits in the mantissa remain unchanged. + // Use the infinity norm (max(sum(abs(a)…)) to determine the appropriate + // scale factor. See @hkrish in #1087#issuecomment-231526156 + var norm = Math.max.apply(Math, arguments); + return norm && (norm < 1e-8 || norm > 1e8) + ? pow(2, -Math.round(log2(norm))) + : 0; + } + return /** @lends Numerical */{ TOLERANCE: 1e-6, /** @@ -202,6 +245,8 @@ var Numerical = new function() { * Kahan W. - "To Solve a Real Cubic Equation" * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf * Blinn J. - "How to solve a Quadratic Equation" + * Harikrishnan G. + * https://gist.github.com/hkrish/9e0de1f121971ee0fbab281f5c986de9 * * @param {Number} a the quadratic term * @param {Number} b the linear term @@ -215,58 +260,52 @@ var Numerical = new function() { * @author Harikrishnan Gopalakrishnan */ solveQuadratic: function(a, b, c, roots, min, max) { - var count = 0, - eMin = min - EPSILON, - eMax = max + EPSILON, - x1, x2 = Infinity, - B = b, - D; - // a, b, c are expected to be the coefficients of the equation: - // Ax² - 2Bx + C == 0, so we take b = -B/2: - b /= -2; - D = b * b - a * c; // Discriminant - // If the discriminant is very small, we can try to pre-condition - // the coefficients, so that we may get better accuracy - if (D !== 0 && abs(D) < MACHINE_EPSILON) { - // If the geometric mean of the coefficients is small enough - var gmC = pow(abs(a * b * c), 1 / 3); - if (gmC < 1e-8) { - // We multiply with a factor to normalize the coefficients. - // The factor is just the nearest exponent of 10, big enough - // to raise all the coefficients to nearly [-1, +1] range. - var mult = gmC === 0 ? 0 : pow(10, - abs(Math.floor(Math.log(gmC) * Math.LOG10E))); - a *= mult; - b *= mult; - c *= mult; - // Recalculate the discriminant - D = b * b - a * c; - } - } + var x1, x2 = Infinity; if (abs(a) < EPSILON) { // This could just be a linear equation - if (abs(B) < EPSILON) + if (abs(b) < EPSILON) return abs(c) < EPSILON ? -1 : 0; - x1 = -c / B; - } else if (D >= -MACHINE_EPSILON) { // No real roots if D < 0 - var Q = D < 0 ? 0 : sqrt(D), - R = b + (b < 0 ? -Q : Q); - // Try to minimize floating point noise. - if (R === 0) { - x1 = c / a; - x2 = -x1; - } else { - x1 = R / a; - x2 = c / R; + x1 = -c / b; + } else { + // a, b, c are expected to be the coefficients of the equation: + // Ax² - 2Bx + C == 0, so we take b = -b/2: + b *= -0.5; + var D = getDiscriminant(a, b, c); + // If the discriminant is very small, we can try to normalize + // the coefficients, so that we may get better accuracy. + if (D && abs(D) < MACHINE_EPSILON) { + var f = getNormalizationFactor(abs(a), abs(b), abs(c)); + if (f) { + a *= f; + b *= f; + c *= f; + D = getDiscriminant(a, b, c); + } + } + if (D >= -MACHINE_EPSILON) { // No real roots if D < 0 + var Q = D < 0 ? 0 : sqrt(D), + R = b + (b < 0 ? -Q : Q); + // Try to minimize floating point noise. + if (R === 0) { + x1 = c / a; + x2 = -x1; + } else { + x1 = R / a; + x2 = c / R; + } } } + var count = 0, + boundless = min == null, + minB = min - EPSILON, + maxB = max + EPSILON; // We need to include EPSILON in the comparisons with min / max, // as some solutions are ever so lightly out of bounds. - if (isFinite(x1) && (min == null || x1 > eMin && x1 < eMax)) - roots[count++] = min == null ? x1 : clamp(x1, min, max); + if (isFinite(x1) && (boundless || x1 > minB && x1 < maxB)) + roots[count++] = boundless ? x1 : clamp(x1, min, max); if (x2 !== x1 - && isFinite(x2) && (min == null || x2 > eMin && x2 < eMax)) - roots[count++] = min == null ? x2 : clamp(x2, min, max); + && isFinite(x2) && (boundless || x2 > minB && x2 < maxB)) + roots[count++] = boundless ? x2 : clamp(x2, min, max); return count; }, @@ -283,6 +322,8 @@ var Numerical = new function() { * References: * Kahan W. - "To Solve a Real Cubic Equation" * http://www.cs.berkeley.edu/~wkahan/Math128/Cubic.pdf + * Harikrishnan G. + * https://gist.github.com/hkrish/9e0de1f121971ee0fbab281f5c986de9 * * W. Kahan's paper contains inferences on accuracy of cubic * zero-finding methods. Also testing methods for robustness. @@ -300,8 +341,25 @@ var Numerical = new function() { * @author Harikrishnan Gopalakrishnan */ solveCubic: function(a, b, c, d, roots, min, max) { - var count = 0, - x, b1, c2; + var f = getNormalizationFactor(abs(a), abs(b), abs(c), abs(d)), + x, b1, c2, qd, q; + if (f) { + a *= f; + b *= f; + c *= f; + d *= f; + } + + function evaluate(x0) { + x = x0; + // Evaluate q, q', b1 and c2 at x + var tmp = a * x; + b1 = tmp + b; + c2 = b1 * x + c; + qd = (tmp + b1) * x + c2; + q = c2 * x + d; + } + // If a or d is zero, we only need to solve a quadratic, so we set // the coefficients appropriately. if (abs(a) < EPSILON) { @@ -314,38 +372,24 @@ var Numerical = new function() { c2 = c; x = 0; } else { - var ec = 1 + MACHINE_EPSILON, // 1.000...002 - x0, q, qd, t, r, s, tmp; // Here onwards we iterate for the leftmost root. Proceed to // deflate the cubic into a quadratic (as a side effect to the // iteration) and solve the quadratic. - x = -(b / a) / 3; - // Evaluate q, q', b1 and c2 at x - tmp = a * x; - b1 = tmp + b; - c2 = b1 * x + c; - qd = (tmp + b1) * x + c2; - q = c2 * x + d; + evaluate(-(b / a) / 3); // Get a good initial approximation. - t = q / a; - r = pow(abs(t), 1/3); - s = t < 0 ? -1 : 1; - t = -qd / a; - // See Kahan's notes on why 1.324718*... works. - r = t > 0 ? 1.3247179572 * Math.max(r, sqrt(t)) : r; - x0 = x - s * r; + var t = q / a, + r = pow(abs(t), 1/3), + s = t < 0 ? -1 : 1, + td = -qd / a, + // See Kahan's notes on why 1.324718*... works. + rd = td > 0 ? 1.324717957244746 * Math.max(r, sqrt(td)) : r, + x0 = x - s * rd; if (x0 !== x) { do { - x = x0; - // Evaluate q, q', b1 and c2 at x - tmp = a * x; - b1 = tmp + b; - c2 = b1 * x + c; - qd = (tmp + b1) * x + c2; - q = c2 * x + d; - // Newton's. Divide by ec to avoid x0 crossing over a - // root. - x0 = qd === 0 ? x : x - q / qd / ec; + evaluate(x0); + // Newton's. Divide by 1 + MACHINE_EPSILON (1.000...002) + // to avoid x0 crossing over a root. + x0 = qd === 0 ? x : x - q / qd / (1 + MACHINE_EPSILON); } while (s * x0 > s * x); // Adjust the coefficients for the quadratic. if (abs(a) * x * x > abs(d / x)) { @@ -355,10 +399,12 @@ var Numerical = new function() { } } // The cubic has been deflated to a quadratic. - var count = Numerical.solveQuadratic(a, b1, c2, roots, min, max); - if (isFinite(x) && (count === 0 || x !== roots[count - 1]) - && (min == null || x > min - EPSILON && x < max + EPSILON)) - roots[count++] = min == null ? x : clamp(x, min, max); + var count = Numerical.solveQuadratic(a, b1, c2, roots, min, max), + boundless = min == null; + if (isFinite(x) && (count === 0 + || count > 0 && x !== roots[0] && x !== roots[1]) + && (boundless || x > min - EPSILON && x < max + EPSILON)) + roots[count++] = boundless ? x : clamp(x, min, max); return count; } }; diff --git a/src/view/View.js b/src/view/View.js index 59c4becd..278e14bd 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -1152,7 +1152,18 @@ new function() { // Injection scope for event handling on the browser fallbacks = { doubleclick: 'click', mousedrag: 'mousemove' - }; + }, + // Various variables required by #_handleMouseEvent() + wasInView = false, + overView, + downPoint, + lastPoint, + downItem, + overItem, + dragItem, + clickItem, + clickTime, + dblClick; // Returns true if event was prevented, false otherwise. function emitMouseEvent(obj, target, type, event, point, prevPoint, @@ -1196,8 +1207,7 @@ new function() { // Injection scope for event handling on the browser } // Returns true if event was stopped, false otherwise. - function emitMouseEvents(view, hitItem, hitTest, type, event, point, - prevPoint) { + function emitMouseEvents(view, hitItem, type, event, point, prevPoint) { // Before handling events, process removeOn() calls for cleanup. // NOTE: As soon as there is one event handler receiving mousedrag // events, non of the removeOnMove() items will be removed while the @@ -1219,9 +1229,7 @@ new function() { // Injection scope for event handling on the browser && emitMouseEvent(hitItem, null, fallbacks[type] || type, event, point, prevPoint, dragItem) // Lastly handle the mouse events on the view, if we're still here. - // Choose from the potential targets in the right sequence, with the - // hitTest() function as the fall-back getter for MouseEvent#target. - || emitMouseEvent(view, dragItem || hitItem || hitTest, type, event, + || emitMouseEvent(view, dragItem || hitItem || view, type, event, point, prevPoint)); } @@ -1251,20 +1259,6 @@ new function() { // Injection scope for event handling on the browser } }; - /** - * Various variables required by #_handleMouseEvent() - */ - var downPoint, - lastPoint, - downItem, - overItem, - dragItem, - clickItem, - clickTime, - dblClick, - overView, - wasInView = false; - return { _viewEvents: viewEvents, @@ -1299,7 +1293,12 @@ new function() { // Injection scope for event handling on the browser // Run the hit-test on items first, but only if we're required to do // so for this given mouse event, see hitItems, #_countItemEvent(): var inView = this.getBounds().contains(point), - hitItem, + hit = hitItems && inView && view._project.hitTest(point, { + tolerance: 0, + fill: true, + stroke: true + }), + hitItem = hit && hit.item || null, // Keep track if view event should be handled, so we can use it // to decide if tool._handleMouseEvent() shall be called after. handle = false, @@ -1308,28 +1307,6 @@ new function() { // Injection scope for event handling on the browser // mouse event types. mouse[type.substr(5)] = true; - // Provide a hit-test function that makes sure to only perform the - // hit-test once, and only when it's actually required. This method - // is passed to emitMouseEvents() and as target to emitMouseEvent(), - // as the fall-back getter for MouseEvent#target. - function hitTest() { - // - if (hitItem === undefined) { - var hit = inView && view._project.hitTest(point, { - tolerance: 0, - fill: true, - stroke: true - }); - hitItem = hit && hit.item || null; - } - // Return the target with view as the fall-back, as expected by - // MouseEvent#target. - return hitItem || view; - } - - // Execute hitTest right away if we have events relying on hitItem. - if (hitItems) - hitTest(); // Handle mouseenter / leave between items and views first. if (hitItems && hitItem !== overItem) { if (overItem) { @@ -1355,9 +1332,8 @@ new function() { // Injection scope for event handling on the browser if ((inView || mouse.drag) && !point.equals(lastPoint)) { // Handle mousemove even if this is not actually a mousemove // event but the mouse has moved since the last event. - emitMouseEvents(this, hitItem, hitTest, - nativeMove ? type : 'mousemove', event, - point, lastPoint); + emitMouseEvents(this, hitItem, nativeMove ? type : 'mousemove', + event, point, lastPoint); handle = true; } wasInView = inView; @@ -1365,8 +1341,7 @@ new function() { // Injection scope for event handling on the browser // We emit mousedown only when in the view, and mouseup regardless, // as long as the mousedown event was inside. if (mouse.down && inView || mouse.up && downPoint) { - emitMouseEvents(this, hitItem, hitTest, type, event, - point, downPoint); + emitMouseEvents(this, hitItem, type, event, point, downPoint); if (mouse.down) { // See if we're clicking again on the same item, within the // double-click time. Firefox uses 300ms as the max time @@ -1383,9 +1358,8 @@ new function() { // Injection scope for event handling on the browser // not the view. if (!prevented && hitItem === downItem) { clickTime = Date.now(); - emitMouseEvents(this, hitItem, hitTest, - dblClick ? 'doubleclick' : 'click', event, - point, downPoint); + emitMouseEvents(this, hitItem, dblClick ? 'doubleclick' + : 'click', event, point, downPoint); dblClick = false; } downItem = dragItem = null; diff --git a/test/tests/Numerical.js b/test/tests/Numerical.js new file mode 100644 index 00000000..01a5cb08 --- /dev/null +++ b/test/tests/Numerical.js @@ -0,0 +1,45 @@ +/* + * 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. + */ + +QUnit.module('Numerical'); + +test('Numerical.solveQuadratic()', function() { + function solve(s) { + var roots = [], + count = Numerical.solveQuadratic(s, 0, -s, roots); + return roots; + } + + var expected = [1, -1]; + + equals(solve(1), expected, + 'Numerical.solveQuadratic().'); + equals(solve(Numerical.EPSILON), expected, + 'Numerical.solveQuadratic() with an identical set of' + + 'coefficients at different scale.'); +}); + +test('Numerical.solveCubic()', function() { + function solve(s) { + var roots = [], + count = Numerical.solveCubic(0.5 * s, -s, -s, -s, roots); + return roots; + } + + var expected = [2.919639565839418]; + + equals(solve(1), expected, + 'Numerical.solveCubic().'); + equals(solve(Numerical.EPSILON), expected, + 'Numerical.solveCubic() with an identical set of' + + 'coefficients at different scale.'); +}); diff --git a/test/tests/PathItem_Contains.js b/test/tests/PathItem_Contains.js index 2cfd2ed5..f556603c 100644 --- a/test/tests/PathItem_Contains.js +++ b/test/tests/PathItem_Contains.js @@ -17,7 +17,7 @@ function testPoint(item, point, inside, message) { + ' should be ' + (inside ? 'inside' : 'outside') + '.')); } -test('Path#contains() (Regular Polygon)', function() { +test('Path#contains() (regular polygon: #208)', function() { var path = new Path.RegularPolygon([0, 0], 6, 20); testPoint(path, new Point(0, -20), true); @@ -39,7 +39,7 @@ test('Path#contains() (Regular Polygon)', function() { testPoint(path, new Point(10, 20), false); }); -test('Path#contains() (Circle Contours)', function() { +test('Path#contains() (circle contours)', function() { var path = new Path.Circle({ center: [100, 100], radius: 50, @@ -56,7 +56,7 @@ test('Path#contains() (Circle Contours)', function() { testPoint(path, path.bounds.bottomRight, false); }); -test('Path#contains() (Transformed Circle Contours)', function() { +test('Path#contains() (transformed circle contours)', function() { var path = new Path.Circle({ center: [200, 200], radius: 50, @@ -74,7 +74,7 @@ test('Path#contains() (Transformed Circle Contours)', function() { testPoint(path, path.bounds.bottomRight, false); }); -test('Path#contains() (Round Rectangle)', function() { +test('Path#contains() (round rectangle: #227)', function() { var rectangle = new Rectangle({ point: new Point(0, 0), size: new Size(200, 40) @@ -83,14 +83,14 @@ test('Path#contains() (Round Rectangle)', function() { testPoint(path, new Point(100, 20), true); }); -test('Path#contains() (Open Circle)', function() { +test('Path#contains() (open circle)', function() { var path = new Path.Circle([100, 100], 100); path.closed = false; path.fillColor = '#ff0000'; testPoint(path, new Point(40, 160), false); }); -test('CompoundPath#contains() (Donut)', function() { +test('CompoundPath#contains() (donut)', function() { var path = new CompoundPath([ new Path.Circle([0, 0], 50), new Path.Circle([0, 0], 25) @@ -163,7 +163,7 @@ test('Shape#contains()', function() { testPoint(shape, new Point(1.1, 0).multiply(half), false); }); -test('Path#contains() (Rectangle Contours)', function() { +test('Path#contains() (rectangle contours)', function() { var path = new Path.Rectangle(new Point(100, 100), [200, 200]), curves = path.getCurves(); @@ -174,7 +174,7 @@ test('Path#contains() (Rectangle Contours)', function() { }); -test('Path#contains() (Rotated Rectangle Contours)', function() { +test('Path#contains() (rotated rectangle contours)', function() { var path = new Path.Rectangle(new Point(100, 100), [200, 200]), curves = path.getCurves(); @@ -200,7 +200,7 @@ test('Path#contains() (touching stationary point with changing orientation)', fu testPoint(path, new Point(200, 200), true); }); -test('Path#contains() (complex shape)', function() { +test('Path#contains() (complex shape: #400)', function() { var path = new Path({ pathData: 'M301 162L307 154L315 149L325 139.5L332.5 135.5L341 128.5L357.5 117.5L364.5 114.5L368.5 110.5L380 105.5L390.5 102L404 96L410.5 96L415 97.5L421 104L425.5 113.5L428.5 126L429.5 134L429.5 141L429.5 148L425.5 161.5L425.5 169L414 184.5L409.5 191L401 201L395 209L386 214.5L378.5 217L368 220L348 219.5L338 218L323.5 212.5L312 205.5L302.5 197.5L295.5 189L291.5 171.5L294 168L298 165.5L301 162z', fillColor: 'blue', @@ -215,7 +215,7 @@ test('Path#contains() (complex shape)', function() { }); -test('Path#contains() (straight curves with zero-winding)', function() { +test('Path#contains() (straight curves with zero-winding: #943)', function() { var pointData = [ [[250, 230], true, true, false, true], [[200, 230], true, true, true, true], @@ -282,7 +282,7 @@ test('Path#contains() (straight curves with zero-winding)', function() { } }); -test('CompoundPath#contains() (nested touching circles)', function() { +test('CompoundPath#contains() (nested touching circles: #944)', function() { var c1 = new Path.Circle({ center: [200, 200], radius: 100 diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js index 739c2095..3beef6c7 100644 --- a/test/tests/Path_Boolean.js +++ b/test/tests/Path_Boolean.js @@ -1013,7 +1013,7 @@ test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function( ]]; var results = [ ['M240,270l4.48298,-20.84932l-14.48298,0.84932l7.67824,-56.20204l82.32176,46.20204z M237.67824,193.79796z', 'M244.48298,249.15068l6.05168,-28.14496l12.77751,27.04076z'], - ['M450,230l-87.53067,34.43944l-0.01593,-0.02371c-0.91969,-0.32388 -30.35274,-0.48069 -30.67835,2.89141l-2.76789,-52.67014z', 'M362.46933,264.43944l-0.01593,-0.02371c0.02221,0.00782 0.02779,0.01574 0.01593,0.02371z'], + ['M450,230l-87.53067,34.43944l-0.01593,-0.02371l0,0c-0.91969,-0.32388 -30.35274,-0.48069 -30.67835,2.89141l-2.76789,-52.67014z', 'M362.46933,264.43944l-0.01593,-0.02371c0.02221,0.00782 0.02779,0.01574 0.01593,0.02371z'], ['M211.76471,391.55301c-1.13066,-11.28456 3.81977,12.25688 -5.47954,24.76763l-28.00902,-21.41225l-26.21252,2.62636l13.04584,-12.69198l-22.93592,-17.53397l30.159,10.50681l46.32376,-45.06725c-9.58101,10.09791 -8.78302,39.92732 -6.89161,58.80464z', 'M178.27615,394.90839l-13.16668,-10.06562l7.22309,-7.02716l39.43215,13.7374z'], ['M138.31417,456.06811c1.62465,-1.18877 -18.69614,16.61617 -34.61033,15.37458l22.34488,-66.7556l-23.16516,-97.04078l209.03396,72.10619c-68.45789,0.5466 -139.96009,51.6986 -173.60335,76.31563z', 'M126.04871,404.68708l21.5947,-64.51445l-9.32924,115.89547z'], ['M300.04122,200l-0.02301,55.81328l19.98178,24.18672l-19.98771,-9.82138l-0.01229,29.82138l-29.04124,-44.09743l-40.1367,-19.722z', 'M300.01822,255.81328l-0.00592,14.36534l-29.05353,-14.27606l-10.39571,-15.78528l29.74019,3.93662z'], @@ -1035,9 +1035,9 @@ test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function( ['M150,290l30,-30l50,10l-24,16l14,14l-50,10l21.17647,-14.11765z', 'M206,286l14,14l-28.82353,-4.11765z'], ['M324.40827,86.1091l0.45123,-0.29442l-16.06828,-25.33943l22.46895,-3.97479l18.72801,12.91832c4.53222,6.94617 -10.24683,11.75354 -25.05014,16.51976l15.98481,25.2078l-22.46194,3.97305l6.36357,-29.14428c-0.13874,0.04467 -0.27748,0.08933 -0.4162,0.134z M324.40827,86.1091l-24.66974,16.09646c-4.37001,-6.69756 10.04945,-11.38915 24.66974,-16.09646z', 'M324.8595,85.81469l0,0l0,0z M324.93803,85.93854c-0.03785,0.01219 -0.07571,0.02438 -0.11356,0.03656l0.03503,-0.16042z'], ['M388.92139,223.32159c120.27613,-2.24369 208.69681,19.62691 208.69681,19.62691l0.0116,0.00003l0,0l21.89025,-21.07283c0,0 8.54573,10.72909 8.51542,21.16115c-0.03104,10.68629 -208.75655,121.23392 -208.75655,121.23392z', 'M597.62979,242.94854l-21.33555,20.53884l-8.25852,-20.6248z'], - ['M272.11155,196.81853l-22.11155,-66.81853l32.64261,64.05832c-6.17669,-1.04412 -8.29,-0.01335 -8.40866,0.04617l-0.00118,-0.00313c-0.09777,0.03858 -0.90866,0.4932 -2.12123,2.71717z', ''], + ['M272.11155,196.81853l-22.11155,-66.81853l32.64261,64.05832c-6.17669,-1.04412 -8.29,-0.01335 -8.40866,0.04617l-0.00118,-0.00313c-0.09777,0.03858 -0.90866,0.4932 -2.12123,2.71717z', 'M274.23395,194.10449l-0.00118,-0.00313c0.01275,-0.00503 0.01337,-0.00299 0.00118,0.00313z'], ['M386.40934,270.73027c77.4169,15.77719 129.39044,24.64733 142.22581,18.90659l5.47489,12.24096l0,0l40.60627,23.71608l-21.58416,18.81428z', 'M528.63515,289.63686l5.47489,12.24096l-11.2043,-6.54387c2.3982,-4.10616 5.41393,-5.55599 5.72941,-5.69709z'], - ['M525,345l32.31797,18.04379l23.72504,-18.37449l6.0793,21.87157l-87.1223,33.45913z', 'M557.31797,363.04379l-23.71188,18.3643l-6.07498,-21.85933z'], + ['M525,345l32.31797,18.04379l23.72504,-18.37449l6.0793,21.87157l-87.1223,33.45913z', 'M557.31797,363.04379l0,0l0,0z M557.31797,363.04379l-23.71188,18.3643l-6.07498,-21.85933z'], ['M250,150l-15.25375,32.05024l29.10275,-7.1942z M225.84314,153.10482l-45.84314,36.89518l63.48255,20.45333z', 'M205.60228,189.25463l0,0.2l0.01028,-0.20254z'], ['M598.5487,408.11025l-42.28034,59.10612l0,0l-37.73535,-87.53938l36.05709,27.56285l0.78705,28.12706z', 'M556.26835,467.21638l-3.83913,-29.98828l2.94792,-1.86119z'], ['M570,290l5.8176,33.58557l-28.17314,-14.439c-14.32289,2.0978 -28.17688,4.12693 -28.17688,4.12693l19.08162,-8.78834l-16.12739,-8.26544l27.9654,2.81326z', 'M538.5492,304.48516l11.83801,-5.45218l9.61279,0.96702l15.8176,23.58557z'], diff --git a/test/tests/Segment.js b/test/tests/Segment.js index 24ce21e9..3a2107af 100644 --- a/test/tests/Segment.js +++ b/test/tests/Segment.js @@ -12,6 +12,11 @@ QUnit.module('Segment'); +test('new Segment()', function() { + var segment = new Segment(null, null, null); + equals(segment.toString(), '{ point: { x: 0, y: 0 } }'); +}); + test('new Segment(point)', function() { var segment = new Segment(new Point(10, 10)); equals(segment.toString(), '{ point: { x: 10, y: 10 } }'); @@ -22,6 +27,11 @@ test('new Segment(x, y)', function() { equals(segment.toString(), '{ point: { x: 10, y: 10 } }'); }); +test('new Segment(undefined)', function() { + var segment = new Segment(undefined); + equals(segment.toString(), '{ point: { x: 0, y: 0 } }'); +}); + test('new Segment(object)', function() { var segment = new Segment({ point: { x: 10, y: 10 }, handleIn: { x: 5, y: 5 }, handleOut: { x: 15, y: 15 } }); equals(segment.toString(), '{ point: { x: 10, y: 10 }, handleIn: { x: 5, y: 5 }, handleOut: { x: 15, y: 15 } }'); @@ -32,6 +42,16 @@ test('new Segment(point, handleIn, handleOut)', function() { equals(segment.toString(), '{ point: { x: 10, y: 10 }, handleIn: { x: 5, y: 5 }, handleOut: { x: 15, y: 15 } }'); }); +test('new Segment(null, null, null)', function() { + var segment = new Segment(null, null, null); + equals(segment.toString(), '{ point: { x: 0, y: 0 } }'); +}); + +test('new Segment(undefined, null, null)', function() { + var segment = new Segment(undefined, null, null); + equals(segment.toString(), '{ point: { x: 0, y: 0 } }'); +}); + test('new Segment(x, y, inX, inY, outX, outY)', function() { var segment = new Segment(10, 10, 5, 5, 15, 15); equals(segment.toString(), '{ point: { x: 10, y: 10 }, handleIn: { x: 5, y: 5 }, handleOut: { x: 15, y: 15 } }'); diff --git a/test/tests/load.js b/test/tests/load.js index 6514919d..c1f9f6aa 100644 --- a/test/tests/load.js +++ b/test/tests/load.js @@ -58,3 +58,5 @@ /*#*/ include('SvgImport.js'); /*#*/ include('SvgExport.js'); + +/*#*/ include('Numerical.js');