├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── RELEASE-NOTES.md ├── dist ├── index-esm.js ├── index-esm.min.js ├── index-esm.min.js.map ├── index-umd.js ├── index-umd.min.js └── index-umd.min.js.map ├── docs ├── ShapeInfo.md ├── ShapesApi.md ├── SvgShapesApi.md ├── images │ ├── example-1.png │ ├── example-2.png │ └── example-3.png └── jsdoc-config.js ├── examples ├── example-1.js ├── example-1.svg ├── example-2.js ├── example-2.svg ├── example-3.js ├── example-3.svg ├── path-line.js ├── path-line.svg ├── rotate-rectangle-line.js ├── rotate-rectangle-line.svg ├── rotated-ellipse-line.js ├── rotated-ellipse-line.svg ├── tessellate-cubic-beziers.js └── tessellate-cubic-beziers.svg ├── index.js ├── lgtm.yml ├── lib ├── AffineShapes.js ├── Intersection.js ├── IntersectionQuery.js ├── PathHandler.js ├── ShapeInfo.js ├── Shapes.js └── SvgShapes.js ├── package-lock.json ├── package.json ├── reference ├── Bezout.m ├── Cubic-Cubic Failure Case.nb ├── Cubic-Cubic Intersection.nb └── Quad-Quad Intersection.nb ├── rollup.config.js ├── scratchpad ├── arc-angle-test.js ├── arc-fail.js ├── arc-test-2.js ├── arc-test.js ├── cubic-cubic-fail.js ├── find_parameters.js ├── shapeInfo-test.js ├── temp.svg └── visual-test-conversion │ ├── convert │ └── transform_visual_test.xsl ├── test ├── Itertools.js ├── affine-shapes-test.js ├── intersection-tests.js ├── shape-info-test.js └── shapes-test.js └── visual-test ├── arc-fail.svg ├── bezier2_bezier2.svg ├── bezier2_bezier3.svg ├── bezier2_circle.svg ├── bezier2_ellipse.svg ├── bezier2_line.svg ├── bezier2_polygon.svg ├── bezier2_rect.svg ├── bezier3_bezier3.svg ├── bezier3_circle.svg ├── bezier3_ellipse.svg ├── bezier3_line.svg ├── bezier3_polygon.svg ├── bezier3_rect.svg ├── circle_circle.svg ├── circle_ellipse.svg ├── circle_line.svg ├── circle_polygon.svg ├── circle_rect.svg ├── ellipse_ellipse.svg ├── ellipse_line.svg ├── ellipse_polygon.svg ├── ellipse_rect.svg ├── index.html ├── line_line.svg ├── line_polygon.svg ├── line_rect.svg ├── line_rounded_rect.svg ├── line_rounded_rect_2.svg ├── path_circle.svg ├── path_ellipse.svg ├── path_line.svg ├── path_path.svg ├── path_polygon.svg ├── path_rect.svg ├── polygon_polygon.svg ├── polygon_rectangle.svg ├── rect_rect.svg ├── rel_path_circle.svg ├── rel_path_ellipse.svg ├── rel_path_line.svg ├── rel_path_path.svg ├── rel_path_polygon.svg ├── rel_path_rect.svg └── show_intersections.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs/jsdoc 4 | examples 5 | scratchpad 6 | test 7 | visual-test 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "extends": ["ash-nazg/sauron-node"], 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "settings": { 16 | "polyfills": [ 17 | "console", 18 | "JSON", 19 | "Array.isArray" 20 | ] 21 | }, 22 | "overrides": [ 23 | { 24 | files: "docs/jsdoc-config.js", 25 | globals: { 26 | "module": "readonly" 27 | }, 28 | rules: { 29 | strict: "off", 30 | "import/unambiguous": "off", 31 | "import/no-commonjs": "off" 32 | } 33 | }, 34 | { 35 | files: ["**/*.md"], 36 | settings: { 37 | polyfills: [ 38 | "console", 39 | "document.querySelector" 40 | ] 41 | }, 42 | rules: { 43 | "eol-last": "off", 44 | "no-console": "off", 45 | "no-undef": "off", 46 | "padded-blocks": "off", 47 | "import/unambiguous": "off", 48 | "import/no-unresolved": "off", 49 | "node/no-missing-import": "off", 50 | "import/no-commonjs": "off", 51 | "no-multi-spaces": "off", 52 | "no-unused-vars": ["error", { 53 | "varsIgnorePattern": "Point2D|ShapeInfo|Intersection|result|centers|radii|arcs|points|beziers|circles|ellipses|lines|paths|polylines|polygons|rectangles" 54 | }], 55 | "node/no-missing-require": ["error", { 56 | "allowModules": ["kld-intersections"] 57 | }] 58 | } 59 | } 60 | ], 61 | "rules": { 62 | "indent": [ 63 | "error", 64 | 4, 65 | {"SwitchCase": 1} 66 | ], 67 | "quotes": [ 68 | "error", 69 | "double" 70 | ], 71 | "space-before-function-paren": [ 72 | "error", 73 | "never" 74 | ], 75 | "brace-style": [ 76 | "error", 77 | "stroustrup" 78 | ], 79 | "arrow-parens": [ 80 | "error", 81 | "as-needed" 82 | ], 83 | "no-multiple-empty-lines": "off", 84 | "max-len": "off", 85 | "node/exports-style": "off", 86 | "unicorn/no-zero-fractions": "off", 87 | "require-unicode-regexp": "off", 88 | "yoda": "off", 89 | "valid-jsdoc": 0 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | 5 | node_modules 6 | npm-debug.log 7 | 8 | docs/jsdoc 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/jsdoc 2 | examples 3 | scratchpad 4 | test 5 | visual-test 6 | rollup.config.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Kevin Lindsey 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kld-intersections 2 | 3 | - [Installation](#installation) 4 | - [Importing](#importing) 5 | - [Usage](#usage) 6 | - [Queries](#queries) 7 | - [Known Issues](#known-issues) 8 | - [Links and Related Projects](#links-and-related-projects) 9 | 10 | --- 11 | 12 | A library of intersection algorithms covering all permutations for any two of the following SVG shapes: 13 | 14 | - Arcs 15 | - Quadratic Bézier 16 | - Cubic Bézier 17 | - Circle 18 | - Ellipse 19 | - Line 20 | - Paths 21 | - Polygon 22 | - Polyline 23 | - Rectangle 24 | 25 | # Installation 26 | 27 | ``` 28 | npm install kld-intersections 29 | ``` 30 | 31 | # Importing 32 | 33 | The following sections indicate how you can import the code for use in various environments. 34 | 35 | ## Node 36 | 37 | ```javascript 38 | const {ShapeInfo, Intersection} = require("kld-intersections"); 39 | ``` 40 | 41 | ## Browsers 42 | 43 | ```html 44 | 45 | 49 | ``` 50 | 51 | ## Modern Browsers (ESM) 52 | 53 | ```javascript 54 | import {ShapeInfo, Intersection} from "./node_modules/kld-intersections/dist/index-esm.js"; 55 | ``` 56 | 57 | ## Bundlers 58 | 59 | ```javascript 60 | import {ShapeInfo, Intersection} from "kld-intersections"; 61 | ``` 62 | 63 | ## Usage 64 | 65 | In order to perform an intersection, you need to create descriptions of each shape to intersect. This is done using ShapeInfo. 66 | 67 | Once you have created your ShapeInfos, pass them into `Intersection.intersect` and you will get back an `Intersection` object. Intersection objects contain the intersections, an array of Point2D instances, in their `points` property. 68 | 69 | The following example creates a path (from SVG path data) and a line. The two shapes are passed into `Intersection.intersect` and the results are displayed in the console. 70 | 71 | ```javascript 72 | const {ShapeInfo, Intersection} = require("kld-intersections"); 73 | 74 | const path = ShapeInfo.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 75 | const line = ShapeInfo.line([15, 75], [355, 140]); 76 | const intersections = Intersection.intersect(path, line); 77 | 78 | intersections.points.forEach(console.log); 79 | ``` 80 | 81 | Each of the shape constructors in ShapeInfo supports a wide variety of formats. Be sure to look at the examples in the ![ShapeInfo](./docs/ShapeInfo.md) docs to get an idea of how you can define shapes. 82 | 83 | Note that there are some older APIs that have been deprecated. If you need to work with those APIs or wish to use the Intersection methods directly, you can read about those in [Shapes API](#docs/ShapesApi.md). 84 | 85 | # Queries 86 | 87 | In the original intersection code written for kevlindev.com, there were some functions that allowed one to determine if a point was contained within a given shape. That code has been extracted into a separate class named `IntersectionQuery`. Those methods are currently limited to the following list: 88 | 89 | * IntersectionQuery.pointInCircle(**point** : Point2D, **center** : Point2D, **radius** : number) 90 | * IntersectionQuery.pointInEllipse(**point** : Point2D, **center** : Point2D, **radiusX** : number, **radiusY** : number) 91 | * IntersectionQuery.pointInPolyline(**point** : Point2D, **points** : Array) 92 | * IntersectionQuery.pointInPolygon(**point** : Point2D, **points** : Array) 93 | * IntersectionQuery.pointInRectangle(**point** : Point2D, **topLeft** : Point2D, **bottomRight** : Point2D) 94 | 95 | The first argument is the point you wish to test. The remaining parameters describe the shape to test against. All methods return a boolean value indicating whether or not the given point is contained within the shape. 96 | 97 | Note that currently bézier curves are not included in this list. As a workaround, bézier shapes can be approximated using polylines and then tested with `pointInPolyline` or `pointInPolygon`. See [tessellate-cubic-beziers.js](examples/tessellate-cubic-beziers.js) as an example of how you might tesselate bézier curves for this purpose. 98 | 99 | # Known Issues 100 | 101 | Please note that the bézier-bézier intersections may not be well-behaved. There is work being done to make these more stable, but no eta is available at this time. As a workaround, bézier shapes can be approximated using polylines and then intersected with an appropriate polyline intersection method. See [tessellate-cubic-beziers.js](examples/tessellate-cubic-beziers.js) as an example of how you might do this. 102 | 103 | # Links and Related Projects 104 | 105 | - [http://www.quazistax.com](http://www.quazistax.com/testIntersection.html) 106 | - [kld-path-parser](https://github.com/thelonious/kld-path-parser) 107 | - [kld-transform-parser](https://github.com/thelonious/kld-transform-parser) 108 | - [kld-affine](https://github.com/thelonious/kld-affine) 109 | - [kld-polynomial](https://github.com/thelonious/kld-polynomial) 110 | - [kld-contours](https://github.com/thelonious/kld-contours) 111 | - [kld-data-transformer](https://github.com/thelonious/kld-data-transformer) 112 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | - [0.6.0](#0.6.0) 2 | - [0.5.2](#0.5.2) 3 | 4 | --- 5 | 6 | # 0.6.0 7 | 8 | - Rename IntersectionArgs to ShapeInfo 9 | - Move Shape and AffineShape functionality into ShapeInfo 10 | - Deprecate IntersectionArgs, Shape, and AffineShape classes 11 | 12 | # 0.5.2 13 | 14 | - Removed console logging from Intersection.js 15 | -------------------------------------------------------------------------------- /docs/ShapeInfo.md: -------------------------------------------------------------------------------- 1 | # Shape Info 2 | 3 | - [Arc](#arc) 4 | - [Arc Examples](#arc-examples) 5 | - [Quadratic Bezier](#quadratic-bezier) 6 | - [Quadratic Bezier Examples](#quadratic-bezier-examples) 7 | - [Cubic Bezier](#cubic-bezier) 8 | - [Cubic Bezier Examples](#cubic-bezier-examples) 9 | - [Circle](#circle) 10 | - [Circle Examples](#circle-examples) 11 | - [Ellipse](#ellipse) 12 | - [Ellipse Examples](#ellipse-examples) 13 | - [Line](#line) 14 | - [Line Examples](#line-examples) 15 | - [Path](#path) 16 | - [Path Examples](#path-examples) 17 | - [Polygon](#polygon) 18 | - [Polygon Examples](#polygon-examples) 19 | - [Polyline](#polyline) 20 | - [Polyline Examples](#polyline-examples) 21 | - [Rectangle](#rectangle) 22 | - [Rectangle Examples](#rectangle-examples) 23 | 24 | --- 25 | 26 | This document describes how to use the ShapeInfo class to describe shapes for intersection. This class is built up of the following shape creation methods: 27 | 28 | - ShapeInfo.arc(**...**) 29 | - ShapeInfo.quadraticBezier(**...**) 30 | - ShapeInfo.cubicBezier(**...**) 31 | - ShapeInfo.circle(**...**) 32 | - ShapeInfo.ellipse(**...**) 33 | - ShapeInfo.line(**...**) 34 | - ShapeInfo.path(**...**) 35 | - ShapeInfo.polygon(**...**) 36 | - ShapeInfo.polyline(**...**) 37 | - ShapeInfo.rectangle(**...**) 38 | 39 | In place of **...**, you can pass in one of the following: 40 | 41 | - An object describing the shape 42 | - An array describing the shape 43 | - A list of arguments, describing the shape. This is the same as the array format; however, the array's elements are spread into the method call. 44 | 45 | Each section below gives a description of the various formats that may be used to describe a given shape type. 46 | 47 | # Arc 48 | 49 | An arc is defined by: 50 | 51 | - A center point 52 | - An x radius and a y radius 53 | - A start angle 54 | - An end angle 55 | 56 | ## A Center Point 57 | 58 | The center may be represented four ways: 59 | 60 | - A `center` property that is an object with `x` and `y` number properties 61 | - A `center` property that is an array with two number elements, `x` being the first element and `y` being the second element 62 | - A `cx` number property and a `cy` number property 63 | - A `centerX` number property and a `centerY` number property 64 | 65 | ### Center Examples 66 | 67 | ```javascript 68 | const centers = [ 69 | {center: new Point2D(10, 20)}, 70 | {center: {x: 10, y: 20}}, 71 | {center: [10, 20]}, 72 | {cx: 10, cy: 20}, 73 | {centerX: 10, centerY: 20} 74 | ]; 75 | ``` 76 | 77 | ## An X Radius and a Y Radius 78 | 79 | The radii may be represented four ways: 80 | 81 | - A `radii` property that is an object with `x` and `y` number properties 82 | - A `radii` property that is an array with two number elements, `rx` being the first element and `ry` being the second element 83 | - An `rx` number property and an `ry` number property 84 | - A `radiusX` number property and a `radiusY` number property 85 | 86 | ### X and Y Radii Examples 87 | 88 | ```javascript 89 | const radii = [ 90 | {radii: {x: 5, y: 10}}, 91 | {radii: [5, 10]}, 92 | {rx: 5, ry: 10}, 93 | {radiusX: 5, radiusY: 10} 94 | ]; 95 | ``` 96 | 97 | ## A start angle 98 | 99 | The starting angle is a `startAngle` number property. This value is measured in radians. 100 | 101 | ## An end angle 102 | 103 | The ending angle is a `endAngle` number property. This value is measured in radians. 104 | 105 | ## Array input 106 | 107 | The above discussed how an object may be used to describe an Arc. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 108 | 109 | There are two supported formats: 110 | 111 | - A 6 element array defined as follows: 112 | - a number element being `centerX` 113 | - a number element being `centerY` 114 | - a number element being `radiusX` 115 | - a number element being `radiusY` 116 | - a number element being `startRadians` 117 | - a number element being `endRadians` 118 | - A 5 element array defined as follows: 119 | - an object with `x` and `y` number properties being the `center` 120 | - a number element being `radiusX` 121 | - a number element being `radiusY` 122 | - a number element being `startRadians` 123 | - a number element being `endRadians` 124 | 125 | ## Arc Examples 126 | 127 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 128 | 129 | ```javascript 130 | const arcs = [ 131 | ShapeInfo.arc({center: {x: 10, y: 20}, radii: {x: 5, y: 10}, startAngle: 0, endAngle: Math.PI}), 132 | ShapeInfo.arc({center: [10, 20], radii: [5, 10], startAngle: 0, endAngle: Math.PI}), 133 | ShapeInfo.arc({cx: 10, cy: 20, rx: 5, ry: 10, startAngle: 0, endAngle: Math.PI}), 134 | ShapeInfo.arc({centerX: 10, centerY: 20, radiusX: 5, radiusY: 10, startAngle: 0, endAngle: Math.PI}), 135 | ShapeInfo.arc([10, 20, 5, 10, 0, Math.PI]), 136 | ShapeInfo.arc([{x: 10, y: 20}, 5, 10, 0, Math.PI]), 137 | ShapeInfo.arc(10, 20, 5, 10, 0, Math.PI), 138 | ShapeInfo.arc({x: 10, y: 20}, 5, 10, 0, Math.PI) 139 | ]; 140 | ``` 141 | 142 | # Quadratic Bezier 143 | 144 | A quadratic bezier is defined by: 145 | 146 | - 3 control points: p1, p2, and p3 147 | 148 | ## Control Points 149 | 150 | A control point may be represented two ways: 151 | 152 | - An object with `x` and `y` number properties 153 | - An array with two number elements, `x` being the first element and `y` being the second element 154 | 155 | ### Control Point Examples 156 | 157 | ```javascript 158 | const points = [ 159 | {p1: new Point2D(10, 20)}, 160 | {p2: {x: 10, y: 20}}, 161 | {p3: [10, 20]} 162 | ]; 163 | ``` 164 | 165 | ## Array input 166 | 167 | The above discussed how an object may be used to describe a Quadratic Bezier. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 168 | 169 | There are two supported formats: 170 | 171 | - A 6 element array defined as follows: 172 | - a number element being `p1.x` 173 | - a number element being `p1.y` 174 | - a number element being `p2.x` 175 | - a number element being `p2.y` 176 | - a number element being `p3.x` 177 | - a number element being `p3.y` 178 | - A 3 element array defined as follows: 179 | - an object with `x` and `y` number properties being `p1` 180 | - an object with `x` and `y` number properties being `p2` 181 | - an object with `x` and `y` number properties being `p3` 182 | 183 | ## Quadratic Bezier Examples 184 | 185 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 186 | 187 | ```javascript 188 | const beziers = [ 189 | ShapeInfo.quadraticBezier({p1: {x: 10, y: 20}, p2: {x: 5, y: 10}, p3: {x: 15, y: 30}}), 190 | ShapeInfo.quadraticBezier({p1: [10, 20], p2: [5, 10], p3: [15, 30]}), 191 | ShapeInfo.quadraticBezier([10, 20, 5, 10, 15, 30]), 192 | ShapeInfo.quadraticBezier([{x: 10, y: 20}, {x: 5, y: 10}, {x: 15, y: 30}]), 193 | ShapeInfo.quadraticBezier(10, 20, 5, 10, 15, 30), 194 | ShapeInfo.quadraticBezier({x: 10, y: 20}, {x: 5, y: 10}, {x: 15, y: 30}) 195 | ]; 196 | ``` 197 | 198 | # Cubic Bezier 199 | 200 | A cubic bezier is defined by: 201 | 202 | - 4 control points: p1, p2, p3, and p4 203 | 204 | ## Control Points 205 | 206 | A control point may be represented two ways: 207 | 208 | - An object with `x` and `y` number properties 209 | - An array with two number elements, `x` being the first element and `y` being the second element 210 | 211 | ### Control Point Examples 212 | 213 | ```javascript 214 | const points = [ 215 | {p1: new Point2D(10, 20)}, 216 | {p2: {x: 10, y: 20}}, 217 | {p3: [10, 20]} 218 | ]; 219 | ``` 220 | 221 | ## Array input 222 | 223 | The above discussed how an object may be used to describe a Quadratic Bezier. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 224 | 225 | There are two supported formats: 226 | 227 | - A 8 element array defined as follows: 228 | - a number element being `p1.x` 229 | - a number element being `p1.y` 230 | - a number element being `p2.x` 231 | - a number element being `p2.y` 232 | - a number element being `p3.x` 233 | - a number element being `p3.y` 234 | - a number element being `p4.x` 235 | - a number element being `p4.y` 236 | - A 4 element array defined as follows: 237 | - an object with `x` and `y` number properties being `p1` 238 | - an object with `x` and `y` number properties being `p2` 239 | - an object with `x` and `y` number properties being `p3` 240 | - an object with `x` and `y` number properties being `p4` 241 | 242 | ## Cubic Bezier Examples 243 | 244 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 245 | 246 | ```javascript 247 | const beziers = [ 248 | ShapeInfo.cubicBezier({p1: {x: 10, y: 20}, p2: {x: 5, y: 10}, p3: {x: 15, y: 30}, p4: {x: 20, y: 15}}), 249 | ShapeInfo.cubicBezier({p1: [10, 20], p2: [5, 10], p3: [15, 30], p4: [20, 15]}), 250 | ShapeInfo.cubicBezier([10, 20, 5, 10, 15, 30, 20, 15]), 251 | ShapeInfo.cubicBezier([{x: 10, y: 20}, {x: 5, y: 10}, {x: 15, y: 30}, {x: 20, y: 15}]), 252 | ShapeInfo.cubicBezier(10, 20, 5, 10, 15, 30, 20, 15), 253 | ShapeInfo.cubicBezier({x: 10, y: 20}, {x: 5, y: 10}, {x: 15, y: 30}, {x: 20, y: 15}) 254 | ]; 255 | ``` 256 | 257 | # Circle 258 | 259 | An circle is defined by: 260 | 261 | - A center point 262 | - A radius 263 | 264 | ## A Center Point 265 | 266 | The center may be represented four ways: 267 | 268 | - A `center` property that is an object with `x` and `y` number properties 269 | - A `center` property that is an array with two number elements, `x` being the first element and `y` being the second element 270 | - A `cx` number property and a `cy` number property 271 | - A `centerX` number property and a `centerY` number property 272 | 273 | ### Center Examples 274 | 275 | ```javascript 276 | const centers = [ 277 | {center: new Point2D(10, 20)}, 278 | {center: {x: 10, y: 20}}, 279 | {center: [10, 20]}, 280 | {cx: 10, cy: 20}, 281 | {centerX: 10, centerY: 20} 282 | ]; 283 | ``` 284 | 285 | ## A Radius 286 | 287 | The radius may be represented two ways: 288 | 289 | - An `r` number property 290 | - A `radius` number 291 | 292 | ### Radius Examples 293 | 294 | ```javascript 295 | const radii = [ 296 | {r: 5}, 297 | {radius: 5} 298 | ]; 299 | ``` 300 | 301 | ## Array input 302 | 303 | The above discussed how an object may be used to describe an Circle. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 304 | 305 | There are two supported formats: 306 | 307 | - A 3 element array defined as follows: 308 | - a number element being `centerX` 309 | - a number element being `centerY` 310 | - a number element being `radius` 311 | - A 2 element array defined as follows: 312 | - an object with `x` and `y` number properties being the `center` 313 | - a number element being `radius` 314 | 315 | ## Circle Examples 316 | 317 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 318 | 319 | ```javascript 320 | const circles = [ 321 | ShapeInfo.circle({center: {x: 10, y: 20}, radius: 15}), 322 | ShapeInfo.circle({center: [10, 20], radius: 15}), 323 | ShapeInfo.circle({cx: 10, cy: 20, r: 15}), 324 | ShapeInfo.circle({centerX: 10, centerY: 20, radius: 15}), 325 | ShapeInfo.circle([10, 20, 15]), 326 | ShapeInfo.circle([{x: 10, y: 20}, 15]), 327 | ShapeInfo.circle(10, 20, 15), 328 | ShapeInfo.circle({x: 10, y: 20}, 15) 329 | ]; 330 | ``` 331 | 332 | # Ellipse 333 | 334 | An ellipse is defined by: 335 | 336 | - A center point 337 | - An x radius and a y radius 338 | 339 | ## A Center Point 340 | 341 | The center may be represented four ways: 342 | 343 | - A `center` property that is an object with `x` and `y` number properties 344 | - A `center` property that is an array with two number elements, `x` being the first element and `y` being the second element 345 | - A `cx` number property and a `cy` number property 346 | - A `centerX` number property and a `centerY` number property 347 | 348 | ### Center Examples 349 | 350 | ```javascript 351 | const centers = [ 352 | {center: new Point2D(10, 20)}, 353 | {center: {x: 10, y: 20}}, 354 | {center: [10, 20]}, 355 | {cx: 10, cy: 20}, 356 | {centerX: 10, centerY: 20} 357 | ]; 358 | ``` 359 | 360 | ## An X Radius and a Y Radius 361 | 362 | The radii may be represented four ways: 363 | 364 | - A `radii` property that is an object with `x` and `y` number properties 365 | - A `radii` property that is an array with two number elements, `rx` being the first element and `ry` being the second element 366 | - An `rx` number property and an `ry` number property 367 | - A `radiusX` number property and a `radiusY` number property 368 | 369 | ### X and Y Radii Examples 370 | 371 | ```javascript 372 | const radii = [ 373 | {radii: {x: 5, y: 10}}, 374 | {radii: [5, 10]}, 375 | {rx: 5, ry: 10}, 376 | {radiusX: 5, radiusY: 10} 377 | ]; 378 | ``` 379 | 380 | ## Array input 381 | 382 | The above discussed how an object may be used to describe an Ellipse. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 383 | 384 | There are two supported formats: 385 | 386 | - A 4 element array defined as follows: 387 | - a number element being `centerX` 388 | - a number element being `centerY` 389 | - a number element being `radiusX` 390 | - a number element being `radiusY` 391 | - A 3 element array defined as follows: 392 | - an object with `x` and `y` number properties being the `center` 393 | - a number element being `radiusX` 394 | - a number element being `radiusY` 395 | 396 | ## Ellipse Examples 397 | 398 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 399 | 400 | ```javascript 401 | const ellipses = [ 402 | ShapeInfo.ellipse({center: {x: 10, y: 20}, radii: {x: 5, y: 10}}), 403 | ShapeInfo.ellipse({center: [10, 20], radii: [5, 10]}), 404 | ShapeInfo.ellipse({cx: 10, cy: 20, rx: 5, ry: 10}), 405 | ShapeInfo.ellipse({centerX: 10, centerY: 20, radiusX: 5, radiusY: 10}), 406 | ShapeInfo.ellipse([10, 20, 3, 5]), 407 | ShapeInfo.ellipse([{x: 10, y: 20}, 3, 5]), 408 | ShapeInfo.ellipse(10, 20, 3, 5), 409 | ShapeInfo.ellipse({x: 10, y: 20}, 3, 5) 410 | ]; 411 | ``` 412 | 413 | # Line 414 | 415 | A line is defined by: 416 | 417 | - 2 points: p1 and p2 418 | 419 | ## Points 420 | 421 | A point may be represented two ways: 422 | 423 | - An object with `x` and `y` number properties 424 | - An array with two number elements, `x` being the first element and `y` being the second element 425 | 426 | ### Center Examples 427 | 428 | ```javascript 429 | const points = [ 430 | {p1: new Point2D(10, 20)}, 431 | {p2: {x: 10, y: 20}}, 432 | {p1: [10, 20]} 433 | ]; 434 | ``` 435 | 436 | ## Array input 437 | 438 | The above discussed how an object may be used to describe a Quadratic Bezier. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 439 | 440 | There are two supported formats: 441 | 442 | - A 4 element array defined as follows: 443 | - a number element being `p1.x` 444 | - a number element being `p1.y` 445 | - a number element being `p2.x` 446 | - a number element being `p2.y` 447 | - A 2 element array defined as follows: 448 | - an object with `x` and `y` number properties being `p1` 449 | - an object with `x` and `y` number properties being `p2` 450 | 451 | ## Line Examples 452 | 453 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 454 | 455 | ```javascript 456 | const lines = [ 457 | ShapeInfo.line({p1: {x: 10, y: 20}, p2: {x: 5, y: 10}}), 458 | ShapeInfo.line({p1: [10, 20], p2: [5, 10]}), 459 | ShapeInfo.line([10, 20, 5, 10]), 460 | ShapeInfo.line([{x: 10, y: 20}, {x: 5, y: 10}]), 461 | ShapeInfo.line(10, 20, 5, 10), 462 | ShapeInfo.line({x: 10, y: 20}, {x: 5, y: 10}) 463 | ]; 464 | ``` 465 | 466 | # Path 467 | 468 | A path is defined by: 469 | 470 | - Path data 471 | 472 | ## Path Data 473 | 474 | Path data is a string following the path syntax as defined by the [SVG Specification](https://www.w3.org/TR/SVG2/paths.html#PathData). 475 | 476 | ## Path Examples 477 | 478 | ```javascript 479 | const paths = [ 480 | ShapeInfo.path("M0,0 L100,100") 481 | ]; 482 | ``` 483 | 484 | # Polygon 485 | 486 | A polygon is defined by 487 | 488 | - An array of zero or points 489 | 490 | ## Points 491 | 492 | A point may be represented two ways: 493 | 494 | - An object with `x` and `y` number properties 495 | - An array with two number elements, `x` being the first element and `y` being the second element 496 | 497 | Each of these representations is concatenated into a single array. 498 | 499 | ## Polygon Examples 500 | 501 | ```javascript 502 | const polygons = [ 503 | ShapeInfo.polygon([10, 20, 30, 40, 50, 60]), 504 | ShapeInfo.polygon([{x: 10, y: 20}, {x: 30, y: 40}, {x: 50, y: 60}]) 505 | ]; 506 | ``` 507 | 508 | # Polyline 509 | 510 | A polyline is defined by 511 | 512 | - An array of zero or points 513 | 514 | ## Points 515 | 516 | A point may be represented two ways: 517 | 518 | - An object with `x` and `y` number properties 519 | - An array with two number elements, `x` being the first element and `y` being the second element 520 | 521 | Each of these representations is concatenated into a single array. 522 | 523 | ## Polyline Examples 524 | 525 | ```javascript 526 | const polylines = [ 527 | ShapeInfo.polyline([10, 20, 30, 40, 50, 60]), 528 | ShapeInfo.polyline([{x: 10, y: 20}, {x: 30, y: 40}, {x: 50, y: 60}]) 529 | ]; 530 | ``` 531 | 532 | # Rectangle 533 | 534 | A rectangle is defined by: 535 | 536 | - A topLeft point 537 | - A bottomRight point 538 | - Optional x and y radii 539 | 540 | ## Points and Vectors 541 | 542 | A point and a vector may be represented two ways: 543 | 544 | - An object with `x` and `y` number properties 545 | - An array with two number elements, `x` being the first element and `y` being the second element 546 | 547 | ## TopLeft Point 548 | 549 | The `topLeft` point may be represented four ways: 550 | 551 | - An object with a `topLeft` property that is a point as an object 552 | - An object with a `topLeft` property that is a point as an array 553 | - An object with a `x` number property and a `y` number property 554 | - An object with a `top` number property and a `left` number property 555 | 556 | ## BottomRight Point 557 | 558 | The `bottomRight` point may be represented six ways: 559 | 560 | - An object with a `bottomRight` property that is a point as an object 561 | - An object with a `bottomRight` property that is a point as an array 562 | - An object with a `w` number property and a `h` number property 563 | - An object with a `width` number property and a `height` number property 564 | - An object with a `size` property that is a vector as an object 565 | - An object with a `size` property that is a vector as an array 566 | 567 | ### Point Examples 568 | 569 | ```javascript 570 | const points = [ 571 | {topLeft: {x: 10, y: 20}, bottomRight: {x: 30, y: 40}}, 572 | {topLeft: [10, 20], bottomRight: [30, 40]}, 573 | {x: 10, y: 20, w: 20, h: 20}, 574 | {top: 20, left: 10, width: 20, height: 20}, 575 | {topLeft: {x: 10, y: 20}, size: {x: 20, y: 20}}, 576 | {topLeft: [10, 20], size: [20, 20]} 577 | ]; 578 | ``` 579 | 580 | ## An X Radius 581 | 582 | The x radius may be represented two ways: 583 | 584 | - An `rx` number property 585 | - A `radiusX` number property 586 | 587 | ## A Y Radius 588 | 589 | - An `ry` number property 590 | - A `radiusY` number property 591 | 592 | ## Array input 593 | 594 | The above discussed how an object may be used to describe a Quadratic Bezier. For backward compatibility with older shapes APIs, it is also possible to pass an array or a list of arguments. 595 | 596 | There are five supported formats: 597 | 598 | - A 4 element array defined as follows: 599 | - a number element being `x` 600 | - a number element being `y` 601 | - a number element being `width` 602 | - a number element being `height` 603 | - A 6 element array defined as follows: 604 | - a number element being `x` 605 | - a number element being `y` 606 | - a number element being `width` 607 | - a number element being `height` 608 | - a number element being `rx` 609 | - a number element being `ry` 610 | - A 2 element array defined as follows: 611 | - an object with `x` and `y` number properties being `topLeft` 612 | - an object with `x` and `y` number properties being `size` (width, height) 613 | - A 3 element array defined as follows: 614 | - an object with `x` and `y` number properties being `topLeft` 615 | - an object with `x` and `y` number properties being `size` (width, height) 616 | - an object with `rx` and `ry` number properties being `radii` 617 | - A 3 element array defined as follows: 618 | - an object with `x` and `y` number properties being `topLeft` 619 | - an object with `x` and `y` number properties being `size` (width, height) 620 | - an object with `radiusX` and `radiusY` number properties being `radii` 621 | 622 | ## Rectangle Examples 623 | 624 | Note that these examples are not exhaustive. You may combine the above descriptions in any way you wish. 625 | 626 | ```javascript 627 | const rectangles = [ 628 | ShapeInfo.rectangle({topLeft: {x: 10, y: 20}, bottomRight: {x: 5, y: 10}}), 629 | ShapeInfo.rectangle({topLeft: {x: 10, y: 20}, bottomRight: {x: 5, y: 10}, radiusX: 10, radiusY: 15}), 630 | ShapeInfo.rectangle({topLeft: [10, 20], bottomRight: [5, 10]}), 631 | ShapeInfo.rectangle({topLeft: [10, 20], bottomRight: [5, 10], radiusX: 10, radiusY: 15}), 632 | ShapeInfo.rectangle({top: 20, left: 10, width: 5, height: 10}), 633 | ShapeInfo.rectangle({x: 10, y: 20, w: 5, h: 10, rx: 10, ry: 15}), 634 | ShapeInfo.rectangle([10, 20, 5, 10, 10, 15]), 635 | ShapeInfo.rectangle(10, 20, 5, 10, 10, 15) 636 | ]; 637 | ``` 638 | -------------------------------------------------------------------------------- /docs/ShapesApi.md: -------------------------------------------------------------------------------- 1 | # Shapes API 2 | 3 | - [Intersection API](#intersection-api) 4 | - [Shapes API](#shapes-api) 5 | - [Affine Shapes API](#affine-shapes-api) 6 | 7 | --- 8 | 9 | kld-intersection allows you to find intersections using two general approaches: 10 | 11 | - By calling the intersection methods directly 12 | - By creating simple descriptors of shapes and then letting the library determine which method to invoke 13 | 14 | # Intersection API 15 | 16 | At the lowest level, you can invoke intersection methods directly in the `Intersection` class. In order to determine which intersection routine to invoke, you will need to determine two bits of information for each curve involved in the intersection: 17 | 18 | 1. The name the library uses to refer to the given curve type 19 | 2. The arguments used to represent the curve parameters to the library 20 | 21 | Below is a table listing each of the supported curve types. The `Name` column is the name you will need to use to refer to the shape of the given type. `Arguments` lists the parameters you will use to describe the shape of the given curve. 22 | 23 | | Shape | Name | Arguments | 24 | | --- | --- | --- | 25 | | Quadratic Bézier | Bezier2 | **c1** : *Point2D*, **c2** : *Point2D*, **c3**: *Point2D* | 26 | | Cubic Bézier | Bezier3 | **c1** : *Point2D*, **c2** : *Point2D*, **c3**: *Point2D*, **c4**: *Point2D* | 27 | | Circle | Circle | **center** : *Point2D*, **radius** : Number | 28 | | Ellipse | Ellipse | **center** : *Point2D*, **radiusX** : Number, **radiusY** : Number | 29 | | Arc | Arc | **center** : *Point2D*, **radiusX** : Number, **radiusY** : Number, **startRadians** : Number, **endRadians** : Number | 30 | | Line | Line | **p1** : *Point2D*, **p2** : *Point2D* | 31 | | Polygon | Polygon | **points** : *Array< Point2D >* | 32 | | Polyline | Polyline | **points** : *Array< Point2D >* | 33 | | Rectangle | Rectangle | **topLeft** : *Point2D*, **bottomRight** : *Point2D* | 34 | | Path | Path | **segments** : *Array< ShapeInfo >* | 35 | 36 | Once you've determined the names and arguments, you can build the intersection method name like so: 37 | 38 | 1. All intersections begin with `intersect` 39 | 2. Append the `Name` of the first curve type 40 | 3. Append the `Name` of the second curve type 41 | 42 | It is important to note that not all combinations of names are available in the API. The current implementation supports 10 types of curves. If we count all combinations of any two curves, we end up needing `10 * 10 = 100` methods to cover all cases. Fortunately, the order in which we specify the curves is not important. 43 | 44 | If we intersect `a rectangle with a circle` or `a circle with a rectangle`, we will get the same results, regardless of their order. Recognizing this allows us to reduce the number of methods to `10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 54`. Due to this reduction, one more restriction applies when determining the intersection method name: 45 | 46 | > Shape names must be appended to the method name in alphabetical order 47 | 48 | In our earlier example of intersecting a `Rectangle` and a `Circle`, we will need to append `Circle` before `Rectangle` giving us `intersectCircleRectangle` instead of `intersectRectangleCircle`. 49 | 50 | Now that you have determined the method name, you need to pass in arguments to describe the shape of each curve. You do that as below: 51 | 52 | 1. Review the `Arguments` list in the table above 53 | 2. Add all arguments for the first curve in the specified order to the method call 54 | 3. Add all arguments for the second curve in the specified order to the method call 55 | 56 | ## Example 57 | 58 | Lets intersect a `circle` and a `rectangle`. From the table above, we see that the circle's name is `Circle`, and the name of the rectangle is `Rectangle`. Following the rules stated above, this means our method name is: 59 | 60 | `intersectCircleRectangle` 61 | 62 | A circle is described with a center point and a radius. Those are our first two arguments. 63 | 64 | A rectangle is described with two points: the top-left corner and the bottom-right corner. Those are our final arguments. 65 | 66 | Putting this all together, we end up with something like the following: 67 | 68 | ```javascript 69 | const {Point2D, Intersection} = require("kld-intersections"); 70 | 71 | const circle = { 72 | center: new Point2D(0, 0), 73 | radius: 50 74 | }; 75 | 76 | const rectangle = { 77 | topLeft: new Point2D(0, 0), 78 | bottomRight: new Point2D(60, 30) 79 | }; 80 | 81 | const result = Intersection.intersectCircleRectangle( 82 | circle.center, circle.radius, 83 | rectangle.topLeft, rectangle.bottomRight 84 | ); 85 | 86 | console.log(result); 87 | ``` 88 | 89 | Note that the `circle` and `rectangle` variables were used to make the code more readable. You could easily remove those objects, passing in their arguments directly. That would make a minimal example like the following: 90 | 91 | ```javascript 92 | const {Point2D, Intersection} = require("kld-intersections"); 93 | 94 | const result = Intersection.intersectCircleRectangle( 95 | new Point2D(0, 0), 50, 96 | new Point2D(0, 0), new Point2D(60, 30) 97 | ); 98 | 99 | console.log(result); 100 | ``` 101 | 102 | ## Result 103 | 104 | ``` 105 | Intersection { 106 | status: 'Intersection', 107 | points: 108 | [ Point2D { x: 50, y: 0 }, 109 | Point2D { x: 40.00000000000001, y: 30.000000000000004 } ] } 110 | ``` 111 | 112 | Note that `x` and `y` in the second point are not exactly 40 and 30 due to floating point imprecision. 113 | 114 | ## Visual Representation 115 | 116 | If we were to plot the results, we would end up with an image like the following. 117 | 118 | ![Example image 1](./images/example-1.png) 119 | 120 | # Shapes API 121 | 122 | The Shapes API allows you to create descriptions of shapes and curves which the intersection library can then use to determine which intersection method to invoke. This API allows you to create the following shapes: 123 | 124 | - quadraticBezier 125 | - cubicBezier 126 | - circle 127 | - ellipse 128 | - arc 129 | - line 130 | - polygon 131 | - polyline 132 | - rectangle 133 | - path 134 | 135 | To create these shapes, invoke the method name from the list above, passing in the arguments as described in the table in [Intersection API](#intersection-api). However, when passing in, for example, a `Point2D`, you will need to pass in the `x` and `y` values separately. This allows your code to utilize the intersection library without having to commit to the kld-affine classes. 136 | 137 | To find the intersections of these shapes, invoke `Intersection.intersect` passing in your two shape descriptors, in any order. 138 | 139 | ## Example 140 | 141 | ```javascript 142 | const {Point2D, Intersection, Shapes} = require("kld-intersections"); 143 | 144 | const circle = Shapes.circle(0, 0, 50); 145 | const rectangle = Shapes.rectangle(0, 0, 60, 30); 146 | const result = Intersection.intersect(circle, rectangle); 147 | 148 | console.log(result); 149 | ``` 150 | 151 | ## Result 152 | 153 | ``` 154 | Intersection { 155 | status: 'Intersection', 156 | points: 157 | [ Point2D { x: 50, y: 0 }, 158 | Point2D { x: 40.00000000000001, y: 30.000000000000004 } ] } 159 | ``` 160 | 161 | ## Visual Representation 162 | 163 | ![Example image 2](./images/example-2.png) 164 | 165 | 166 | ## Working with Path Data 167 | 168 | Building paths from the other atomic shapes can be tedious and error-prone. In order to simplify this process, the Shapes API includes a `pathData` method. Simply pass in a string of path data and you will get a path shape with all of its segments populated for you. 169 | 170 | ```javascript 171 | import {Shapes} from "kld-intersections"; 172 | 173 | // create a path from path data 174 | const path = Shapes.pathData("M0,0 C100,0 100,100 0,100z m20,0 c100,0 100,100 0,100z"); 175 | 176 | // create a line 177 | const line = Shapes.line(0, -10, 110, 110); 178 | 179 | // intersect 180 | const result = Intersection.intersect(path, line); 181 | ``` 182 | 183 | # Affine Shapes API 184 | 185 | The Affine Shapes API is very similar to the Shapes API but it allows you to use a slightly higher level of abstraction by utilizing the kld-affine classes. This API allows you to create the following shapes: 186 | 187 | - quadraticBezier 188 | - cubicBezier 189 | - circle 190 | - ellipse 191 | - arc 192 | - line 193 | - polygon 194 | - polyline 195 | - rectangle 196 | - path 197 | 198 | To create these shapes, invoke the method name from the list above, passing in the arguments as described in the table in [Intersection API](#intersection-api). 199 | 200 | To find the intersections of these shapes, invoke `Intersection.intersect` passing in your two shape descriptors, in any order. 201 | 202 | # Example 203 | 204 | ```javascript 205 | const {Point2D, Vector2D, Intersection, AffineShapes} = require("kld-intersections"); 206 | 207 | const circle = AffineShapes.circle(new Point2D(0, 0), 50); 208 | const rectangle = AffineShapes.rectangle(new Point2D(0, 0), new Vector2D(60, 30)); 209 | const result = Intersection.intersect(circle, rectangle); 210 | 211 | console.log(result); 212 | ``` 213 | 214 | # Result 215 | 216 | ``` 217 | Intersection { 218 | status: 'Intersection', 219 | points: 220 | [ Point2D { x: 50, y: 0 }, 221 | Point2D { x: 40.00000000000001, y: 30.000000000000004 } ] } 222 | ``` 223 | 224 | # Visual Representation 225 | 226 | ![Example image 3](./images/example-3.png) 227 | 228 | # Working with Path Data 229 | 230 | Building paths from the other atomic shapes can be tedious and error-prone. In order to simplify this process, the AffineShapes API includes a `pathData` method. Simply pass in a string of path data and you will get a path shape with all of its segments populated for you. 231 | 232 | ```javascript 233 | import {AffineShapes} from "kld-intersections"; 234 | 235 | // create a path from path data 236 | const path = AffineShapes.pathData("M0,0 C100,0 100,100 0,100z m20,0 c100,0 100,100 0,100z"); 237 | 238 | // create a line 239 | const line = AffineShapes.line(0, -10, 110, 110); 240 | 241 | // intersect 242 | const result = Intersection.intersect(path, line); 243 | ``` 244 | 245 | ## Use Your Own Objects 246 | 247 | If you have a look at the various API classes, you'll find that they are quite simple. These APIs create instances of the `ShapeInfo` class. In turn the `ShapeInfo` class is quite simple too, consisting of two properties: `name` and `args`. `name` is the name of the shape or curve as defined in the table describing the [Intersection API](#intersection-api). Likewise, `args` is an array of the arguments described in the arguments column in that same table. So, you can pass in any object to `Intersection.intersect` as long as it contains those two properties which need to follow the name and argument conventions just described. 248 | -------------------------------------------------------------------------------- /docs/SvgShapesApi.md: -------------------------------------------------------------------------------- 1 | # SVG Shapes API 2 | 3 | The SVG Shapes API allows you to create descriptions of shapes and curves using SVG elements from the DOM. With this API, you can create shape descriptions for the following elements: 4 | 5 | - circle 6 | - ellipse 7 | - line 8 | - path 9 | - polygon 10 | - polyline 11 | - rect 12 | 13 | When converting path elements, the element's path data will be used to generate shape descriptions, one shape description per path segment. All SVG path commands are supported. 14 | 15 | In order to simplify code, `SvgShapes` includes an `element` method. Pass in an SVG element and this method will dispatch to the appropriate conversion method. An exception will be thrown for unsupported SVG element types. 16 | 17 | The example below shows how you might convert and intersect two arbitrary SVG elements. 18 | 19 | ### Example 20 | 21 | ```javascript 22 | const {SvgShapes, Intersection} = require("kld-intersections"); 23 | 24 | const shape1 = SvgShapes.element(document.querySelector("#a")); 25 | const shape2 = SvgShapes.element(document.querySelector("#b")); 26 | const result = Intersection.intersect(shape1, shape2); 27 | 28 | console.log(result); 29 | ``` 30 | 31 | For more complete examples, have a look at the SVG files in the `visual-test` directory. These cover all combinations of shape intersections. 32 | -------------------------------------------------------------------------------- /docs/images/example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thelonious/kld-intersections/3e329985e2f997fdae84396a3a0839b9a1652371/docs/images/example-1.png -------------------------------------------------------------------------------- /docs/images/example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thelonious/kld-intersections/3e329985e2f997fdae84396a3a0839b9a1652371/docs/images/example-2.png -------------------------------------------------------------------------------- /docs/images/example-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thelonious/kld-intersections/3e329985e2f997fdae84396a3a0839b9a1652371/docs/images/example-3.png -------------------------------------------------------------------------------- /docs/jsdoc-config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | plugins: [], 5 | recurseDepth: 10, 6 | source: { 7 | exclude: [ 8 | "node_modules", 9 | "dist", 10 | "scratchpad", 11 | "test" 12 | ], 13 | excludePattern: "rollup*" 14 | }, 15 | sourceType: "module", 16 | tags: { 17 | allowUnknownTags: false 18 | }, 19 | templates: { 20 | cleverLinks: true, 21 | monospaceLinks: false 22 | }, 23 | opts: { 24 | recurse: true, 25 | verbose: true, 26 | destination: "docs/jsdoc" 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /examples/example-1.js: -------------------------------------------------------------------------------- 1 | import {Point2D, Intersection} from "../index.js"; 2 | 3 | const circle = { 4 | center: new Point2D(0, 0), 5 | radius: 50 6 | }; 7 | 8 | const rectangle = { 9 | topLeft: new Point2D(0, 0), 10 | bottomRight: new Point2D(60, 30) 11 | }; 12 | 13 | const result = Intersection.intersectCircleRectangle( 14 | circle.center, circle.radius, 15 | rectangle.topLeft, rectangle.bottomRight 16 | ); 17 | 18 | // build SVG file showing the shapes, the center point, and intersection points 19 | const intersectionSVG = result.points.map(p => { 20 | return ``; 21 | }).join("\n "); 22 | 23 | const svg = ` 24 | 25 | 26 | 27 | ${intersectionSVG} 28 | 29 | `; 30 | 31 | console.log(svg); 32 | -------------------------------------------------------------------------------- /examples/example-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/example-2.js: -------------------------------------------------------------------------------- 1 | import {Intersection, Shapes} from "../index.js"; 2 | 3 | const circle = Shapes.circle(0, 0, 50); 4 | const rectangle = Shapes.rectangle(0, 0, 60, 30); 5 | const result = Intersection.intersect(circle, rectangle); 6 | 7 | // build SVG file showing the shapes, the center point, and intersection points 8 | const intersectionSVG = result.points.map(p => { 9 | return ``; 10 | }).join("\n "); 11 | 12 | const svg = ` 13 | 14 | 15 | 16 | ${intersectionSVG} 17 | 18 | `; 19 | 20 | console.log(svg); 21 | -------------------------------------------------------------------------------- /examples/example-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/example-3.js: -------------------------------------------------------------------------------- 1 | import {Point2D, Vector2D, Intersection, AffineShapes} from "../index.js"; 2 | 3 | const circle = AffineShapes.circle(new Point2D(0, 0), 50); 4 | const rectangle = AffineShapes.rectangle(new Point2D(0, 0), new Vector2D(60, 30)); 5 | const result = Intersection.intersect(circle, rectangle); 6 | 7 | // build SVG file showing the shapes, the center point, and intersection points 8 | const intersectionSVG = result.points.map(p => { 9 | return ``; 10 | }).join("\n "); 11 | 12 | const svg = ` 13 | 14 | 15 | 16 | ${intersectionSVG} 17 | 18 | `; 19 | 20 | console.log(svg); 21 | -------------------------------------------------------------------------------- /examples/example-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/path-line.js: -------------------------------------------------------------------------------- 1 | import {Shapes, Intersection} from "../index.js"; 2 | 3 | // parser path data 4 | const pathData = "M0,0 C100,0 100,100 0,100z m20,0 c100,0 100,100 0,100z"; 5 | // let pathData = 'M0,0 Q100,50 0,100z m10,0 q100,50 0,100z'; 6 | // let pathData = 'M0,0 H100 V100 H0z m5,5 h100 v100 h-100z'; 7 | 8 | // create path 9 | const path = Shapes.pathData(pathData); 10 | 11 | // create line 12 | const line = Shapes.line(0, -10, 110, 110); 13 | 14 | // intersect 15 | const result = Intersection.intersect(path, line); 16 | 17 | // build SVG file showing the shapes, the center point, and intersection points 18 | const intersectionSVG = result.points.map(p => { 19 | return ``; 20 | }).join("\n "); 21 | 22 | const svg = ` 23 | 24 | 25 | 26 | ${intersectionSVG} 27 | 28 | `; 29 | 30 | console.log(svg); 31 | -------------------------------------------------------------------------------- /examples/path-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/rotate-rectangle-line.js: -------------------------------------------------------------------------------- 1 | import {Intersection, Point2D, Matrix2D} from "../index.js"; 2 | 3 | // define line 4 | const line = { 5 | p1: new Point2D(40, 0), 6 | p2: new Point2D(70, 130) 7 | }; 8 | 9 | // define rectangle 10 | const rect = { 11 | topLeft: new Point2D(10, 10), 12 | bottomRight: new Point2D(110, 110) 13 | }; 14 | 15 | // convert rectangle corners to polygon (list of points) 16 | const poly = [ 17 | rect.topLeft, 18 | new Point2D(rect.bottomRight.x, rect.topLeft.y), 19 | rect.bottomRight, 20 | new Point2D(rect.topLeft.x, rect.bottomRight.y) 21 | ]; 22 | 23 | // find center point of rectangle 24 | const center = rect.topLeft.lerp(rect.bottomRight, 0.5); 25 | 26 | // define rotation in radians 27 | const angle = 45.0 * Math.PI / 180.0; 28 | 29 | // create matrix for rotating around center of rectangle 30 | const rotation = Matrix2D.rotationAt(angle, center); 31 | 32 | // create new rotated polygon 33 | const rotatedPoly = poly.map(p => p.transform(rotation)); 34 | 35 | // find intersections 36 | const result = Intersection.intersectLinePolygon(line.p1, line.p2, rotatedPoly); 37 | 38 | // build SVG file showing the shapes, the center point, and intersection points 39 | const intersectionSVG = result.points.map(p => { 40 | return ``; 41 | }).join("\n "); 42 | 43 | const svg = ` 44 | 45 | 46 | 47 | 48 | ${intersectionSVG} 49 | 50 | `; 51 | 52 | console.log(svg); 53 | -------------------------------------------------------------------------------- /examples/rotate-rectangle-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/rotated-ellipse-line.js: -------------------------------------------------------------------------------- 1 | import {Intersection, Point2D, Matrix2D} from "../index.js"; 2 | 3 | // define line 4 | const line = { 5 | p1: new Point2D(40, 5), 6 | p2: new Point2D(70, 150) 7 | }; 8 | 9 | // define rotated ellipse 10 | const ellipse = { 11 | center: new Point2D(110, 60), 12 | radiusX: 100, 13 | radiusY: 50, 14 | angle: 10 15 | }; 16 | 17 | // rotate the line into the ellipse's axis-aligned coordinate system 18 | const radians = ellipse.angle * Math.PI / 180.0; 19 | const rotation = Matrix2D.rotation(-radians); 20 | const rotatedLine = { 21 | p1: line.p1.transform(rotation), 22 | p2: line.p2.transform(rotation) 23 | }; 24 | 25 | // find intersections 26 | const result = Intersection.intersectEllipseLine( 27 | ellipse.center, ellipse.radiusX, ellipse.radiusY, 28 | rotatedLine.p1, rotatedLine.p2 29 | ); 30 | 31 | // build SVG file showing the shapes, the center point, and intersection points 32 | const intersectionSVG = result.points.map(p => { 33 | return ``; 34 | }).join("\n "); 35 | 36 | // 37 | // NOTE: This SVG transforms the intersection points back into the original 38 | // coordinate system. If you need to do that in your code, use something like 39 | // the following: 40 | // 41 | // const unrotation = Matrix2D.rotation(radians); 42 | // const unrotated_points = result.points(p => p.transform(unrotation)); 43 | // 44 | 45 | const svg = ` 46 | 47 | 48 | 49 | 50 | 51 | 52 | ${intersectionSVG} 53 | 54 | 55 | 56 | `; 57 | 58 | console.log(svg); 59 | -------------------------------------------------------------------------------- /examples/rotated-ellipse-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/tessellate-cubic-beziers.js: -------------------------------------------------------------------------------- 1 | import {CubicBezier2D} from "kld-contours"; 2 | import {Intersection, Point2D} from "../index.js"; 3 | 4 | // create beziers 5 | const b1 = new CubicBezier2D( 6 | new Point2D(203, 140), 7 | new Point2D(206, 359), 8 | new Point2D(245, 6), 9 | new Point2D(248, 212) 10 | ); 11 | const b2 = new CubicBezier2D( 12 | new Point2D(177, 204), 13 | new Point2D(441, 204), 14 | new Point2D(8, 149), 15 | new Point2D(265, 154) 16 | ); 17 | 18 | // create polylines 19 | const p1 = b1.toPolygon2D(); 20 | const p2 = b2.toPolygon2D(); 21 | 22 | // find intersections 23 | const result = Intersection.intersectPolylinePolyline(p1.points, p2.points); 24 | 25 | // build SVG file showing beziers, polylines, and intersection points 26 | const intersectionSVG = result.points.map(p => { 27 | return ``; 28 | }).join("\n "); 29 | 30 | const svg = ` 31 | 32 | 33 | 34 | 35 | 36 | ${intersectionSVG} 37 | 38 | `; 39 | 40 | console.log(svg); 41 | -------------------------------------------------------------------------------- /examples/tessellate-cubic-beziers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module kld-intersections 3 | */ 4 | 5 | export {default as Intersection} from "./lib/Intersection.js"; 6 | 7 | /** 8 | * @deprecated Use ShapeInfo instead 9 | */ 10 | export {default as IntersectionArgs} from "./lib/ShapeInfo.js"; 11 | 12 | /** 13 | * @namespace ShapeInfo 14 | * @implements {module:Shapes~ShapeInfo} 15 | */ 16 | export {default as ShapeInfo} from "./lib/ShapeInfo.js"; 17 | 18 | /** 19 | * @namespace Shapes 20 | * @implements {module:Shapes~Shapes} 21 | */ 22 | export {default as Shapes} from "./lib/Shapes.js"; 23 | 24 | /** 25 | * @namespace AffineShapes 26 | * @implements {module:AffineShapes~AffineShapes} 27 | */ 28 | export {default as AffineShapes} from "./lib/AffineShapes.js"; 29 | 30 | /** 31 | * @namespace SvgShapes 32 | * @implements {module:SvgShapes~SvgShapes} 33 | */ 34 | export {default as SvgShapes} from "./lib/SvgShapes.js"; 35 | 36 | /** 37 | * @namespace IntersectionQuery 38 | * @implements {module:IntersectionQuery~IntersectionQuery} 39 | */ 40 | export {default as IntersectionQuery} from "./lib/IntersectionQuery.js"; 41 | 42 | // Expose affine module classes 43 | 44 | /** 45 | * @external Point2D 46 | */ 47 | 48 | /** 49 | * @external Vector2D 50 | */ 51 | 52 | /** 53 | * @external Matrix2D 54 | */ 55 | 56 | /** 57 | * @class Point2D 58 | * @memberof module:kld-intersections 59 | * @implements {external:Point2D} 60 | */ 61 | export {Point2D} from "kld-affine"; 62 | 63 | /** 64 | * @class Vector2D 65 | * @memberof module:kld-intersections 66 | * @implements {external:Vector2D} 67 | */ 68 | export {Vector2D} from "kld-affine"; 69 | 70 | /** 71 | * @class Matrix2D 72 | * @memberof module:kld-intersections 73 | * @implements {external:Matrix2D} 74 | */ 75 | export {Matrix2D} from "kld-affine"; 76 | 77 | 78 | /** 79 | * @external Polynomial 80 | */ 81 | -------------------------------------------------------------------------------- /lgtm.yml: -------------------------------------------------------------------------------- 1 | extraction: 2 | javascript: 3 | index: 4 | filters: 5 | - exclude: "dist" 6 | -------------------------------------------------------------------------------- /lib/AffineShapes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | /** 3 | * AffineShapes 4 | * 5 | * @copyright 2017-2019, Kevin Lindsey 6 | * @module AffineShapes 7 | * @deprecated use ShapeInfo 8 | */ 9 | 10 | import ShapeInfo from "./ShapeInfo.js"; 11 | 12 | /** 13 | * Build shapes for intersection 14 | */ 15 | class AffineShapes { 16 | /** 17 | * arc 18 | * 19 | * @deprecated use ShapeInfo.arc 20 | * @param {module:kld-intersections.Point2D} center 21 | * @param {number} radiusX 22 | * @param {number} radiusY 23 | * @param {number} startRadians 24 | * @param {number} endRadians 25 | * @returns {module:kld-intersections.ShapeInfo} 26 | */ 27 | static arc(center, radiusX, radiusY, startRadians, endRadians) { 28 | return ShapeInfo.arc(...arguments); 29 | } 30 | 31 | /** 32 | * quadraticBezier 33 | * 34 | * @deprecated use ShapeInfo.quadraticBezier 35 | * @param {module:kld-intersections.Point2D} p1 36 | * @param {module:kld-intersections.Point2D} p2 37 | * @param {module:kld-intersections.Point2D} p3 38 | * @returns {module:kld-intersections.ShapeInfo} 39 | */ 40 | static quadraticBezier(p1, p2, p3) { 41 | return ShapeInfo.quadraticBezier(...arguments); 42 | } 43 | 44 | /** 45 | * cubicBezier 46 | * 47 | * @deprecated use ShapeInfo.cubicBezier 48 | * @param {module:kld-intersections.Point2D} p1 49 | * @param {module:kld-intersections.Point2D} p2 50 | * @param {module:kld-intersections.Point2D} p3 51 | * @param {module:kld-intersections.Point2D} p4 52 | * @returns {module:kld-intersections.ShapeInfo} 53 | */ 54 | static cubicBezier(p1, p2, p3, p4) { 55 | return ShapeInfo.cubicBezier(...arguments); 56 | } 57 | 58 | /** 59 | * circle 60 | * 61 | * @deprecated use ShapeInfo.circle 62 | * @param {module:kld-intersections.Point2D} center 63 | * @param {number} radius 64 | * @returns {module:kld-intersections.ShapeInfo} 65 | */ 66 | static circle(center, radius) { 67 | return ShapeInfo.circle(...arguments); 68 | } 69 | 70 | /** 71 | * ellipse 72 | * 73 | * @deprecated use ShapeInfo.ellipse 74 | * @param {module:kld-intersections.Point2D} center 75 | * @param {number} radiusX 76 | * @param {number} radiusY 77 | * @returns {module:kld-intersections.ShapeInfo} 78 | */ 79 | static ellipse(center, radiusX, radiusY) { 80 | return ShapeInfo.ellipse(...arguments); 81 | } 82 | 83 | /** 84 | * line 85 | * 86 | * @deprecated use ShapeInfo.line 87 | * @param {module:kld-intersections.Point2D} p1 88 | * @param {module:kld-intersections.Point2D} p2 89 | * @returns {module:kld-intersections.ShapeInfo} 90 | */ 91 | static line(p1, p2) { 92 | return ShapeInfo.line(...arguments); 93 | } 94 | 95 | /** 96 | * path 97 | * 98 | * @deprecated use ShapeInfo.path 99 | * @param {string} pathData 100 | * @returns {module:kld-intersections.ShapeInfo} 101 | */ 102 | static path(pathData) { 103 | return ShapeInfo.path(...arguments); 104 | } 105 | 106 | /** 107 | * polygon 108 | * 109 | * @deprecated use ShapeInfo.polygon 110 | * @param {Array} points 111 | * @returns {module:kld-intersections.ShapeInfo} 112 | */ 113 | static polygon(points) { 114 | return ShapeInfo.polygon(...arguments); 115 | } 116 | 117 | /** 118 | * polyline 119 | * 120 | * @deprecated use ShapeInfo.polyline 121 | * @param {Array} points 122 | * @returns {module:kld-intersections.ShapeInfo} 123 | */ 124 | static polyline(points) { 125 | return ShapeInfo.polyline(...arguments); 126 | } 127 | 128 | /** 129 | * rectangle 130 | * 131 | * @deprecated use ShapeInfo.rectangle 132 | * @param {module:kld-intersections.Point2D} topLeft 133 | * @param {module:kld-intersections.Vector2D} size 134 | * @param {number} [rx] 135 | * @param {number} [ry] 136 | * @returns {module:kld-intersections.ShapeInfo} 137 | */ 138 | static rectangle(topLeft, size, rx = 0, ry = 0) { 139 | return ShapeInfo.rectangle(...arguments); 140 | } 141 | } 142 | 143 | export default AffineShapes; 144 | -------------------------------------------------------------------------------- /lib/IntersectionQuery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * IntersectionQuery.js 4 | * 5 | * @copyright 2017 Kevin Lindsey 6 | * @module IntersectionQuery 7 | */ 8 | 9 | import {Vector2D} from "kld-affine"; 10 | 11 | /** 12 | * @namespace 13 | */ 14 | const IntersectionQuery = {}; 15 | 16 | 17 | /** 18 | * pointInCircle 19 | * 20 | * @param {module:kld-intersections.Point2D} point 21 | * @param {module:kld-intersections.Point2D} center 22 | * @param {number} radius 23 | * @returns {boolean} 24 | */ 25 | IntersectionQuery.pointInCircle = function(point, center, radius) { 26 | const v = Vector2D.fromPoints(center, point); 27 | 28 | return v.length() <= radius; 29 | }; 30 | 31 | 32 | /** 33 | * pointInEllipse 34 | * 35 | * @param {module:kld-intersections.Point2D} point 36 | * @param {module:kld-intersections.Point2D} center 37 | * @param {number} radiusX 38 | * @param {number} radiusY 39 | * @returns {boolean} 40 | */ 41 | IntersectionQuery.pointInEllipse = function(point, center, radiusX, radiusY) { 42 | const len = point.subtract(center); 43 | 44 | return (len.x * len.x) / (radiusX * radiusX) + (len.y * len.y) / (radiusY * radiusY) <= 1; 45 | }; 46 | 47 | 48 | /** 49 | * pointInPolyline 50 | * 51 | * @param {module:kld-intersections.Point2D} point 52 | * @param {Array} points 53 | */ 54 | IntersectionQuery.pointInPolyline = function(point, points) { 55 | const {length: len} = points; 56 | let counter = 0; 57 | let xInter; 58 | 59 | let p1 = points[0]; 60 | 61 | for (let i = 1; i <= len; i++) { 62 | const p2 = points[i % len]; 63 | const minY = Math.min(p1.y, p2.y); 64 | const maxY = Math.max(p1.y, p2.y); 65 | const maxX = Math.max(p1.x, p2.x); 66 | 67 | if (p1.y !== p2.y && minY < point.y && point.y <= maxY && point.x <= maxX) { 68 | xInter = (point.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; 69 | 70 | if (p1.x === p2.x || point.x <= xInter) { 71 | counter++; 72 | } 73 | } 74 | 75 | p1 = p2; 76 | } 77 | 78 | return (counter % 2 === 1); 79 | }; 80 | 81 | 82 | /** 83 | * pointInPolyline 84 | * 85 | * @param {module:kld-intersections.Point2D} point 86 | * @param {Array} points 87 | */ 88 | IntersectionQuery.pointInPolygon = IntersectionQuery.pointInPolyline; 89 | 90 | 91 | /** 92 | * pointInRectangle 93 | * 94 | * @param {module:kld-intersections.Point2D} point 95 | * @param {module:kld-intersections.Point2D} topLeft 96 | * @param {module:kld-intersections.Point2D} bottomRight 97 | * @returns {boolean} 98 | */ 99 | IntersectionQuery.pointInRectangle = function(point, topLeft, bottomRight) { 100 | return ( 101 | topLeft.x <= point.x && point.x < bottomRight.x && 102 | topLeft.y <= point.y && point.y < bottomRight.y 103 | ); 104 | }; 105 | 106 | 107 | export default IntersectionQuery; 108 | -------------------------------------------------------------------------------- /lib/PathHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PathHandler.js 3 | * 4 | * @copyright 2017 Kevin Lindsey 5 | */ 6 | 7 | import {Point2D, Vector2D} from "kld-affine"; 8 | 9 | const TWO_PI = 2.0 * Math.PI; 10 | 11 | /** 12 | * Based on the SVG 1.1 specification, Appendix F: Implementation Requirements, 13 | * Section F.6 "Elliptical arc implementation notes" 14 | * {@see https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes} 15 | * 16 | * @param {module:kld-affine.Point2D} startPoint 17 | * @param {module:kld-affine.Point2D} endPoint 18 | * @param {number} rx 19 | * @param {number} ry 20 | * @param {number} angle 21 | * @param {boolean} arcFlag 22 | * @param {boolean} sweepFlag 23 | * @returns {Array} 24 | */ 25 | function getArcParameters(startPoint, endPoint, rx, ry, angle, arcFlag, sweepFlag) { 26 | angle = angle * Math.PI / 180; 27 | 28 | const c = Math.cos(angle); 29 | const s = Math.sin(angle); 30 | const TOLERANCE = 1e-6; 31 | 32 | // Section (F.6.5.1) 33 | const halfDiff = startPoint.subtract(endPoint).multiply(0.5); 34 | const x1p = halfDiff.x * c + halfDiff.y * s; 35 | const y1p = halfDiff.x * -s + halfDiff.y * c; 36 | 37 | // Section (F.6.6.1) 38 | rx = Math.abs(rx); 39 | ry = Math.abs(ry); 40 | 41 | // Section (F.6.6.2) 42 | const x1px1p = x1p * x1p; 43 | const y1py1p = y1p * y1p; 44 | const lambda = (x1px1p / (rx * rx)) + (y1py1p / (ry * ry)); 45 | 46 | // Section (F.6.6.3) 47 | if (lambda > 1) { 48 | const factor = Math.sqrt(lambda); 49 | 50 | rx *= factor; 51 | ry *= factor; 52 | } 53 | 54 | // Section (F.6.5.2) 55 | const rxrx = rx * rx; 56 | const ryry = ry * ry; 57 | const rxy1 = rxrx * y1py1p; 58 | const ryx1 = ryry * x1px1p; 59 | 60 | let factor = (rxrx * ryry - rxy1 - ryx1) / (rxy1 + ryx1); 61 | 62 | if (Math.abs(factor) < TOLERANCE) { 63 | factor = 0; 64 | } 65 | 66 | let sq = Math.sqrt(factor); 67 | 68 | if (arcFlag === sweepFlag) { 69 | sq = -sq; 70 | } 71 | 72 | // Section (F.6.5.3) 73 | const mid = startPoint.add(endPoint).multiply(0.5); 74 | const cxp = sq * rx * y1p / ry; 75 | const cyp = sq * -ry * x1p / rx; 76 | 77 | // Section (F.6.5.5 - F.6.5.6) 78 | const xcr1 = (x1p - cxp) / rx; 79 | const xcr2 = (x1p + cxp) / rx; 80 | const ycr1 = (y1p - cyp) / ry; 81 | const ycr2 = (y1p + cyp) / ry; 82 | 83 | const theta1 = new Vector2D(1, 0).angleBetween(new Vector2D(xcr1, ycr1)); 84 | // let deltaTheta = normalizeAngle(new Vector2D(xcr1, ycr1).angleBetween(new Vector2D(-xcr2, -ycr2))); 85 | let deltaTheta = new Vector2D(xcr1, ycr1).angleBetween(new Vector2D(-xcr2, -ycr2)); 86 | 87 | if (sweepFlag === false) { 88 | deltaTheta -= TWO_PI; 89 | } 90 | 91 | return [ 92 | cxp * c - cyp * s + mid.x, 93 | cxp * s + cyp * c + mid.y, 94 | rx, 95 | ry, 96 | theta1, 97 | theta1 + deltaTheta 98 | ]; 99 | } 100 | 101 | /** 102 | * PathHandler 103 | */ 104 | class PathHandler { 105 | /** 106 | * PathHandler 107 | * 108 | * @param {ShapeInfo} shapeCreator 109 | */ 110 | constructor(shapeCreator) { 111 | this.shapeCreator = shapeCreator; 112 | this.shapes = []; 113 | this.firstX = null; 114 | this.firstY = null; 115 | this.lastX = null; 116 | this.lastY = null; 117 | this.lastCommand = null; 118 | } 119 | 120 | /** 121 | * beginParse 122 | */ 123 | beginParse() { 124 | // zero out the sub-path array 125 | this.shapes = []; 126 | 127 | // clear firstX, firstY, lastX, and lastY 128 | this.firstX = null; 129 | this.firstY = null; 130 | this.lastX = null; 131 | this.lastY = null; 132 | 133 | // need to remember last command type to determine how to handle the 134 | // relative Bezier commands 135 | this.lastCommand = null; 136 | } 137 | 138 | /** 139 | * addShape 140 | * 141 | * @param {ShapeInfo} shape 142 | */ 143 | addShape(shape) { 144 | this.shapes.push(shape); 145 | } 146 | 147 | /** 148 | * arcAbs - A 149 | * 150 | * @param {number} rx 151 | * @param {number} ry 152 | * @param {number} xAxisRotation 153 | * @param {boolean} arcFlag 154 | * @param {boolean} sweepFlag 155 | * @param {number} x 156 | * @param {number} y 157 | */ 158 | arcAbs(rx, ry, xAxisRotation, arcFlag, sweepFlag, x, y) { 159 | if (rx === 0 || ry === 0) { 160 | this.addShape(this.shapeCreator.line( 161 | this.lastX, this.lastY, 162 | x, y 163 | )); 164 | } 165 | else { 166 | const arcParameters = getArcParameters( 167 | new Point2D(this.lastX, this.lastY), 168 | new Point2D(x, y), 169 | rx, ry, 170 | xAxisRotation, 171 | arcFlag, sweepFlag 172 | ); 173 | 174 | this.addShape(this.shapeCreator.arc(...arcParameters)); 175 | } 176 | 177 | this.lastCommand = "A"; 178 | this.lastX = x; 179 | this.lastY = y; 180 | } 181 | 182 | /** 183 | * arcRel - a 184 | * 185 | * @param {number} rx 186 | * @param {number} ry 187 | * @param {number} xAxisRotation 188 | * @param {boolean} arcFlag 189 | * @param {boolean} sweepFlag 190 | * @param {number} x 191 | * @param {number} y 192 | */ 193 | arcRel(rx, ry, xAxisRotation, arcFlag, sweepFlag, x, y) { 194 | if (rx === 0 || ry === 0) { 195 | this.addShape(this.shapeCreator.line( 196 | this.lastX, this.lastY, 197 | this.lastX + x, this.lastY + y 198 | )); 199 | } 200 | else { 201 | const arcParameters = getArcParameters( 202 | new Point2D(this.lastX, this.lastY), 203 | new Point2D(this.lastX + x, this.lastY + y), 204 | rx, ry, 205 | xAxisRotation, 206 | arcFlag, sweepFlag 207 | ); 208 | 209 | this.addShape(this.shapeCreator.arc(...arcParameters)); 210 | } 211 | 212 | this.lastCommand = "a"; 213 | this.lastX += x; 214 | this.lastY += y; 215 | } 216 | 217 | /** 218 | * curvetoCubicAbs - C 219 | * 220 | * @param {number} x1 221 | * @param {number} y1 222 | * @param {number} x2 223 | * @param {number} y2 224 | * @param {number} x 225 | * @param {number} y 226 | */ 227 | curvetoCubicAbs(x1, y1, x2, y2, x, y) { 228 | this.addShape(this.shapeCreator.cubicBezier( 229 | this.lastX, this.lastY, 230 | x1, y1, 231 | x2, y2, 232 | x, y 233 | )); 234 | 235 | this.lastX = x; 236 | this.lastY = y; 237 | this.lastCommand = "C"; 238 | } 239 | 240 | /** 241 | * curvetoCubicRel - c 242 | * 243 | * @param {number} x1 244 | * @param {number} y1 245 | * @param {number} x2 246 | * @param {number} y2 247 | * @param {number} x 248 | * @param {number} y 249 | */ 250 | curvetoCubicRel(x1, y1, x2, y2, x, y) { 251 | this.addShape(this.shapeCreator.cubicBezier( 252 | this.lastX, this.lastY, 253 | this.lastX + x1, this.lastY + y1, 254 | this.lastX + x2, this.lastY + y2, 255 | this.lastX + x, this.lastY + y 256 | )); 257 | 258 | this.lastX += x; 259 | this.lastY += y; 260 | this.lastCommand = "c"; 261 | } 262 | 263 | /** 264 | * linetoHorizontalAbs - H 265 | * 266 | * @param {number} x 267 | */ 268 | linetoHorizontalAbs(x) { 269 | this.addShape(this.shapeCreator.line( 270 | this.lastX, this.lastY, 271 | x, this.lastY 272 | )); 273 | 274 | this.lastX = x; 275 | this.lastCommand = "H"; 276 | } 277 | 278 | /** 279 | * linetoHorizontalRel - h 280 | * 281 | * @param {number} x 282 | */ 283 | linetoHorizontalRel(x) { 284 | this.addShape(this.shapeCreator.line( 285 | this.lastX, this.lastY, 286 | this.lastX + x, this.lastY 287 | )); 288 | 289 | this.lastX += x; 290 | this.lastCommand = "h"; 291 | } 292 | 293 | /** 294 | * linetoAbs - L 295 | * 296 | * @param {number} x 297 | * @param {number} y 298 | */ 299 | linetoAbs(x, y) { 300 | this.addShape(this.shapeCreator.line( 301 | this.lastX, this.lastY, 302 | x, y 303 | )); 304 | 305 | this.lastX = x; 306 | this.lastY = y; 307 | this.lastCommand = "L"; 308 | } 309 | 310 | /** 311 | * linetoRel - l 312 | * 313 | * @param {number} x 314 | * @param {number} y 315 | */ 316 | linetoRel(x, y) { 317 | this.addShape(this.shapeCreator.line( 318 | this.lastX, this.lastY, 319 | this.lastX + x, this.lastY + y 320 | )); 321 | 322 | this.lastX += x; 323 | this.lastY += y; 324 | this.lastCommand = "l"; 325 | } 326 | 327 | /** 328 | * movetoAbs - M 329 | * 330 | * @param {number} x 331 | * @param {number} y 332 | */ 333 | movetoAbs(x, y) { 334 | this.firstX = x; 335 | this.firstY = y; 336 | this.lastX = x; 337 | this.lastY = y; 338 | this.lastCommand = "M"; 339 | } 340 | 341 | /** 342 | * movetoRel - m 343 | * 344 | * @param {number} x 345 | * @param {number} y 346 | */ 347 | movetoRel(x, y) { 348 | this.firstX += x; 349 | this.firstY += y; 350 | this.lastX += x; 351 | this.lastY += y; 352 | this.lastCommand = "m"; 353 | } 354 | 355 | /** 356 | * curvetoQuadraticAbs - Q 357 | * 358 | * @param {number} x1 359 | * @param {number} y1 360 | * @param {number} x 361 | * @param {number} y 362 | */ 363 | curvetoQuadraticAbs(x1, y1, x, y) { 364 | this.addShape(this.shapeCreator.quadraticBezier( 365 | this.lastX, this.lastY, 366 | x1, y1, 367 | x, y 368 | )); 369 | 370 | this.lastX = x; 371 | this.lastY = y; 372 | this.lastCommand = "Q"; 373 | } 374 | 375 | /** 376 | * curvetoQuadraticRel - q 377 | * 378 | * @param {number} x1 379 | * @param {number} y1 380 | * @param {number} x 381 | * @param {number} y 382 | */ 383 | curvetoQuadraticRel(x1, y1, x, y) { 384 | this.addShape(this.shapeCreator.quadraticBezier( 385 | this.lastX, this.lastY, 386 | this.lastX + x1, this.lastY + y1, 387 | this.lastX + x, this.lastY + y 388 | )); 389 | 390 | this.lastX += x; 391 | this.lastY += y; 392 | this.lastCommand = "q"; 393 | } 394 | 395 | /** 396 | * curvetoCubicSmoothAbs - S 397 | * 398 | * @param {number} x2 399 | * @param {number} y2 400 | * @param {number} x 401 | * @param {number} y 402 | */ 403 | curvetoCubicSmoothAbs(x2, y2, x, y) { 404 | let controlX, controlY; 405 | 406 | if (this.lastCommand.match(/^[SsCc]$/)) { 407 | const secondToLast = this.shapes[this.shapes.length - 1].args[2]; 408 | 409 | controlX = 2 * this.lastX - secondToLast.x; 410 | controlY = 2 * this.lastY - secondToLast.y; 411 | } 412 | else { 413 | controlX = this.lastX; 414 | controlY = this.lastY; 415 | } 416 | 417 | this.addShape(this.shapeCreator.cubicBezier( 418 | this.lastX, this.lastY, 419 | controlX, controlY, 420 | x2, y2, 421 | x, y 422 | )); 423 | 424 | this.lastX = x; 425 | this.lastY = y; 426 | this.lastCommand = "S"; 427 | } 428 | 429 | /** 430 | * curvetoCubicSmoothRel - s 431 | * 432 | * @param {number} x2 433 | * @param {number} y2 434 | * @param {number} x 435 | * @param {number} y 436 | */ 437 | curvetoCubicSmoothRel(x2, y2, x, y) { 438 | let controlX, controlY; 439 | 440 | if (this.lastCommand.match(/^[SsCc]$/)) { 441 | const secondToLast = this.shapes[this.shapes.length - 1].args[2]; 442 | 443 | controlX = 2 * this.lastX - secondToLast.x; 444 | controlY = 2 * this.lastY - secondToLast.y; 445 | } 446 | else { 447 | controlX = this.lastX; 448 | controlY = this.lastY; 449 | } 450 | 451 | this.addShape(this.shapeCreator.cubicBezier( 452 | this.lastX, this.lastY, 453 | controlX, controlY, 454 | this.lastX + x2, this.lastY + y2, 455 | this.lastX + x, this.lastY + y 456 | )); 457 | 458 | this.lastX += x; 459 | this.lastY += y; 460 | this.lastCommand = "s"; 461 | } 462 | 463 | /** 464 | * curvetoQuadraticSmoothAbs - T 465 | * 466 | * @param {number} x 467 | * @param {number} y 468 | */ 469 | curvetoQuadraticSmoothAbs(x, y) { 470 | let controlX, controlY; 471 | 472 | if (this.lastCommand.match(/^[QqTt]$/)) { 473 | const secondToLast = this.shapes[this.shapes.length - 1].args[1]; 474 | 475 | controlX = 2 * this.lastX - secondToLast.x; 476 | controlY = 2 * this.lastY - secondToLast.y; 477 | } 478 | else { 479 | controlX = this.lastX; 480 | controlY = this.lastY; 481 | } 482 | 483 | this.addShape(this.shapeCreator.quadraticBezier( 484 | this.lastX, this.lastY, 485 | controlX, controlY, 486 | x, y 487 | )); 488 | 489 | this.lastX = x; 490 | this.lastY = y; 491 | this.lastCommand = "T"; 492 | } 493 | 494 | /** 495 | * curvetoQuadraticSmoothRel - t 496 | * 497 | * @param {number} x 498 | * @param {number} y 499 | */ 500 | curvetoQuadraticSmoothRel(x, y) { 501 | let controlX, controlY; 502 | 503 | if (this.lastCommand.match(/^[QqTt]$/)) { 504 | const secondToLast = this.shapes[this.shapes.length - 1].args[1]; 505 | 506 | controlX = 2 * this.lastX - secondToLast.x; 507 | controlY = 2 * this.lastY - secondToLast.y; 508 | } 509 | else { 510 | controlX = this.lastX; 511 | controlY = this.lastY; 512 | } 513 | 514 | this.addShape(this.shapeCreator.quadraticBezier( 515 | this.lastX, this.lastY, 516 | controlX, controlY, 517 | this.lastX + x, this.lastY + y 518 | )); 519 | 520 | this.lastX += x; 521 | this.lastY += y; 522 | this.lastCommand = "t"; 523 | } 524 | 525 | /** 526 | * linetoVerticalAbs - V 527 | * 528 | * @param {number} y 529 | */ 530 | linetoVerticalAbs(y) { 531 | this.addShape(this.shapeCreator.line( 532 | this.lastX, this.lastY, 533 | this.lastX, y 534 | )); 535 | 536 | this.lastY = y; 537 | 538 | this.lastCommand = "V"; 539 | } 540 | 541 | /** 542 | * linetoVerticalRel - v 543 | * 544 | * @param {number} y 545 | */ 546 | linetoVerticalRel(y) { 547 | this.addShape(this.shapeCreator.line( 548 | this.lastX, this.lastY, 549 | this.lastX, this.lastY + y 550 | )); 551 | 552 | this.lastY += y; 553 | 554 | this.lastCommand = "v"; 555 | } 556 | 557 | /** 558 | * closePath - z or Z 559 | */ 560 | closePath() { 561 | this.addShape(this.shapeCreator.line( 562 | this.lastX, this.lastY, 563 | this.firstX, this.firstY 564 | )); 565 | 566 | this.lastX = this.firstX; 567 | this.lastY = this.firstY; 568 | this.lastCommand = "z"; 569 | } 570 | } 571 | 572 | export default PathHandler; 573 | -------------------------------------------------------------------------------- /lib/ShapeInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ShapeInfo.js 3 | * @copyright 2002, 2017 Kevin Lindsey 4 | */ 5 | 6 | import {Point2D} from "kld-affine"; 7 | import {PathParser} from "kld-path-parser"; 8 | import PathHandler from "./PathHandler.js"; 9 | 10 | const degree90 = Math.PI * 0.5; 11 | const parser = new PathParser(); 12 | 13 | 14 | /** 15 | * getValues 16 | * 17 | * @param {Array} types 18 | * @param {Array} args 19 | * @returns {Array} 20 | */ 21 | export function getValues(types, args) { 22 | const result = []; 23 | 24 | for (const [names, type] of types) { 25 | let value = null; 26 | 27 | if (type === "Point2D") { 28 | value = parsePoint(names, args); 29 | } 30 | else if (type === "Number") { 31 | value = parseNumber(names, args); 32 | } 33 | else if (type === "Array" || type === "Point2D[]") { 34 | const values = []; 35 | 36 | while (args.length > 0) { 37 | values.push(parsePoint(names, args)); 38 | } 39 | 40 | if (values.length > 0) { 41 | value = values; 42 | } 43 | } 44 | else if (type === "Optional" || type === "Number?") { 45 | value = parseNumber(names, args); 46 | 47 | if (value === null) { 48 | value = undefined; 49 | } 50 | } 51 | else { 52 | throw new TypeError(`Unrecognized value type: ${type}`); 53 | } 54 | 55 | if (value !== null) { 56 | result.push(value); 57 | } 58 | else { 59 | throw new TypeError(`Unable to extract value for ${names}`); 60 | } 61 | } 62 | 63 | return result; 64 | } 65 | 66 | /** 67 | * parseNumber 68 | * 69 | * @param {Array} names 70 | * @param {Array} args 71 | * @returns {number} 72 | */ 73 | export function parseNumber(names, args) { 74 | let result = null; 75 | 76 | if (args.length > 0) { 77 | const item = args[0]; 78 | const itemType = typeof item; 79 | 80 | if (itemType === "number") { 81 | return args.shift(); 82 | } 83 | else if (itemType === "object") { 84 | for (const prop of names) { 85 | if (prop in item && typeof item[prop] === "number") { 86 | result = item[prop]; 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | 93 | return result; 94 | } 95 | 96 | /** 97 | * parsePoint 98 | * 99 | * @param {Array} names 100 | * @param {Array} args 101 | * @returns {Array} 102 | */ 103 | export function parsePoint(names, args) { 104 | let result = null; 105 | 106 | if (args.length > 0) { 107 | const item = args[0]; 108 | const itemType = typeof item; 109 | 110 | if (itemType === "number") { 111 | if (args.length > 1) { 112 | const x = args.shift(); 113 | const y = args.shift(); 114 | 115 | result = new Point2D(x, y); 116 | } 117 | } 118 | else if (Array.isArray(item) && item.length > 1) { 119 | if (item.length === 2) { 120 | const [x, y] = args.shift(); 121 | 122 | result = new Point2D(x, y); 123 | } 124 | else { 125 | throw new TypeError(`Unhandled array of length ${item.length}`); 126 | } 127 | } 128 | else if (itemType === "object") { 129 | if ("x" in item && "y" in item) { 130 | result = new Point2D(item.x, item.y); 131 | // eslint-disable-next-line compat/compat 132 | if (Object.keys(item).length === 2) { 133 | args.shift(); 134 | } 135 | } 136 | else { 137 | for (const props of names) { 138 | if (Array.isArray(props)) { 139 | if (props.every(p => p in item)) { 140 | result = new Point2D(item[props[0]], item[props[1]]); 141 | break; 142 | } 143 | } 144 | else if (props in item) { 145 | result = parsePoint([], [item[props]]); 146 | break; 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | return result; 154 | } 155 | 156 | /** 157 | * ShapeInfo 158 | * @memberof module:kld-intersections 159 | */ 160 | export default class ShapeInfo { 161 | /** 162 | * @param {string} name 163 | * @param {Array} args 164 | * @returns {module:kld-intersections.ShapeInfo} 165 | */ 166 | constructor(name, args) { 167 | this.name = name; 168 | this.args = args; 169 | } 170 | 171 | static arc(...args) { 172 | const types = [ 173 | [["center", ["centerX", "centerY"], ["cx", "cy"]], "Point2D"], 174 | [["radiusX", "rx"], "Number"], 175 | [["radiusY", "ry"], "Number"], 176 | [["startRadians"], "Number"], 177 | [["endRadians"], "Number"] 178 | ]; 179 | const values = getValues(types, args); 180 | 181 | return new ShapeInfo(ShapeInfo.ARC, values); 182 | } 183 | 184 | static quadraticBezier(...args) { 185 | const types = [ 186 | [["p1", ["p1x", "p1y"]], "Point2D"], 187 | [["p2", ["p2x", "p2y"]], "Point2D"], 188 | [["p3", ["p3x", "p3y"]], "Point2D"] 189 | ]; 190 | const values = getValues(types, args); 191 | 192 | return new ShapeInfo(ShapeInfo.QUADRATIC_BEZIER, values); 193 | } 194 | 195 | static cubicBezier(...args) { 196 | const types = [ 197 | [["p1", ["p1x", "p1y"]], "Point2D"], 198 | [["p2", ["p2x", "p2y"]], "Point2D"], 199 | [["p3", ["p3x", "p3y"]], "Point2D"], 200 | [["p4", ["p4x", "p4y"]], "Point2D"] 201 | ]; 202 | const values = getValues(types, args); 203 | 204 | return new ShapeInfo(ShapeInfo.CUBIC_BEZIER, values); 205 | } 206 | 207 | static circle(...args) { 208 | const types = [ 209 | [["center", ["centerX", "centerY"], ["cx", "cy"]], "Point2D"], 210 | [["radius", "r"], "Number"] 211 | ]; 212 | const values = getValues(types, args); 213 | 214 | return new ShapeInfo(ShapeInfo.CIRCLE, values); 215 | } 216 | 217 | static ellipse(...args) { 218 | const types = [ 219 | [["center", ["centerX", "centerY"], ["cx", "cy"]], "Point2D"], 220 | [["radiusX", "rx"], "Number"], 221 | [["radiusY", "ry"], "Number"] 222 | ]; 223 | const values = getValues(types, args); 224 | 225 | return new ShapeInfo(ShapeInfo.ELLIPSE, values); 226 | } 227 | 228 | static line(...args) { 229 | const types = [ 230 | [["p1", ["p1x", "p1y"], ["x1", "y1"]], "Point2D"], 231 | [["p2", ["p2x", "p2y"], ["x2", "y2"]], "Point2D"] 232 | ]; 233 | const values = getValues(types, args); 234 | 235 | return new ShapeInfo(ShapeInfo.LINE, values); 236 | } 237 | 238 | static path(...args) { 239 | parser.parseData(args[0]); 240 | 241 | return new ShapeInfo(ShapeInfo.PATH, handler.shapes); 242 | } 243 | 244 | static polygon(...args) { 245 | const types = [ 246 | [[], "Array"] 247 | ]; 248 | const values = getValues( 249 | types, 250 | args.length === 1 && Array.isArray(args[0]) ? args[0] : args 251 | ); 252 | 253 | return new ShapeInfo(ShapeInfo.POLYGON, values); 254 | } 255 | 256 | static polyline(...args) { 257 | const types = [ 258 | [[], "Array"] 259 | ]; 260 | const values = getValues( 261 | types, 262 | args.length === 1 && Array.isArray(args[0]) ? args[0] : args 263 | ); 264 | 265 | return new ShapeInfo(ShapeInfo.POLYLINE, values); 266 | } 267 | 268 | static rectangle(...args) { 269 | const types = [ 270 | [["topLeft", ["x", "y"], ["left", "top"]], "Point2D"], 271 | [["size", ["width", "height"], ["w", "h"]], "Point2D"], 272 | [["radiusX", "rx"], "Optional"], 273 | [["radiusY", "ry"], "Optional"] 274 | ]; 275 | const values = getValues(types, args); 276 | 277 | // fix up bottom-right point 278 | const p1 = values[0]; 279 | const p2 = values[1]; 280 | values[1] = new Point2D(p1.x + p2.x, p1.y + p2.y); 281 | 282 | // create shape info 283 | const result = new ShapeInfo(ShapeInfo.RECTANGLE, values); 284 | 285 | // handle possible rounded rectangle values 286 | let ry = result.args.pop(); 287 | let rx = result.args.pop(); 288 | 289 | rx = rx === undefined ? 0 : rx; 290 | ry = ry === undefined ? 0 : ry; 291 | 292 | if (rx === 0 && ry === 0) { 293 | return result; 294 | } 295 | 296 | const {x: p1x, y: p1y} = result.args[0]; 297 | const {x: p2x, y: p2y} = result.args[1]; 298 | const width = p2x - p1x; 299 | const height = p2y - p1y; 300 | 301 | if (rx === 0) { 302 | rx = ry; 303 | } 304 | if (ry === 0) { 305 | ry = rx; 306 | } 307 | if (rx > width * 0.5) { 308 | rx = width * 0.5; 309 | } 310 | if (ry > height * 0.5) { 311 | ry = height * 0.5; 312 | } 313 | 314 | const x0 = p1x; 315 | const y0 = p1y; 316 | const x1 = p1x + rx; 317 | const y1 = p1y + ry; 318 | const x2 = p2x - rx; 319 | const y2 = p2y - ry; 320 | const x3 = p2x; 321 | const y3 = p2y; 322 | 323 | const segments = [ 324 | ShapeInfo.arc(x1, y1, rx, ry, 2 * degree90, 3 * degree90), 325 | ShapeInfo.line(x1, y0, x2, y0), 326 | ShapeInfo.arc(x2, y1, rx, ry, 3 * degree90, 4 * degree90), 327 | ShapeInfo.line(x3, y1, x3, y2), 328 | ShapeInfo.arc(x2, y2, rx, ry, 0, degree90), 329 | ShapeInfo.line(x2, y3, x1, y3), 330 | ShapeInfo.arc(x1, y2, rx, ry, degree90, 2 * degree90), 331 | ShapeInfo.line(x0, y2, x0, y1) 332 | ]; 333 | 334 | return new ShapeInfo(ShapeInfo.PATH, segments); 335 | } 336 | } 337 | 338 | // define shape name constants 339 | ShapeInfo.ARC = "Arc"; 340 | ShapeInfo.QUADRATIC_BEZIER = "Bezier2"; 341 | ShapeInfo.CUBIC_BEZIER = "Bezier3"; 342 | ShapeInfo.CIRCLE = "Circle"; 343 | ShapeInfo.ELLIPSE = "Ellipse"; 344 | ShapeInfo.LINE = "Line"; 345 | ShapeInfo.PATH = "Path"; 346 | ShapeInfo.POLYGON = "Polygon"; 347 | ShapeInfo.POLYLINE = "Polyline"; 348 | ShapeInfo.RECTANGLE = "Rectangle"; 349 | 350 | // setup path parser handler after ShapeInfo has been defined 351 | const handler = new PathHandler(ShapeInfo); 352 | 353 | parser.setHandler(handler); 354 | -------------------------------------------------------------------------------- /lib/Shapes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | /** 3 | * Shapes 4 | * 5 | * @copyright 2017, Kevin Lindsey 6 | * @module Shapes 7 | * @deprecated use ShapeInfo 8 | */ 9 | 10 | import ShapeInfo from "./ShapeInfo.js"; 11 | 12 | /** 13 | * Build shapes for intersection 14 | */ 15 | class Shapes { 16 | /** 17 | * arc 18 | * 19 | * @deprecated use ShapeInfo.arc 20 | * @param {number} centerX 21 | * @param {number} centerY 22 | * @param {number} radiusX 23 | * @param {number} radiusY 24 | * @param {number} startRadians 25 | * @param {number} endRadians 26 | * @returns {module:kld-intersections.ShapeInfo} 27 | */ 28 | static arc(centerX, centerY, radiusX, radiusY, startRadians, endRadians) { 29 | return ShapeInfo.arc(...arguments); 30 | } 31 | 32 | /** 33 | * quadraticBezier 34 | * 35 | * @deprecated use ShapeInfo.quadraticBezier 36 | * @param {number} p1x 37 | * @param {number} p1y 38 | * @param {number} p2x 39 | * @param {number} p2y 40 | * @param {number} p3x 41 | * @param {number} p3y 42 | * @returns {module:kld-intersections.ShapeInfo} 43 | */ 44 | static quadraticBezier(p1x, p1y, p2x, p2y, p3x, p3y) { 45 | return ShapeInfo.quadraticBezier(...arguments); 46 | } 47 | 48 | /** 49 | * cubicBezier 50 | * 51 | * @deprecated use ShapeInfo.cubicBezier 52 | * @param {number} p1x 53 | * @param {number} p1y 54 | * @param {number} p2x 55 | * @param {number} p2y 56 | * @param {number} p3x 57 | * @param {number} p3y 58 | * @param {number} p4x 59 | * @param {number} p4y 60 | * @returns {module:kld-intersections.ShapeInfo} 61 | */ 62 | static cubicBezier(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { 63 | return ShapeInfo.cubicBezier(...arguments); 64 | } 65 | 66 | /** 67 | * circle 68 | * 69 | * @deprecated use ShapeInfo.circle 70 | * @param {number} centerX 71 | * @param {number} centerY 72 | * @param {number} radius 73 | * @returns {module:kld-intersections.ShapeInfo} 74 | */ 75 | static circle(centerX, centerY, radius) { 76 | return ShapeInfo.circle(...arguments); 77 | } 78 | 79 | /** 80 | * ellipse 81 | * 82 | * @deprecated use ShapeInfo.ellipse 83 | * @param {number} centerX 84 | * @param {number} centerY 85 | * @param {number} radiusX 86 | * @param {number} radiusY 87 | * @returns {module:kld-intersections.ShapeInfo} 88 | */ 89 | static ellipse(centerX, centerY, radiusX, radiusY) { 90 | return ShapeInfo.ellipse(...arguments); 91 | } 92 | 93 | /** 94 | * line 95 | * 96 | * @deprecated use ShapeInfo.line 97 | * @param {number} p1x 98 | * @param {number} p1y 99 | * @param {number} p2x 100 | * @param {number} p2y 101 | * @returns {module:kld-intersections.ShapeInfo} 102 | */ 103 | static line(p1x, p1y, p2x, p2y) { 104 | return ShapeInfo.line(...arguments); 105 | } 106 | 107 | /** 108 | * path 109 | * 110 | * @deprecated use ShapeInfo.path 111 | * @param {string} pathData 112 | * @returns {module:kld-intersections.ShapeInfo} 113 | */ 114 | static path(pathData) { 115 | return ShapeInfo.path(...arguments); 116 | } 117 | 118 | /** 119 | * polygon 120 | * 121 | * @deprecated use ShapeInfo.polygon 122 | * @param {Array} coords 123 | * @returns {module:kld-intersections.ShapeInfo} 124 | */ 125 | static polygon(coords) { 126 | return ShapeInfo.polygon(...arguments); 127 | } 128 | 129 | /** 130 | * polyline 131 | * 132 | * @deprecated use ShapeInfo.polyline 133 | * @param {Array} coords 134 | * @returns {module:kld-intersections.ShapeInfo} 135 | */ 136 | static polyline(coords) { 137 | return ShapeInfo.polyline(...arguments); 138 | } 139 | 140 | /** 141 | * rectangle 142 | * 143 | * @deprecated use ShapeInfo.rectangle 144 | * @param {number} x 145 | * @param {number} y 146 | * @param {number} width 147 | * @param {number} height 148 | * @param {number} [rx] 149 | * @param {number} [ry] 150 | * @returns {module:kld-intersections.ShapeInfo} 151 | */ 152 | static rectangle(x, y, width, height, rx = 0, ry = 0) { 153 | return ShapeInfo.rectangle(...arguments); 154 | } 155 | } 156 | 157 | export default Shapes; 158 | -------------------------------------------------------------------------------- /lib/SvgShapes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | import {Point2D} from "kld-affine"; 3 | import ShapeInfo from "./ShapeInfo.js"; 4 | 5 | class SvgShapes { 6 | /** 7 | * circle 8 | * 9 | * @param {SVGCircleElement} circle 10 | * @returns {module:kld-intersections.ShapeInfo} 11 | */ 12 | static circle(circle) { 13 | if (circle instanceof SVGCircleElement === false) { 14 | throw new TypeError(`Expected SVGCircleElement, but found ${circle}`); 15 | } 16 | 17 | const center = new Point2D( 18 | circle.cx.baseVal.value, 19 | circle.cy.baseVal.value 20 | ); 21 | const radius = circle.r.baseVal.value; 22 | 23 | return ShapeInfo.circle(center, radius); 24 | } 25 | 26 | /** 27 | * ellipse 28 | * 29 | * @param {SVGEllipseElement} ellipse 30 | * @returns {module:kld-intersections.ShapeInfo} 31 | */ 32 | static ellipse(ellipse) { 33 | if (ellipse instanceof SVGEllipseElement === false) { 34 | throw new TypeError(`Expected SVGEllipseElement, but found ${ellipse}`); 35 | } 36 | 37 | const center = new Point2D( 38 | ellipse.cx.baseVal.value, 39 | ellipse.cy.baseVal.value 40 | ); 41 | const radiusX = ellipse.rx.baseVal.value; 42 | const radiusY = ellipse.ry.baseVal.value; 43 | 44 | return ShapeInfo.ellipse(center, radiusX, radiusY); 45 | } 46 | 47 | /** 48 | * line 49 | * 50 | * @param {SVGLineElement} line 51 | * @returns {module:kld-intersections.ShapeInfo} 52 | */ 53 | static line(line) { 54 | if (line instanceof SVGLineElement === false) { 55 | throw new TypeError(`Expected SVGLineElement, but found ${line}`); 56 | } 57 | 58 | const p1 = new Point2D( 59 | line.x1.baseVal.value, 60 | line.y1.baseVal.value 61 | ); 62 | const p2 = new Point2D( 63 | line.x2.baseVal.value, 64 | line.y2.baseVal.value 65 | ); 66 | 67 | return ShapeInfo.line(p1, p2); 68 | } 69 | 70 | /** 71 | * path 72 | * 73 | * @param {SVGPathElement} path 74 | * @returns {module:kld-intersections.ShapeInfo} 75 | */ 76 | static path(path) { 77 | if (path instanceof SVGPathElement === false) { 78 | throw new TypeError(`Expected SVGPathElement, but found ${path}`); 79 | } 80 | 81 | const pathData = path.getAttributeNS(null, "d"); 82 | 83 | return ShapeInfo.path(pathData); 84 | } 85 | 86 | /** 87 | * polygon 88 | * 89 | * @param {SVGPolygonElement} polygon 90 | * @returns {module:kld-intersections.ShapeInfo} 91 | */ 92 | static polygon(polygon) { 93 | if (polygon instanceof SVGPolygonElement === false) { 94 | throw new TypeError(`Expected SVGPolygonElement, but found ${polygon}`); 95 | } 96 | 97 | const points = []; 98 | 99 | for (let i = 0; i < polygon.points.numberOfItems; i++) { 100 | const point = polygon.points.getItem(i); 101 | 102 | points.push(new Point2D(point.x, point.y)); 103 | } 104 | 105 | return ShapeInfo.polygon(points); 106 | } 107 | 108 | /** 109 | * polyline 110 | * 111 | * @param {SVGPolylineElement} polyline 112 | * @returns {module:kld-intersections.ShapeInfo} 113 | */ 114 | static polyline(polyline) { 115 | if (polyline instanceof SVGPolylineElement === false) { 116 | throw new TypeError(`Expected SVGPolylineElement, but found ${polyline}`); 117 | } 118 | 119 | const points = []; 120 | 121 | for (let i = 0; i < polyline.points.numberOfItems; i++) { 122 | const point = polyline.points.getItem(i); 123 | 124 | points.push(new Point2D(point.x, point.y)); 125 | } 126 | 127 | return ShapeInfo.polyline(points); 128 | } 129 | 130 | /** 131 | * rect 132 | * 133 | * @param {SVGRectElement} rect 134 | * @returns {module:kld-intersections.ShapeInfo} 135 | */ 136 | static rect(rect) { 137 | if (rect instanceof SVGRectElement === false) { 138 | throw new TypeError(`Expected SVGRectElement, but found ${rect}`); 139 | } 140 | 141 | return ShapeInfo.rectangle( 142 | rect.x.baseVal.value, 143 | rect.y.baseVal.value, 144 | rect.width.baseVal.value, 145 | rect.height.baseVal.value, 146 | rect.rx.baseVal.value, 147 | rect.ry.baseVal.value 148 | ); 149 | } 150 | 151 | /** 152 | * element 153 | * 154 | * @param {SVGElement} element 155 | * @returns {module:kld-intersections.ShapeInfo} 156 | */ 157 | static element(element) { 158 | if (element instanceof SVGElement === false) { 159 | throw new TypeError(`Expected SVGElement, but found ${element}`); 160 | } 161 | 162 | /* eslint-disable-next-line prefer-destructuring */ 163 | const tagName = element.tagName; 164 | 165 | switch (tagName) { 166 | case "circle": 167 | return SvgShapes.circle(element); 168 | case "ellipse": 169 | return SvgShapes.ellipse(element); 170 | case "line": 171 | return SvgShapes.line(element); 172 | case "path": 173 | return SvgShapes.path(element); 174 | case "polygon": 175 | return SvgShapes.polygon(element); 176 | case "polyline": 177 | return SvgShapes.polyline(element); 178 | case "rect": 179 | return SvgShapes.rect(element); 180 | default: 181 | throw new TypeError(`Unrecognized element type: '${tagName}'`); 182 | } 183 | } 184 | } 185 | 186 | export default SvgShapes; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kld-intersections", 3 | "version": "0.7.0", 4 | "description": "A library of intersection algorithms covering all SVG shape types", 5 | "author": { 6 | "name": "Kevin Lindsey", 7 | "email": "kevin@kevlindev.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Robert Benko (Quazistax)", 12 | "email": "quazistax@gmail.com" 13 | }, 14 | "Brett Zamir" 15 | ], 16 | "license": "BSD-3-Clause", 17 | "bugs": "https://github.com/thelonious/kld-intersections/issues", 18 | "homepage": "https://github.com/thelonious/kld-intersections", 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/thelonious/kld-intersections.git" 22 | }, 23 | "main": "dist/index-umd.js", 24 | "module": "index.js", 25 | "browserslist": [ 26 | "cover 100%" 27 | ], 28 | "scripts": { 29 | "eslint": "eslint --ext js,md .", 30 | "rollup": "rollup -c", 31 | "test": "npm run eslint && npm run rollup && mocha --require @babel/register", 32 | "start": "static -p 8055", 33 | "build-docs": "rm -rf docs/jsdoc/*;jsdoc --pedantic -c docs/jsdoc-config.js .", 34 | "open-docs": "opn http://localhost:8055/docs/jsdoc/ && npm start" 35 | }, 36 | "keywords": [ 37 | "intersection", 38 | "svg", 39 | "arc", 40 | "bezier", 41 | "circle", 42 | "ellipse", 43 | "line", 44 | "path", 45 | "polygon", 46 | "rectangle" 47 | ], 48 | "dependencies": { 49 | "kld-affine": "^2.1.1", 50 | "kld-path-parser": "^0.2.1", 51 | "kld-polynomial": "^0.3.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.9.6", 55 | "@babel/preset-env": "^7.9.6", 56 | "@babel/register": "^7.9.0", 57 | "@mysticatea/eslint-plugin": "^10.0.3", 58 | "eslint": "^5.16.0", 59 | "eslint-config-ash-nazg": "^4.0.0", 60 | "eslint-config-standard": "^12.0.0", 61 | "eslint-plugin-compat": "^3.5.1", 62 | "eslint-plugin-eslint-comments": "^3.1.2", 63 | "eslint-plugin-import": "^2.20.2", 64 | "eslint-plugin-jsdoc": "^4.8.4", 65 | "eslint-plugin-markdown": "^1.0.2", 66 | "eslint-plugin-no-use-extend-native": "^0.4.1", 67 | "eslint-plugin-node": "^9.2.0", 68 | "eslint-plugin-promise": "^4.2.1", 69 | "eslint-plugin-standard": "^4.0.1", 70 | "eslint-plugin-unicorn": "^8.0.2", 71 | "esm": "^3.2.25", 72 | "jsdoc": "^3.6.4", 73 | "kld-contours": "^0.3.2", 74 | "mocha": "^6.2.3", 75 | "node-static": "^0.7.11", 76 | "opn-cli": "^4.1.0", 77 | "rollup": "^1.32.1", 78 | "rollup-plugin-babel": "^4.4.0", 79 | "rollup-plugin-node-resolve": "^4.2.4", 80 | "rollup-plugin-terser": "^4.0.4", 81 | "typescript": "^3.8.3" 82 | }, 83 | "engines": { 84 | "node": ">= 10.15.3" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /reference/Bezout.m: -------------------------------------------------------------------------------- 1 | BeginPackage["Bezout`"] 2 | 3 | (* Mathematica 5.0 defines Global`BezoutMatrix as being protected with no 4 | value. I'm unprotecting it here. This may cause problems in future 5 | versions of Mathematica. *) 6 | 7 | ClearAttributes[BezoutMatrix, Protected] 8 | 9 | BezoutMatrix::usage = 10 | "Create a BezoutMatrix of order l without expanding its entries" 11 | 12 | Begin["`Private`"] 13 | bezoutMin[i_, j_, l_] := Min[l, 2 l - 1 - i - j] 14 | bezoutMax[i_, j_, l_] := Max[l - j, l - i] 15 | bezoutSub[i_, j_, k_, l_] := 2 l - 1 - i - j - k 16 | 17 | bezoutEntry[i_, j_, l_] := \!\(Sum[Global`v\_\(k, bezoutSub[i, j, k, l]\), {k, bezoutMax[i, j, l], bezoutMin[i, j, l]}]\) 18 | BezoutMatrix[l_Integer] := Table[bezoutEntry[i, j, l], {i, 0, l - 1}, {j, 0, l - 1}] 19 | End[] 20 | EndPackage[] 21 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import {terser} from "rollup-plugin-terser"; 4 | 5 | /** 6 | * @external RollupConfig 7 | * @type {PlainObject} 8 | * @see {@link https://rollupjs.org/guide/en#big-list-of-options} 9 | */ 10 | 11 | /** 12 | * @param {PlainObject} [config={}] 13 | * @param {boolean} [config.minifying] 14 | * @param {string} [config.format="umd"] 15 | * @returns {external:RollupConfig} 16 | */ 17 | function getRollupObject({minifying, format = "umd"} = {}) { 18 | const nonMinified = { 19 | input: "index.js", 20 | output: { 21 | format, 22 | sourcemap: minifying, 23 | file: `dist/index-${format}${minifying ? ".min" : ""}.js`, 24 | name: "KldIntersections" 25 | }, 26 | plugins: [ 27 | babel({ 28 | babelrc: false, 29 | presets: [ 30 | ["@babel/env", {modules: false}] 31 | ] 32 | }), 33 | resolve() 34 | ] 35 | }; 36 | if (minifying) { 37 | nonMinified.plugins.push(terser()); 38 | } 39 | return nonMinified; 40 | } 41 | 42 | // eslint-disable-next-line import/no-anonymous-default-export 43 | export default [ 44 | getRollupObject({minifying: true, format: "umd"}), 45 | getRollupObject({minifying: false, format: "umd"}), 46 | getRollupObject({minifying: true, format: "esm"}), 47 | getRollupObject({minifying: false, format: "esm"}) 48 | ]; 49 | -------------------------------------------------------------------------------- /scratchpad/arc-angle-test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Point2D, Vector2D} from "../index.js"; 3 | 4 | const EPSILON = 0.1; //1e-6; 5 | const TWO_PI = Math.PI * 2.0; 6 | 7 | const angleStep = 100; 8 | const centerX = 100; 9 | const centerY = 75; 10 | const radius = 50; 11 | 12 | describe("Arc Angles", () => { 13 | for (let startAngle = 0; startAngle < 360; startAngle += angleStep) { 14 | for (let endAngle = 0; endAngle < 360; endAngle += angleStep) { 15 | if (startAngle === endAngle) { 16 | continue; 17 | } 18 | 19 | it(`${startAngle} - ${endAngle}`, () => { 20 | const [startPoint, endPoint, flagA, flagS] = getArcCommandParameters( 21 | centerX, centerY, 22 | radius, radius, 23 | toRadians(startAngle), toRadians(endAngle) 24 | ); 25 | const [cx, cy, rx, ry, sa, ea] = getArcParameters( 26 | startPoint, endPoint, 27 | radius, radius, 28 | toRadians(endAngle - startAngle), 29 | flagA, flagS 30 | ); 31 | 32 | // assertNumbers(cx, centerX, "center x"); 33 | // assertNumbers(cy, centerY, "center y"); 34 | assertNumbers(rx, radius, "radius x"); 35 | assertNumbers(ry, radius, "radius y"); 36 | assertNumbers(sa < 0 ? sa + TWO_PI : sa, toRadians(startAngle), "start angle"); 37 | assertNumbers(ea < 0 ? ea + TWO_PI : ea, toRadians(endAngle), "end angle"); 38 | }); 39 | } 40 | } 41 | }); 42 | 43 | function assertNumbers(a, b, message) { 44 | assert(Math.abs(a - b) < EPSILON, `${message}: ${a} != ${b}`); 45 | } 46 | 47 | function toRadians(degrees) { 48 | return Math.PI * degrees / 180.0; 49 | } 50 | 51 | function toCartesian(centerX, centerY, radiusX, radiusY, angleRadians) { 52 | return new Point2D( 53 | centerX + (radiusX * Math.cos(angleRadians)), 54 | centerY + (radiusY * Math.sin(angleRadians)) 55 | ); 56 | } 57 | 58 | function getArcCommandParameters(centerX, centerY, radiusX, radiusY, startRadians, endRadians) { 59 | return [ 60 | toCartesian(centerX, centerY, radiusX, radiusY, startRadians), 61 | toCartesian(centerX, centerY, radiusX, radiusY, endRadians), 62 | (endRadians - startRadians) > Math.PI ? 1 : 0, 63 | (endRadians - startRadians) > 0 ? 1 : 0 64 | ]; 65 | } 66 | 67 | function getArcParameters(startPoint, endPoint, rx, ry, angle, arcFlag, sweepFlag) { 68 | angle = angle * Math.PI / 180; 69 | 70 | const c = Math.cos(angle); 71 | const s = Math.sin(angle); 72 | const TOLERANCE = 1e-6; 73 | 74 | // Section (F.6.5.1) 75 | const halfDiff = startPoint.subtract(endPoint).multiply(0.5); 76 | const x1p = halfDiff.x * c + halfDiff.y * s; 77 | const y1p = halfDiff.x * -s + halfDiff.y * c; 78 | 79 | // Section (F.6.6.1) 80 | rx = Math.abs(rx); 81 | ry = Math.abs(ry); 82 | 83 | // Section (F.6.6.2) 84 | const x1px1p = x1p * x1p; 85 | const y1py1p = y1p * y1p; 86 | const lambda = (x1px1p / (rx * rx)) + (y1py1p / (ry * ry)); 87 | 88 | // Section (F.6.6.3) 89 | if (lambda > 1) { 90 | const factor = Math.sqrt(lambda); 91 | 92 | rx *= factor; 93 | ry *= factor; 94 | } 95 | 96 | // Section (F.6.5.2) 97 | const rxrx = rx * rx; 98 | const ryry = ry * ry; 99 | const rxy1 = rxrx * y1py1p; 100 | const ryx1 = ryry * x1px1p; 101 | 102 | let factor = (rxrx * ryry - rxy1 - ryx1) / (rxy1 + ryx1); 103 | 104 | if (Math.abs(factor) < TOLERANCE) { 105 | factor = 0; 106 | } 107 | 108 | let sq = Math.sqrt(factor); 109 | 110 | if (arcFlag === sweepFlag) { 111 | sq = -sq; 112 | } 113 | 114 | // Section (F.6.5.3) 115 | const mid = startPoint.add(endPoint).multiply(0.5); 116 | const cxp = sq * rx * y1p / ry; 117 | const cyp = sq * -ry * x1p / rx; 118 | 119 | // Section (F.6.5.5 - F.6.5.6) 120 | const xcr1 = (x1p - cxp) / rx; 121 | const xcr2 = (x1p + cxp) / rx; 122 | const ycr1 = (y1p - cyp) / ry; 123 | const ycr2 = (y1p + cyp) / ry; 124 | 125 | const theta1 = new Vector2D(1, 0).angleBetween(new Vector2D(xcr1, ycr1)); 126 | // let deltaTheta = normalizeAngle(new Vector2D(xcr1, ycr1).angleBetween(new Vector2D(-xcr2, -ycr2))); 127 | let deltaTheta = new Vector2D(xcr1, ycr1).angleBetween(new Vector2D(-xcr2, -ycr2)) % TWO_PI; 128 | 129 | if (sweepFlag === false) { 130 | deltaTheta -= TWO_PI; 131 | } 132 | 133 | console.log(deltaTheta); 134 | 135 | return [ 136 | cxp * c - cyp * s + mid.x, 137 | cxp * s + cyp * c + mid.y, 138 | rx, 139 | ry, 140 | theta1, 141 | theta1 + deltaTheta 142 | ]; 143 | } 144 | -------------------------------------------------------------------------------- /scratchpad/arc-fail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import {ShapeInfo, Intersection} from "../index.js"; 4 | 5 | // create geometry 6 | const path = "M80,80 a20,20 0 0,0 0,40 h50 a20,20 0 0,0 0,-40 a10,10 0 0,0 -15,-10 a15,15 0 0,0 -35,10z" 7 | const line = [-105, -45, 245, 230] 8 | const [x1, y1, x2, y2] = line 9 | 10 | // create shapes 11 | const shape1 = ShapeInfo.path(path); 12 | const shape2 = ShapeInfo.line(line) 13 | 14 | // intersect 15 | const result = Intersection.intersect(shape1, shape2); 16 | 17 | // build SVG file showing the shapes and intersection points 18 | const intersectionSVG = result.points.map(p => { 19 | return ``; 20 | }).join("\n "); 21 | 22 | const svg = ` 23 | 24 | 25 | 26 | ${intersectionSVG} 27 | 28 | `; 29 | 30 | console.log(svg); 31 | -------------------------------------------------------------------------------- /scratchpad/arc-test-2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import {Shapes, Intersection} from "../index.js"; 4 | 5 | const pathData = "M100,50 A50 50 0 0 1 0,50"; 6 | const path = Shapes.path(pathData); 7 | 8 | const x1 = 0; 9 | const y1 = 100; 10 | const x2 = 100; 11 | const y2 = 0; 12 | const line = Shapes.line(x1, y1, x2, y2); 13 | 14 | // intersect 15 | const result = Intersection.intersect(path, line); 16 | 17 | // build SVG file showing the shapes, the center point, and intersection points 18 | const intersectionSVG = result.points.map(p => { 19 | return ``; 20 | }).join("\n "); 21 | 22 | const svg = ` 23 | 24 | 25 | 26 | ${intersectionSVG} 27 | 28 | `; 29 | 30 | console.log(svg); 31 | -------------------------------------------------------------------------------- /scratchpad/arc-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import {Intersection, Shapes, Point2D} from "../index.js"; 4 | 5 | function toCartesian(centerX, centerY, radiusX, radiusY, angleRadians) { 6 | return new Point2D( 7 | centerX + (radiusX * Math.cos(angleRadians)), 8 | centerY + (radiusY * Math.sin(angleRadians)) 9 | ); 10 | } 11 | 12 | const centerX = 50; 13 | const centerY = 50; 14 | const radiusX = 50; 15 | const radiusY = 50; 16 | const startRadians = 0; 17 | const endRadians = Math.PI; 18 | const arc = Shapes.arc(centerX, centerY, radiusX, radiusY, startRadians, endRadians); 19 | 20 | const x1 = 0; 21 | const y1 = 100; 22 | const x2 = 100; 23 | const y2 = 0; 24 | const line = Shapes.line(x1, y1, x2, y2); 25 | 26 | // console.log(arc); 27 | // console.log(line); 28 | 29 | const startPoint = toCartesian(centerX, centerY, radiusX, radiusY, startRadians); 30 | const endPoint = toCartesian(centerX, centerY, radiusX, radiusY, endRadians); 31 | const flagA = (endRadians - startRadians) > Math.PI ? 1 : 0; 32 | const flagS = (endRadians - startRadians) > 0 ? 1 : 0; 33 | 34 | // intersect 35 | const result = Intersection.intersect(arc, line); 36 | 37 | // build SVG file showing the shapes, the center point, and intersection points 38 | const intersectionSVG = result.points.map(p => { 39 | return ``; 40 | }).join("\n "); 41 | 42 | const svg = ` 43 | 44 | 45 | 46 | ${intersectionSVG} 47 | 48 | `; 49 | 50 | console.log(svg); 51 | -------------------------------------------------------------------------------- /scratchpad/cubic-cubic-fail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import {Shapes, Intersection} from "../index.js"; 4 | 5 | // define path data 6 | const pathData1 = "M150,150 C183.33333333333331,216.66666666666663 233.33333333333337,216.66666666666663 300,150"; 7 | const pathData2 = "M100,200 C166.66666666666663,133.33333333333337 233.33333333333337,133.33333333333337 300,200"; 8 | 9 | // create pathS 10 | const path1 = Shapes.path(pathData1); 11 | const path2 = Shapes.path(pathData2); 12 | 13 | // intersect 14 | const result = Intersection.intersect(path1, path2); 15 | 16 | // build SVG file showing the shapes, the center point, and intersection points 17 | const intersectionSVG = result.points.map(p => { 18 | return ``; 19 | }).join("\n "); 20 | 21 | const svg = ` 22 | 23 | 24 | 25 | ${intersectionSVG} 26 | 27 | `; 28 | 29 | console.log(svg); 30 | -------------------------------------------------------------------------------- /scratchpad/find_parameters.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import {CubicBezier2D} from "kld-contours"; 4 | import {Point2D, Intersection, IntersectionArgs} from "../index.js"; 5 | 6 | /** 7 | * 8 | * @param {CubicBezier2D} path 9 | * @param {Point2D} point 10 | * @returns {number} 11 | */ 12 | function findParameter(path, point) { 13 | const {x, y} = path.getBernsteinPolynomials(); 14 | 15 | x.coefs[0] -= point.x; 16 | 17 | const roots = x.getRootsInInterval(0.0, 1.0); 18 | 19 | for (const t of roots) { 20 | const candidateY = y.eval(t); 21 | 22 | if (Math.abs(candidateY - point.y) < 1e-6) { 23 | return t; 24 | } 25 | } 26 | 27 | return NaN; 28 | } 29 | 30 | // parser path data 31 | const b1 = new CubicBezier2D( 32 | new Point2D(150, 150), 33 | new Point2D(184, 217), 34 | new Point2D(233, 217), 35 | new Point2D(300, 150) 36 | ); 37 | const b2 = new CubicBezier2D( 38 | new Point2D(100, 200), 39 | new Point2D(167, 133), 40 | new Point2D(233, 133), 41 | new Point2D(300, 200) 42 | ); 43 | 44 | const path1 = new IntersectionArgs("Bezier3", [b1.p1, b1.p2, b1.p3, b1.p4]); 45 | const path2 = new IntersectionArgs("Bezier3", [b2.p1, b2.p2, b2.p3, b2.p4]); 46 | 47 | // intersect 48 | const result = Intersection.intersect(path1, path2); 49 | 50 | // find bezier parametric values from first intersection point 51 | const param1 = findParameter(b1, result.points[0]); 52 | const param2 = findParameter(b2, result.points[0]); 53 | 54 | // calculate points on curves from their parametric values 55 | const point1 = b1.getPointAtParameter(param1); 56 | const point2 = b2.getPointAtParameter(param2); 57 | 58 | // compare the results 59 | console.log(); 60 | console.log("These should all be pretty close to the same point"); 61 | console.log(`Reported intersection: ${result.points[0]}`); 62 | console.log(`Point on first curve: ${point1}, t=${param1}`); 63 | console.log(`Point on second curve: ${point2}, t=${param2}`); 64 | -------------------------------------------------------------------------------- /scratchpad/shapeInfo-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | import util from "util"; 4 | import ShapeInfo from "../lib/ShapeInfo.js"; 5 | import {Point2D, Vector2D} from "kld-affine"; 6 | 7 | const circle = ShapeInfo.circle({ 8 | center: new Point2D(80, 60), 9 | radius: 40 10 | }); 11 | const circle2 = ShapeInfo.circle(80, 60, 40); 12 | const circle3 = ShapeInfo.circle(new Point2D(80, 60), 40); 13 | 14 | const ellipse = ShapeInfo.ellipse({ 15 | center: new Point2D(80, 60), 16 | radiusX: 30, 17 | radiusY: 40 18 | }); 19 | const ellipse2 = ShapeInfo.ellipse(80, 60, 30, 40); 20 | const ellipse3 = ShapeInfo.ellipse(new Point2D(80, 60), 30, 40); 21 | 22 | const line = ShapeInfo.line({ 23 | p1: new Point2D(10, 20), 24 | p2: new Point2D(110, 120) 25 | }); 26 | const line2 = ShapeInfo.line(10, 20, 110, 120); 27 | const line3 = ShapeInfo.line(new Point2D(10, 20), new Point2D(110, 120)); 28 | 29 | const path = ShapeInfo.path({ d: "M0 10 L100 110"}); 30 | const path2 = ShapeInfo.path("M0 10 L100 110"); 31 | // const path3 = ShapeInfo.path({ 32 | // segments: [ 33 | // ShapeInfo.line({p1: new Point2D(10, 10), p2: new Point2D(110, 110)}) 34 | // ] 35 | // }); 36 | 37 | const polygon = ShapeInfo.polygon({ 38 | points: [ 39 | new Point2D(10, 20), 40 | new Point2D(60, 20), 41 | new Point2D(60, 100), 42 | new Point2D(10, 100) 43 | ] 44 | }); 45 | const polygon2 = ShapeInfo.polygon([10, 20, 60, 20, 60, 100, 10, 100]); 46 | const polygon3 = ShapeInfo.polygon([new Point2D(10, 20), new Point2D(60, 20), new Point2D(60, 100), new Point2D(10, 100)]); 47 | 48 | const rectangle = ShapeInfo.rectangle({ 49 | topLeft: new Point2D(35, 25), 50 | bottomRight: new Point2D(80, 100) 51 | }); 52 | const rectangle2 = ShapeInfo.rectangle(35, 25, 80, 100); 53 | const rectangle3 = ShapeInfo.rectangle(new Point2D(35, 25), new Vector2D(80, 100)); 54 | 55 | const shapes = [ 56 | circle, circle2, circle3, 57 | ellipse, ellipse2, ellipse3, 58 | line, line2, line3, 59 | path, path2, /*path3,*/ 60 | polygon, polygon2, polygon3, 61 | rectangle, rectangle2, rectangle3 62 | ]; 63 | 64 | shapes.forEach(shape => console.log(util.inspect(shape, {depth: 4}))); 65 | -------------------------------------------------------------------------------- /scratchpad/temp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scratchpad/visual-test-conversion/convert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # batch convert all example SVGs from http://www.kevlindev.com/geometry/2D/intersections/index.htm 4 | 5 | for f in ../../visual-test-original/*.svg; do 6 | filename=$(basename ${f}) 7 | target=${filename#*_} 8 | xsltproc --output "../../visual-test/${target}" transform_visual_test.xsl ${f} 9 | done 10 | -------------------------------------------------------------------------------- /scratchpad/visual-test-conversion/transform_visual_test.xsl: -------------------------------------------------------------------------------- 1 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /test/Itertools.js: -------------------------------------------------------------------------------- 1 | // TODO: Consider switching these functions over to using iteration and then 2 | // turning into its own module 3 | 4 | function zip(...arrays) { 5 | const lengths = arrays.map(a => a.length); 6 | const minLength = Math.min(...lengths); 7 | const result = []; 8 | 9 | for (let i = 0; i < minLength; i++) { 10 | let element = arrays.map(a => a[i]); 11 | 12 | result.push(element); 13 | } 14 | 15 | return result; 16 | } 17 | 18 | class Product { 19 | constructor(...items) { 20 | const itemsLength = items.length; 21 | 22 | if (itemsLength > 0) { 23 | if (typeof items[itemsLength - 1] === "number" ) { 24 | this.repeating = items.pop(); 25 | } 26 | else { 27 | this.repeating = 1; 28 | } 29 | 30 | this.items = items; 31 | } 32 | else { 33 | this.items = []; 34 | this.repeating = 1; 35 | } 36 | } 37 | 38 | [Symbol.iterator]() { 39 | return { 40 | indexes: Array(this.items.length * this.repeating).fill(0), 41 | lists: Array(this.repeating).fill(this.items).reduce((acc, val) => acc.concat(val), []), 42 | complete: this.items.count === 0, 43 | 44 | next() { 45 | if (this.complete === true) { 46 | return {done: true} 47 | } 48 | 49 | let result = []; 50 | 51 | for (let [list, index] of zip(this.lists, this.indexes)) { 52 | result.push(list[index]); 53 | } 54 | 55 | this.complete = true; 56 | 57 | for (let i = this.indexes.length - 1; i >= 0; i--) { 58 | const list = this.lists[i]; 59 | let index = this.indexes[i]; 60 | 61 | index += 1; 62 | 63 | if (index >= list.length) { 64 | this.indexes[i] = 0; 65 | } 66 | else { 67 | this.indexes[i] = index; 68 | this.complete = false; 69 | break; 70 | } 71 | } 72 | 73 | return {value: result, done: false}; 74 | } 75 | } 76 | } 77 | } 78 | 79 | module.exports = { 80 | zip, 81 | Product, 82 | // Permutation 83 | }; 84 | -------------------------------------------------------------------------------- /test/affine-shapes-test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Point2D, Vector2D} from "kld-affine"; 3 | import AffineShapes from "../lib/AffineShapes.js"; 4 | import ShapeInfo from "../lib/ShapeInfo.js"; 5 | 6 | 7 | describe("Shapes API", () => { 8 | it("arc", () => { 9 | const result = AffineShapes.arc(new Point2D(10, 20), 30, 40, 0, 1.57); 10 | const expected = new ShapeInfo("Arc", [new Point2D(10, 20), 30, 40, 0, 1.57]); 11 | 12 | assert.deepStrictEqual(result, expected); 13 | }); 14 | it("quadratic bezier", () => { 15 | const result = AffineShapes.quadraticBezier(new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60)); 16 | const expected = new ShapeInfo("Bezier2", [new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60)]); 17 | 18 | assert.deepStrictEqual(result, expected); 19 | }); 20 | it("cubic bezier", () => { 21 | const result = AffineShapes.cubicBezier(new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60), new Point2D(70, 80)); 22 | const expected = new ShapeInfo("Bezier3", [new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60), new Point2D(70, 80)]); 23 | 24 | assert.deepStrictEqual(result, expected); 25 | }); 26 | it("circle", () => { 27 | const result = AffineShapes.circle(new Point2D(10, 20), 30); 28 | const expected = new ShapeInfo("Circle", [new Point2D(10, 20), 30]); 29 | 30 | assert.deepStrictEqual(result, expected); 31 | }); 32 | it("ellipse", () => { 33 | const result = AffineShapes.ellipse(new Point2D(10, 20), 30, 40); 34 | const expected = new ShapeInfo("Ellipse", [new Point2D(10, 20), 30, 40]); 35 | 36 | assert.deepStrictEqual(result, expected); 37 | }); 38 | it("line", () => { 39 | const result = AffineShapes.line(new Point2D(10, 20), new Point2D(30, 40)); 40 | const expected = new ShapeInfo("Line", [new Point2D(10, 20), new Point2D(30, 40)]); 41 | 42 | assert.deepStrictEqual(result, expected); 43 | }); 44 | it("path", () => { 45 | const result = AffineShapes.path("M0, 10 L100, 110"); 46 | const expected = new ShapeInfo("Path", [ 47 | AffineShapes.line(new Point2D(0, 10), new Point2D(100, 110)) 48 | ]); 49 | 50 | assert.deepStrictEqual(result, expected); 51 | }); 52 | it("polygon", () => { 53 | const result = AffineShapes.polygon(new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60)); 54 | const expected = new ShapeInfo("Polygon", [ 55 | [ 56 | new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60) 57 | ] 58 | ]); 59 | 60 | assert.deepStrictEqual(result, expected); 61 | }); 62 | it("polyline", () => { 63 | const result = AffineShapes.polyline(new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60)); 64 | const expected = new ShapeInfo("Polyline", [ 65 | [ 66 | new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60) 67 | ] 68 | ]); 69 | 70 | assert.deepStrictEqual(result, expected); 71 | }); 72 | it("rectangle", () => { 73 | const result = AffineShapes.rectangle(new Point2D(10, 20), new Vector2D(30, 40)); 74 | const expected = new ShapeInfo("Rectangle", [new Point2D(10, 20), new Point2D(40, 60)]); 75 | 76 | assert.deepStrictEqual(result, expected); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/intersection-tests.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Shapes, Intersection, Point2D} from "../index.js"; 3 | 4 | /** 5 | * A sorter for Point2Ds 6 | * 7 | * @param {Point2D} p1 8 | * @param {Point2D} p2 9 | * @returns {number} 10 | */ 11 | function pointSorter(p1, p2) { 12 | if (p1.x < p2.x) { 13 | return -1; 14 | } 15 | else if (p1.x > p2.x) { 16 | return 1; 17 | } 18 | else if (p1.y < p2.y) { 19 | return -1; 20 | } 21 | else if (p1.y > p2.y) { 22 | return 1; 23 | } 24 | 25 | return 0; 26 | } 27 | 28 | /** 29 | * Assert two points for equality 30 | * 31 | * @param {Array} points1 32 | * @param {Array} points2 33 | */ 34 | function assertEqualPoints(points1, points2) { 35 | assert.strictEqual(points1.length, points2.length); 36 | 37 | /* eslint-disable-next-line unicorn/no-for-loop */ 38 | for (let i = 0; i < points1.length; i++) { 39 | const p1 = points1[i]; 40 | const p2 = points2[i]; 41 | 42 | assert(p1.equals(p2)); 43 | } 44 | } 45 | 46 | /** 47 | * Intersect two shapes and assert the intersection points 48 | * 49 | * @param {ShapeInfo} shape1 50 | * @param {ShapeInfo} shape2 51 | * @param {Array} expected 52 | */ 53 | function assertIntersections(shape1, shape2, expected) { 54 | const result = Intersection.intersect(shape1, shape2); 55 | const points = result.points.sort(pointSorter); 56 | 57 | assertEqualPoints(points, expected); 58 | } 59 | 60 | describe("Intersections", () => { 61 | it("Quadratic Bezier - Quadratic Bezier", () => { 62 | const shape1 = Shapes.path("M83,214 Q335,173 91,137"); 63 | const shape2 = Shapes.path("M92,233 Q152,30 198,227"); 64 | 65 | const expected = [ 66 | new Point2D(98.72720538504241, 211.3625905201401), 67 | new Point2D(129.54187987698563, 143.27285394359626), 68 | new Point2D(173.83770343111445, 152.940611499889), 69 | new Point2D(188.27575037023598, 190.33459908605727) 70 | ]; 71 | 72 | assertIntersections(shape1, shape2, expected); 73 | }); 74 | it("Quadratic Bezier - Cubic Bezier", () => { 75 | const shape1 = Shapes.path("M123,47 Q146,255 188,47"); 76 | const shape2 = Shapes.path("M171,143 C22,132 330,64 107,65"); 77 | const expected = [ 78 | new Point2D(125.13460408076253, 65.08475401519482), 79 | new Point2D(134.49717251169614, 120.3114904768965), 80 | new Point2D(140.0203543393166, 138.4178510972871), 81 | new Point2D(160.68034080878516, 142.07160371675803), 82 | new Point2D(177.7766308623799, 92.57967705087721), 83 | new Point2D(182.93500601086055, 70.88034340965095) 84 | ]; 85 | 86 | assertIntersections(shape1, shape2, expected); 87 | }); 88 | it("Quadratic Bezier - Circle", () => { 89 | const shape1 = Shapes.path("M50,20 Q200,300 350,10"); 90 | const shape2 = Shapes.circle(80, 80, 40); 91 | const expected = [ 92 | new Point2D(63.27523884077001, 43.66431004966725), 93 | new Point2D(108.75184271113037, 107.8088392550718) 94 | ]; 95 | 96 | assertIntersections(shape1, shape2, expected); 97 | }); 98 | it("Quadratic Bezier - Ellipse", () => { 99 | const shape1 = Shapes.path("M50,20 Q200,300 350,10"); 100 | const shape2 = Shapes.ellipse(80, 80, 30, 40); 101 | const expected = [ 102 | new Point2D(64.5070161096711, 45.74689113414642), 103 | new Point2D(104.55299413054938, 102.98407097584307) 104 | ]; 105 | 106 | assertIntersections(shape1, shape2, expected); 107 | }); 108 | it("Quadratic Bezier - Line", () => { 109 | const shape1 = Shapes.path("M50,20 Q200,300 350,10"); 110 | const shape2 = Shapes.line(10, 20, 300, 210); 111 | const expected = [ 112 | new Point2D(74.86365117074867, 62.49687490497326), 113 | new Point2D(216.42491507244551, 155.2439098750505) 114 | ]; 115 | 116 | assertIntersections(shape1, shape2, expected); 117 | }); 118 | it("Quadratic Bezier - Polygon", () => { 119 | const shape1 = Shapes.path("M50,20 Q200,300 350,10"); 120 | const shape2 = Shapes.polygon([58, 30, 110, 27, 135, 108, 74, 145, 28, 108, 26, 53]); 121 | const expected = [ 122 | new Point2D(56.18558820664384, 31.304108476474752), 123 | new Point2D(118.41228100507112, 118.06140332479293) 124 | ]; 125 | 126 | assertIntersections(shape1, shape2, expected); 127 | }); 128 | it("Quadratic Bezier - Rectangle", () => { 129 | const shape1 = Shapes.path("M50,20 Q200,300 350,10"); 130 | const shape2 = Shapes.rectangle(35, 25, 80, 100); 131 | const expected = [ 132 | new Point2D(52.70336708542367, 25.000000000000014), 133 | new Point2D(115, 114.575) 134 | ]; 135 | 136 | assertIntersections(shape1, shape2, expected); 137 | }); 138 | it("Cubic Bezier - Cubic Bezier", () => { 139 | const shape1 = Shapes.path("M203,140 C206,359 245,6 248,212"); 140 | const shape2 = Shapes.path("M177,204 C441,204 8,149 265,154"); 141 | const expected = [ 142 | new Point2D(203.26720646036085, 154.38186574206628), 143 | new Point2D(203.80448029878028, 171.0080340325034), 144 | new Point2D(206.53089745589955, 203.7205799064048), 145 | new Point2D(218.26560490823644, 203.3977092167175), 146 | new Point2D(226.37034491873996, 177.96211427292837), 147 | new Point2D(234.28537892520342, 153.67909587175902), 148 | new Point2D(244.55650504358164, 153.7151359675932), 149 | new Point2D(247.269811682568, 184.6978038847097), 150 | new Point2D(247.8042453163769, 201.4484074305467) 151 | ]; 152 | 153 | assertIntersections(shape1, shape2, expected); 154 | }); 155 | it("Cubic Bezier - Circle", () => { 156 | const shape1 = Shapes.path("M50,20 C50,210 250,20 250,210"); 157 | const shape2 = Shapes.circle(80, 80, 40); 158 | const expected = [ 159 | new Point2D(52.22352295755968, 51.21689170519913), 160 | new Point2D(104.03665107361067, 111.97247887113267) 161 | ]; 162 | 163 | assertIntersections(shape1, shape2, expected); 164 | }); 165 | it("Cubic Bezier - Ellipse", () => { 166 | const shape1 = Shapes.path("M50,20 C50,210 250,20 250,210"); 167 | const shape2 = Shapes.ellipse(80, 80, 30, 40); 168 | const expected = [ 169 | new Point2D(53.96110954819515, 60.1348464093258), 170 | new Point2D(99.14716919990332, 110.79353212407386) 171 | ]; 172 | 173 | assertIntersections(shape1, shape2, expected); 174 | }); 175 | it("Cubic Bezier - Line", () => { 176 | const shape1 = Shapes.path("M50,20 C50,210 250,20 250,210"); 177 | const shape2 = Shapes.line(10, 20, 300, 210); 178 | const expected = [ 179 | new Point2D(51.64783510324115, 47.286512653847666), 180 | new Point2D(155.00539377733003, 115.0035338541128), 181 | new Point2D(247.19292496558265, 175.4022611843473) 182 | ]; 183 | 184 | assertIntersections(shape1, shape2, expected); 185 | }); 186 | it("Cubic Bezier - Polygon", () => { 187 | const shape1 = Shapes.path("M50,20 C50,210 250,20 250,210"); 188 | const shape2 = Shapes.polygon([58, 30, 110, 27, 135, 108, 74, 145, 28, 108, 26, 53]); 189 | const expected = [ 190 | new Point2D(50.48246685800199, 35.403226945811014), 191 | new Point2D(124.27379778337522, 114.50605708221505) 192 | ]; 193 | 194 | assertIntersections(shape1, shape2, expected); 195 | }); 196 | it("Cubic Bezier - Rectangle", () => { 197 | const shape1 = Shapes.path("M50,20 C50,210 250,20 250,210"); 198 | const shape2 = Shapes.rectangle(35, 25, 80, 100); 199 | const expected = [ 200 | new Point2D(50.04756726751318, 24.99999999999999), 201 | new Point2D(115, 113.72223572223957) 202 | ]; 203 | 204 | assertIntersections(shape1, shape2, expected); 205 | }); 206 | it("Circle - Circle", () => { 207 | const shape1 = Shapes.circle(70, 80, 50); 208 | const shape2 = Shapes.circle(140, 135, 80); 209 | const expected = [ 210 | new Point2D(60.22271286332645, 129.03472908303905), 211 | new Point2D(115.32933760985969, 58.899024860178585) 212 | ]; 213 | 214 | assertIntersections(shape1, shape2, expected); 215 | }); 216 | it("Circle - Ellipse", () => { 217 | const shape1 = Shapes.circle(100, 100, 50); 218 | const shape2 = Shapes.ellipse(150, 120, 40, 50); 219 | const expected = [ 220 | new Point2D(116.445893489817, 147.21792654619213), 221 | new Point2D(140.94197183977488, 71.29886863081731) 222 | ]; 223 | 224 | assertIntersections(shape1, shape2, expected); 225 | }); 226 | it("Circle - Line", () => { 227 | const shape1 = Shapes.circle(105, 115, 80); 228 | const shape2 = Shapes.line(16, 89, 200, 15); 229 | const expected = [ 230 | new Point2D(31.84191758151497, 82.62879401612986), 231 | new Point2D(135.3745982325804, 40.990650710810044) 232 | ]; 233 | 234 | assertIntersections(shape1, shape2, expected); 235 | }); 236 | it("Circle - Polygon", () => { 237 | const shape1 = Shapes.circle(120, 75, 50); 238 | const shape2 = Shapes.polygon([48, 20, 100, 17, 125, 98, 64, 135, 18, 98, 16, 43]); 239 | const expected = [ 240 | new Point2D(93.26257976100801, 117.25056637447055), 241 | new Point2D(103.34990500791412, 27.853692225641723) 242 | ]; 243 | 244 | assertIntersections(shape1, shape2, expected); 245 | }); 246 | it("Circle - Rectangle", () => { 247 | const shape1 = Shapes.circle(90, 55, 40); 248 | const shape2 = Shapes.rectangle(30, 35, 75, 100); 249 | const expected = [ 250 | new Point2D(55.35898384862246, 35), 251 | new Point2D(105, 92.08099243547831) 252 | ]; 253 | 254 | assertIntersections(shape1, shape2, expected); 255 | }); 256 | it("Ellipse - Ellipse", () => { 257 | const shape1 = Shapes.ellipse(100, 100, 50, 40); 258 | const shape2 = Shapes.ellipse(150, 120, 40, 50); 259 | const expected = [ 260 | new Point2D(112.88633296444475, 138.6487147334415), 261 | new Point2D(136.70828576029288, 72.84118698863836) 262 | ]; 263 | 264 | assertIntersections(shape1, shape2, expected); 265 | }); 266 | it("Ellipse - Line", () => { 267 | const shape1 = Shapes.ellipse(100, 100, 50, 40); 268 | const shape2 = Shapes.line(50, 50, 150, 150); 269 | const expected = [ 270 | new Point2D(68.76524762227879, 68.76524762227879), 271 | new Point2D(131.2347523777212, 131.2347523777212) 272 | ]; 273 | 274 | assertIntersections(shape1, shape2, expected); 275 | }); 276 | it("Ellipse - Polygon", () => { 277 | const shape1 = Shapes.ellipse(100, 100, 50, 40); 278 | const shape2 = Shapes.polygon([48, 20, 100, 17, 125, 98, 64, 135, 18, 98, 16, 43]); 279 | const expected = [ 280 | new Point2D(69.47373132371456, 131.67986788561578), 281 | new Point2D(113.74741995085054, 61.54164064075573) 282 | ]; 283 | 284 | assertIntersections(shape1, shape2, expected); 285 | }); 286 | it("Ellipse - Rectangle", () => { 287 | const shape1 = Shapes.ellipse(100, 100, 50, 40); 288 | const shape2 = Shapes.rectangle(35, 25, 80, 100); 289 | const expected = [ 290 | new Point2D(60.96876251001001, 125), 291 | new Point2D(115, 61.842431943322175) 292 | ]; 293 | 294 | assertIntersections(shape1, shape2, expected); 295 | }); 296 | it("Line - Line", () => { 297 | const shape1 = Shapes.line(48, 27, 180, 116); 298 | const shape2 = Shapes.line(180, 27, 48, 116); 299 | 300 | const expected = [new Point2D(114, 71.5)]; 301 | 302 | assertIntersections(shape1, shape2, expected); 303 | }); 304 | it("Line - Polygon", () => { 305 | const shape1 = Shapes.line(20, 20, 100, 150); 306 | const shape2 = Shapes.polygon([48, 20, 100, 17, 125, 98, 64, 135, 18, 98, 16, 43]); 307 | 308 | const expected = [ 309 | new Point2D(28.586666666666666, 33.95333333333333), 310 | new Point2D(83.4931129476584, 123.1763085399449) 311 | ]; 312 | 313 | assertIntersections(shape1, shape2, expected); 314 | }); 315 | it("Line - Rectangle", () => { 316 | const shape1 = Shapes.line(16, 89, 135, 59); 317 | const shape2 = Shapes.rectangle(35, 25, 80, 100); 318 | 319 | const expected = [ 320 | new Point2D(35, 84.21008403361344), 321 | new Point2D(115, 64.0420168067227) 322 | ]; 323 | 324 | assertIntersections(shape1, shape2, expected); 325 | }); 326 | it("Path - Circle", () => { 327 | const shape1 = Shapes.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 328 | const shape2 = Shapes.circle(80, 80, 40); 329 | const expected = [ 330 | new Point2D(40.49378036925451, 73.734330802915), 331 | new Point2D(48.0811192546348, 104.10777990531592), 332 | new Point2D(61.93906996427922, 115.69037413988258), 333 | new Point2D(118.18197295998756, 68.07788018501833) 334 | ]; 335 | 336 | assertIntersections(shape1, shape2, expected); 337 | }); 338 | it("Path - Ellipse", () => { 339 | const shape1 = Shapes.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 340 | const shape2 = Shapes.ellipse(80, 80, 30, 40); 341 | const expected = [ 342 | new Point2D(65.40567989067512, 114.94772783703729), 343 | new Point2D(108.91468665042117, 69.33825981200226) 344 | ]; 345 | 346 | assertIntersections(shape1, shape2, expected); 347 | }); 348 | it("Path - Line", () => { 349 | const shape1 = Shapes.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 350 | const shape2 = Shapes.line(15, 75, 355, 140); 351 | const expected = [ 352 | new Point2D(41.469363095003175, 80.0603194152212), 353 | new Point2D(90.39284374732412, 89.41333777522375), 354 | new Point2D(131.75979837885328, 97.32172616066313), 355 | new Point2D(147.372304199773, 100.30646992054483), 356 | new Point2D(174.0025359682078, 105.3975436409809), 357 | new Point2D(224.63128285241436, 115.07656878060865), 358 | new Point2D(278.6635091553598, 125.40625910323052), 359 | new Point2D(335.91913398904643, 136.35218738025884) 360 | ]; 361 | 362 | assertIntersections(shape1, shape2, expected); 363 | }); 364 | // it("Path - Path", () => assert.fail()); 365 | it("Path - Polygon", () => { 366 | const shape1 = Shapes.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 367 | const shape2 = Shapes.polygon([100, 50, 160, 40, 185, 120, 120, 165, 70, 120, 80, 70]); 368 | const expected = [ 369 | new Point2D(71.72741550068481, 111.36292249657606), 370 | new Point2D(182.8756171982404, 113.20197503436931) 371 | ]; 372 | 373 | assertIntersections(shape1, shape2, expected); 374 | }); 375 | it("Path - Rectangle", () => { 376 | const shape1 = Shapes.path("M40,70 Q50,150 90,90 T135,130 L160,70 C180,180 280,55 280,140 S400,110 290,100"); 377 | const shape2 = Shapes.rectangle(94, 45, 80, 100); 378 | const expected = [ 379 | new Point2D(94, 84.28119297784802), 380 | new Point2D(174, 105.39445191110119) 381 | ]; 382 | 383 | assertIntersections(shape1, shape2, expected); 384 | }); 385 | it("Relative Path - Circle", () => { 386 | const shape1 = Shapes.path("M40,70 q10,80 50,20 t45,40 l25,-60 c20,110 120,-15 120,70 s120,-30 10,-40"); 387 | const shape2 = Shapes.circle(80, 80, 40); 388 | const expected = [ 389 | new Point2D(40.49378036925451, 73.734330802915), 390 | new Point2D(48.0811192546348, 104.10777990531592), 391 | new Point2D(61.93906996427922, 115.69037413988258), 392 | new Point2D(118.18197295998756, 68.07788018501833) 393 | ]; 394 | 395 | assertIntersections(shape1, shape2, expected); 396 | }); 397 | it("Relative Path - Ellipse", () => { 398 | const shape1 = Shapes.path("M40,70 q10,80 50,20 t45,40 l25,-60 c20,110 120,-15 120,70 s120,-30 10,-40"); 399 | const shape2 = Shapes.ellipse(80, 80, 30, 40); 400 | const expected = [ 401 | new Point2D(65.40567989067512, 114.94772783703729), 402 | new Point2D(108.91468665042117, 69.33825981200226) 403 | ]; 404 | 405 | assertIntersections(shape1, shape2, expected); 406 | }); 407 | it("Relative Path - Line", () => { 408 | const shape1 = Shapes.path("M40,70 q10,80 50,20 t45,40 l25,-60 c20,110 120,-15 120,70 s120,-30 10,-40"); 409 | const shape2 = Shapes.line(15, 75, 355, 140); 410 | const expected = [ 411 | new Point2D(41.469363095003175, 80.0603194152212), 412 | new Point2D(90.39284374732412, 89.41333777522375), 413 | new Point2D(131.75979837885328, 97.32172616066313), 414 | new Point2D(147.372304199773, 100.30646992054483), 415 | new Point2D(174.0025359682078, 105.3975436409809), 416 | new Point2D(224.63128285241436, 115.07656878060865), 417 | new Point2D(278.6635091553598, 125.40625910323052), 418 | new Point2D(335.91913398904643, 136.35218738025884) 419 | ]; 420 | 421 | assertIntersections(shape1, shape2, expected); 422 | }); 423 | // it("Relative Path - Relative Path", () => assert.fail()); 424 | it("Relative Path - Polygon", () => { 425 | const shape1 = Shapes.path("M40,70 q10,80 50,20 t45,40 l25,-60 c20,110 120,-15 120,70 s120,-30 10,-40"); 426 | const shape2 = Shapes.polygon([100, 50, 160, 40, 185, 120, 120, 165, 70, 120, 80, 70]); 427 | const expected = [ 428 | new Point2D(71.72741550068481, 111.36292249657606), 429 | new Point2D(182.8756171982404, 113.20197503436931) 430 | ]; 431 | 432 | assertIntersections(shape1, shape2, expected); 433 | }); 434 | it("Relative Path - Rectangle", () => { 435 | const shape1 = Shapes.path("M40,70 q10,80 50,20 t45,40 l25,-60 c20,110 120,-15 120,70 s120,-30 10,-40"); 436 | const shape2 = Shapes.rectangle(94, 45, 80, 100); 437 | const expected = [ 438 | new Point2D(94, 84.28119297784802), 439 | new Point2D(174, 105.39445191110119) 440 | ]; 441 | 442 | assertIntersections(shape1, shape2, expected); 443 | }); 444 | it("Polygon - Polygon", () => { 445 | const shape1 = Shapes.polygon([48, 20, 100, 17, 125, 98, 64, 130, 18, 98]); 446 | const shape2 = Shapes.polygon([20, 48, 17, 100, 98, 125, 130, 64, 98, 18, 43, 16]); 447 | const expected = [ 448 | new Point2D(23.965325936199722, 102.1497919556172), 449 | new Point2D(82.59485061938305, 120.24532426524168), 450 | new Point2D(88.59479553903346, 17.65799256505576), 451 | new Point2D(102.1497919556172, 23.965325936199726), 452 | new Point2D(107.29032258064517, 107.29032258064515), 453 | new Point2D(120.24532426524168, 82.59485061938304) 454 | ]; 455 | 456 | assertIntersections(shape1, shape2, expected); 457 | }); 458 | it("Polygon - Rectangle", () => { 459 | const shape1 = Shapes.polygon([48, 20, 100, 17, 125, 98, 64, 130, 18, 98]); 460 | const shape2 = Shapes.rectangle(60, 30, 100, 80); 461 | const expected = [ 462 | new Point2D(102.125, 110), 463 | new Point2D(104.01234567901234, 30) 464 | ]; 465 | 466 | assertIntersections(shape1, shape2, expected); 467 | }); 468 | it("Rectangle - Rectangle", () => { 469 | const shape1 = Shapes.rectangle(35, 25, 80, 100); 470 | const shape2 = Shapes.rectangle(55, 45, 120, 110); 471 | const expected = [ 472 | new Point2D(55, 125), 473 | new Point2D(115, 45) 474 | ]; 475 | 476 | assertIntersections(shape1, shape2, expected); 477 | }); 478 | }); 479 | -------------------------------------------------------------------------------- /test/shape-info-test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Point2D} from "kld-affine"; 3 | import ShapeInfo from "../lib/ShapeInfo.js"; 4 | import itertools from "./itertools.js"; 5 | import util from "util"; 6 | 7 | function buildPoints(count, useProperties = true) { 8 | const result = []; 9 | let x = 10; 10 | let y = 20; 11 | 12 | for (let i = 1; i <= count; i++) { 13 | if (useProperties) { 14 | let name = `p${i}`; 15 | let nameX = `${name}x`; 16 | let nameY = `${name}y`; 17 | 18 | result.push([ 19 | {[name]: new Point2D(x, y)}, 20 | {[name]: {x, y}}, 21 | {[name]: [x, y]}, 22 | {[nameX]: x, [nameY]: y} 23 | ]); 24 | } 25 | else { 26 | result.push([ 27 | [new Point2D(x, y)], 28 | [{x, y}], 29 | [[x, y]], 30 | [x, y] 31 | ]); 32 | } 33 | 34 | x += 20; 35 | y += 20; 36 | } 37 | 38 | return result; 39 | } 40 | 41 | function buildObject(...parts) { 42 | let argObject = {}; 43 | 44 | for (let part of parts) { 45 | argObject = Object.assign(argObject, part) 46 | } 47 | 48 | return argObject; 49 | } 50 | 51 | 52 | describe("Shapes API", () => { 53 | describe("Arc", () => { 54 | describe("Arc - Objects", () => { 55 | const centers = [ 56 | {center: new Point2D(10, 20)}, 57 | {center: {x: 10, y: 20}}, 58 | {center: [10, 20]}, 59 | {centerX: 10, centerY: 20}, 60 | {cx: 10, cy: 20} 61 | ]; 62 | const radiusXs = [ 63 | {radiusX: 30}, 64 | {rx: 30} 65 | ]; 66 | const radiusYs = [ 67 | {radiusY: 40}, 68 | {ry: 40} 69 | ]; 70 | const startRadians = [{startRadians: 50}]; 71 | const endRadians = [{endRadians: 60}]; 72 | const argProduct = new itertools.Product(centers, radiusXs, radiusYs, startRadians, endRadians); 73 | const expected = new ShapeInfo("Arc", [new Point2D(10, 20), 30, 40, 50, 60]); 74 | 75 | for (let args of argProduct) { 76 | const argObject = buildObject(...args); 77 | it(util.inspect(argObject, {breakLength: Infinity}), () => { 78 | const result = new ShapeInfo.arc(argObject); 79 | 80 | assert.deepStrictEqual(result, expected); 81 | }); 82 | } 83 | }); 84 | describe("Arc - Arrays", () => { 85 | const centers = [ 86 | [new Point2D(10, 20)], 87 | [{x: 10, y: 20}], 88 | [[10, 20]], 89 | [10, 20] 90 | ]; 91 | const radiusXs = [30]; 92 | const radiusYs = [40]; 93 | const startRadians = [50]; 94 | const endRadians = [60]; 95 | const argProduct = new itertools.Product(centers, radiusXs, radiusYs, startRadians, endRadians); 96 | const expected = new ShapeInfo("Arc", [new Point2D(10, 20), 30, 40, 50, 60]); 97 | 98 | for (let args of argProduct) { 99 | const flattenedArgs = args.reduce((acc, val) => acc.concat(val), []); 100 | it(util.inspect(flattenedArgs, {breakLength: Infinity}), () => { 101 | const result = new ShapeInfo.arc(...flattenedArgs); 102 | 103 | assert.deepStrictEqual(result, expected); 104 | }); 105 | } 106 | }); 107 | }); 108 | describe("Quadratic Bezier", () => { 109 | describe("Quadratic Bezier - Objects", () => { 110 | const points = buildPoints(3); 111 | const pointsProduct = new itertools.Product(...points); 112 | const expected = new ShapeInfo("Bezier2", [ 113 | new Point2D(10, 20), 114 | new Point2D(30, 40), 115 | new Point2D(50, 60) 116 | ]); 117 | 118 | for (let points of pointsProduct) { 119 | const argObject = buildObject(...points); 120 | it(util.inspect(argObject, {breakLength: Infinity}), () => { 121 | const result = new ShapeInfo.quadraticBezier(argObject); 122 | 123 | assert.deepStrictEqual(result, expected); 124 | }); 125 | } 126 | }); 127 | describe("Quadratic Bezier - Arrays", () => { 128 | const points = buildPoints(3, false); 129 | const pointsProduct = new itertools.Product(...points); 130 | const expected = new ShapeInfo("Bezier2", [ 131 | new Point2D(10, 20), 132 | new Point2D(30, 40), 133 | new Point2D(50, 60) 134 | ]); 135 | 136 | for (let points of pointsProduct) { 137 | const flattenedPoints = points.reduce((acc, val) => acc.concat(val), []); 138 | it(util.inspect(flattenedPoints, {breakLength: Infinity}), () => { 139 | const result = new ShapeInfo.quadraticBezier(...flattenedPoints); 140 | 141 | assert.deepStrictEqual(result, expected); 142 | }); 143 | } 144 | }); 145 | }); 146 | describe("Cubic Bezier", () => { 147 | describe("Cubic Bezier - Objects", () => { 148 | const points = buildPoints(4); 149 | const pointsProduct = new itertools.Product(...points); 150 | const expected = new ShapeInfo("Bezier3", [ 151 | new Point2D(10, 20), 152 | new Point2D(30, 40), 153 | new Point2D(50, 60), 154 | new Point2D(70, 80) 155 | ]); 156 | 157 | for (let points of pointsProduct) { 158 | const argObject = buildObject(...points); 159 | it(util.inspect(argObject, {breakLength: Infinity}), () => { 160 | const result = new ShapeInfo.cubicBezier(argObject); 161 | 162 | assert.deepStrictEqual(result, expected); 163 | }); 164 | } 165 | }); 166 | describe("Cubic Bezier - Arrays", () => { 167 | const points = buildPoints(4, false); 168 | const pointsProduct = new itertools.Product(...points); 169 | const expected = new ShapeInfo("Bezier3", [ 170 | new Point2D(10, 20), 171 | new Point2D(30, 40), 172 | new Point2D(50, 60), 173 | new Point2D(70, 80) 174 | ]); 175 | 176 | for (let points of pointsProduct) { 177 | const flattenedPoints = points.reduce((acc, val) => acc.concat(val), []); 178 | it(util.inspect(flattenedPoints, {breakLength: Infinity}), () => { 179 | const result = new ShapeInfo.cubicBezier(...flattenedPoints); 180 | 181 | assert.deepStrictEqual(result, expected); 182 | }); 183 | } 184 | }); 185 | }); 186 | describe("Rounded Rectangle", () => { 187 | it("Rounded Rectangle - Object 1", () => { 188 | let rr = { 189 | x: 10, y: 20, 190 | width: 30, height: 40, 191 | rx: 3, ry: 4 192 | }; 193 | try { 194 | let shape = ShapeInfo.rectangle(rr); 195 | 196 | for (let arg of shape.args) { 197 | // TODO: check segments 198 | } 199 | 200 | } 201 | catch (e) { 202 | assert.fail("Unable to parse object: " + JSON.stringify(rr)); 203 | } 204 | }); 205 | it("Rounded Rectangle - Object 2", () => { 206 | let rr = { 207 | x: 10, y: 20, 208 | w: 30, h: 40, 209 | rx: 3, ry: 4 210 | }; 211 | try { 212 | let shape = ShapeInfo.rectangle(rr); 213 | 214 | for (let arg of shape.args) { 215 | // TODO: check segments 216 | } 217 | 218 | } 219 | catch (e) { 220 | assert.fail("Unable to parse object: " + JSON.stringify(rr)); 221 | } 222 | 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/shapes-test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import {Point2D} from "kld-affine"; 3 | import Shapes from "../lib/Shapes.js"; 4 | import ShapeInfo from "../lib/ShapeInfo.js"; 5 | 6 | 7 | describe("Shapes API", () => { 8 | it("arc", () => { 9 | const result = Shapes.arc(10, 20, 30, 40, 0, 1.57); 10 | const expected = new ShapeInfo("Arc", [new Point2D(10, 20), 30, 40, 0, 1.57]); 11 | 12 | assert.deepStrictEqual(result, expected); 13 | }); 14 | it("quadratic bezier", () => { 15 | const result = Shapes.quadraticBezier(10, 20, 30, 40, 50, 60); 16 | const expected = new ShapeInfo("Bezier2", [new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60)]); 17 | 18 | assert.deepStrictEqual(result, expected); 19 | }); 20 | it("cubic bezier", () => { 21 | const result = Shapes.cubicBezier(10, 20, 30, 40, 50, 60, 70, 80); 22 | const expected = new ShapeInfo("Bezier3", [new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60), new Point2D(70, 80)]); 23 | 24 | assert.deepStrictEqual(result, expected); 25 | }); 26 | it("circle", () => { 27 | const result = Shapes.circle(10, 20, 30); 28 | const expected = new ShapeInfo("Circle", [new Point2D(10, 20), 30]); 29 | 30 | assert.deepStrictEqual(result, expected); 31 | }); 32 | it("ellipse", () => { 33 | const result = Shapes.ellipse(10, 20, 30, 40); 34 | const expected = new ShapeInfo("Ellipse", [new Point2D(10, 20), 30, 40]); 35 | 36 | assert.deepStrictEqual(result, expected); 37 | }); 38 | it("line", () => { 39 | const result = Shapes.line(10, 20, 30, 40); 40 | const expected = new ShapeInfo("Line", [new Point2D(10, 20), new Point2D(30, 40)]); 41 | 42 | assert.deepStrictEqual(result, expected); 43 | }); 44 | it("path", () => { 45 | const result = Shapes.path("M0, 10 L100, 110"); 46 | const expected = new ShapeInfo("Path", [ 47 | Shapes.line(0, 10, 100, 110) 48 | ]); 49 | 50 | assert.deepStrictEqual(result, expected); 51 | }); 52 | it("polygon", () => { 53 | const result = Shapes.polygon(10, 20, 30, 40, 50, 60); 54 | const expected = new ShapeInfo("Polygon", [ 55 | [ 56 | new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60) 57 | ] 58 | ]); 59 | 60 | assert.deepStrictEqual(result, expected); 61 | }); 62 | it("polyline", () => { 63 | const result = Shapes.polyline(10, 20, 30, 40, 50, 60); 64 | const expected = new ShapeInfo("Polyline", [ 65 | [ 66 | new Point2D(10, 20), new Point2D(30, 40), new Point2D(50, 60) 67 | ] 68 | ]); 69 | 70 | assert.deepStrictEqual(result, expected); 71 | }); 72 | it("rectangle", () => { 73 | const result = Shapes.rectangle(10, 20, 30, 40); 74 | const expected = new ShapeInfo("Rectangle", [new Point2D(10, 20), new Point2D(40, 60)]); 75 | 76 | assert.deepStrictEqual(result, expected); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /visual-test/arc-fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /visual-test/bezier2_bezier2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_bezier3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier2_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_bezier3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/bezier3_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/circle_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/circle_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/circle_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/circle_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/circle_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/ellipse_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/ellipse_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/ellipse_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/ellipse_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/index.html: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /visual-test/line_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/line_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/line_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/line_rounded_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/line_rounded_rect_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/path_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/polygon_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/polygon_rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rect_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/rel_path_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /visual-test/show_intersections.js: -------------------------------------------------------------------------------- 1 | const {SvgShapes, Intersection} = KldIntersections; 2 | 3 | /** 4 | * Intersect some shapes 5 | */ 6 | function go() { 7 | const shapes = getShapes(); 8 | 9 | if (shapes !== null) { 10 | const [shape1, shape2] = shapes; 11 | const result = Intersection.intersect(shape1, shape2); 12 | 13 | const xml = result.points.map(point => { 14 | return ``; 15 | }).join("\n"); 16 | 17 | document.getElementById("result").innerHTML = xml; 18 | } 19 | } 20 | 21 | /** 22 | * Get the shapes to intersect 23 | * 24 | * @returns {null|module:kld-intersections.ShapeInfo[]} 25 | */ 26 | function getShapes() { 27 | const container = document.getElementById("shapes"); 28 | const shapes = container.querySelectorAll("circle,ellipse,line,path,polygon,polyline,rect"); 29 | 30 | if (shapes.length >= 2) { 31 | return [ 32 | SvgShapes.element(shapes[0]), 33 | SvgShapes.element(shapes[1]) 34 | ]; 35 | } 36 | 37 | return null; 38 | } 39 | --------------------------------------------------------------------------------