├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── TestRunner.js ├── build ├── compgeo.js └── compgeo.js.map ├── es6 ├── clipper │ └── Clipper.js ├── code-builder │ ├── CodeBuilder.js │ ├── SourceWriter.js │ └── index.js ├── datastructures │ ├── Chain.js │ └── Chain.js_old ├── drawing │ ├── Drawing2D.js │ ├── Drawing2DLinearGradient.js │ ├── DrawingCommand2D.js │ └── DrawingContext2D.js ├── helpers │ ├── AffineTransform.js │ ├── Angles.js │ └── Pather.js ├── index.js ├── intersections │ ├── Intersections.js │ └── Intersections.js_old ├── iteratorers │ └── index.js ├── meta-eval │ └── index.js ├── missing-stuff │ └── index.js ├── nd-linalg │ ├── Matrix.js │ ├── Matrix2x2.js │ ├── Matrix3x3.js │ ├── Matrix4x4.js │ ├── MatrixFactory.js │ ├── Vector.js │ ├── Vector2.js │ ├── Vector3.js │ ├── Vector4.js │ └── VectorFactory.js ├── primitives │ ├── Circle.js │ ├── Curve.js │ ├── Line.js │ ├── LineSegment.js │ ├── Ray.js │ ├── Rectangle.js │ └── Triangle.js ├── shapes │ ├── Path.js │ ├── PathCollisionCollection.js │ ├── Shape.js │ └── Stroke.js └── skeleton │ ├── Skeleton.js │ ├── SkeletonCollapseEvent.js │ ├── SkeletonEdge.js │ ├── SkeletonSplitEvent.js │ ├── SkeletonVertex.js │ └── SkeletonWavefront.js ├── example ├── app.js └── testpath.js ├── package.json ├── rollup.config.js ├── test.js └── tests ├── clipper └── ClipperTests.js ├── intersections ├── IntersectionsCircleCircleTests.js ├── IntersectionsCurveCurveTests.js ├── IntersectionsLineLineTests.js ├── IntersectionsLineSegmentCircleTests.js ├── IntersectionsLineSegmentCurveTests.js ├── IntersectionsLineSegmentLineSegmentTests.js ├── IntersectionsRayLineSegmentTests.js ├── IntersectionsRayLineTests.js └── IntersectionsRayRayTests.js ├── primitives ├── CircleTests.js ├── CurveTests.js └── LineTests.js └── skeleton ├── SkeletonClosedPathTests.js └── SkeletonOpenPathTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | .idea 7 | src/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md 11 | es6/ 12 | .idea -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) for portions of project Comp-Geo are held by Anselm Eickhoff and Michael Lucas-Smith 2015 as part of project CityBound. All other copyright for project Comp-Geo are held by Michael Chang. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *This module is forked from [Citybound](http://cityboundsim.com) [Comp-Geo](https://github.com/citybound-old/comp-geo), repaired and made partially working by @mflux.* 2 | 3 | # comp-geo 4 | 5 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 6 | 7 | 2D Computational Geometry for Javascript 8 | 9 | Uses primitives with a configurable **thickness** for giving reasonable results even for inaccurate inputs. 10 | 11 | # Status of this module 12 | 13 | The module this was forked from was no longer being maintained. I've managed to get skeleton and shape / paths working. The rest may still be broken as the tests are not currently passing. Use at your own peril! 14 | 15 | *@mflux* 16 | 17 | # Tests 18 | 19 | `npm install` 20 | `npm run test` 21 | 22 | # Development and Web Example 23 | 24 | `npm install` 25 | `npm install -g budo` 26 | `npm run dev` 27 | 28 | ## Primitives 29 | 30 | * Line, Ray, LineSegment 31 | * Circle, Curve 32 | 33 | ## Shapes 34 | 35 | * Path (LineSegments + Curves) 36 | * Shape 37 | * Stroke 38 | 39 | ## Shape Operations 40 | 41 | * Clipping/Boolean Operations 42 | * Offsetting/Straight Skeleton 43 | 44 | ## Skeleton Triangulation 45 | 46 | ![Example](http://i.imgur.com/wHuq3UF.png) 47 | 48 | See example/app.js 49 | 50 | For general use from path to THREE.js roof shapes: 51 | 52 | // Path is an array of LineSegment(s) 53 | const skeleton = new CompGeo.Skeleton( path, Infinity ); 54 | const skeletonPath = new CompGeo.shapes.Path( skeleton.spokes ); 55 | const shape = new CompGeo.shapes.Shape( path.concat( skeletonPath ) ); 56 | const geometry = shape.triangulate(); 57 | 58 | -------------------------------------------------------------------------------- /TestRunner.js: -------------------------------------------------------------------------------- 1 | import Drawing2D from './es6/drawing/Drawing2D'; 2 | import fs from 'fs'; 3 | 4 | // Set to true to always draw test results, even if the test passes 5 | // This is not advised, there's some sort of exponential slow down 6 | // in console.log output with node-webkit that kills the execution 7 | // of the tests if too many graphics are output. 8 | 9 | // If you want to halt on a test failure of any kind, enable this 10 | var DEBUG_HALT_ON_ERROR = false; 11 | 12 | // If you want to see the output of a specific test even though it 13 | // is passing, set drawMe: true on the test itself. 14 | var DEBUG_DRAW = true; 15 | 16 | // Expand drawings even on passing 17 | var EXPAND_PASSED_DRAW = true; 18 | 19 | // Set to true to show green/red colouring of test results 20 | var OUTPUT_COLOURFUL = true; 21 | 22 | Object.defineProperties(exports, { 23 | "test": {value: (modules) => new TestRunner(modules)} 24 | }); 25 | 26 | function TestRunner(testModules) { 27 | this.isGraphical = !!global["window"]; 28 | this.isColourful = this.isGraphical && OUTPUT_COLOURFUL; 29 | 30 | this.failed = []; 31 | this.modules = []; 32 | this.tests = []; 33 | 34 | for (let module of testModules) { 35 | let tests = [], exclusive = []; 36 | 37 | for (let test of module.tests.slice()) { 38 | let testcase = {module: module, test: test, passed: false}; 39 | if (test.skip !== true) { 40 | test.only === true 41 | ? exclusive.push(testcase) 42 | : exclusive.length === 0 && tests.push(testcase); 43 | } 44 | } 45 | if (exclusive.length) tests = exclusive; 46 | 47 | this.modules.push({name: module.name, original: module, tests: tests}); 48 | 49 | for (let test of tests) 50 | this.tests.push(test); 51 | } 52 | 53 | this.run(); 54 | this.report(); 55 | } 56 | Object.defineProperties(TestRunner.prototype, { 57 | "run": {value: run}, 58 | "runModule": {value: runModule}, 59 | "runTest": {value: runTest}, 60 | "report": {value: report} 61 | }); 62 | 63 | function report() { 64 | var total = this.tests.length, 65 | failures = this.failed.length, 66 | passes = total - failures, 67 | style = "font-weight: bold; color: " + (failures > 0 ? "red" : "black"), 68 | description = `Tests: ${total} Passes: ${passes} Failures: ${failures}`; 69 | 70 | this.isColourful 71 | ? console.log("%c" + description, style) 72 | : console.log(description); 73 | } 74 | 75 | function run() { 76 | for (let module of this.modules) 77 | this.runModule(module); 78 | } 79 | 80 | function runModule(module) { 81 | this.isColourful 82 | ? console.group("%c" + module.name, "font-weight: bold") 83 | : console.log(module.name); 84 | 85 | for (let testcase of module.tests) 86 | this.runTest(testcase); 87 | 88 | this.isColourful && console.groupEnd("%c" + module.name); 89 | } 90 | 91 | function runTest(testcase) { 92 | captureConsole(`while testing ${testcase.test.name}...`); 93 | if (testcase.test.profile) console.profile(testcase.test.name); 94 | var startTime = new Date().getTime(), 95 | test = testcase.test, 96 | data = test.setup() || [], 97 | failed = false, 98 | reason; 99 | if (testcase.test.profile) console.profileEnd(testcase.test.name); 100 | 101 | if (DEBUG_HALT_ON_ERROR) { 102 | test.assert.apply(test, data); 103 | } else { 104 | try { 105 | test.assert.apply(test, data); 106 | } catch(e) { 107 | failed = true; 108 | reason = e; 109 | } 110 | } 111 | var stopTime = new Date().getTime(); 112 | restoreConsole(); 113 | 114 | testcase.passed = !failed; 115 | 116 | var description = `${failed ? "failed" : "passed"} ${test.name} [${stopTime-startTime}ms]`, 117 | style = `font-weight: bold; color: ${failed ? "red" : "green"}`, 118 | showdrawing = this.isGraphical && !!test.draw && (failed || DEBUG_DRAW), 119 | consoleFunction = showdrawing 120 | ? (failed || EXPAND_PASSED_DRAW) 121 | ? console.group 122 | : console.groupCollapsed 123 | : console.log; 124 | 125 | this.isColourful 126 | ? consoleFunction.call(console, "%c" + description, style) 127 | : consoleFunction.call(console, "\t" + description); 128 | 129 | if (failed) { 130 | this.failed.push(testcase); 131 | 132 | if (reason.expected && reason.actual) { 133 | console.warn("%c" + JSON.stringify(reason.expected), "color: blue"); 134 | console.warn("%c" + JSON.stringify(reason.actual), "color: red"); 135 | console.warn(reason, data); 136 | } else { 137 | console.warn(`\t\t${reason.message}`, reason, data); 138 | } 139 | } 140 | 141 | if (showdrawing) { 142 | let drawData = test.draw ? test.draw.apply(test, data) : data; 143 | if (drawData.length > 0) { 144 | let drawing = Drawing2D.canvas(drawData); 145 | console.canvas(drawing); 146 | if (!test.save) { 147 | let filepath = ["test-results", testcase.module.name, test.name + ".png"]; 148 | 149 | for (var k = 1; k < filepath.length; k++) { 150 | let path = filepath.slice(0, k).join("/"); 151 | if (!fs.existsSync(path)) 152 | fs.mkdirSync(path); 153 | } 154 | drawing.saveAsPNG(filepath.join("/")); 155 | } 156 | } 157 | } 158 | showdrawing && console.groupEnd("%c" + description); 159 | } 160 | 161 | // During testing we steal the console.log, but return it at the end of report 162 | // If a test attempts to write to the console, first we output the title of 163 | // the test so that the data is married to the test being tested, rather than 164 | // the 'passed' or 'failed' message of the previous test 165 | var capturedConsole = {}; 166 | function restoreConsole() { 167 | for (let key in capturedConsole) { 168 | console[key] = capturedConsole[key]; 169 | delete capturedConsole[key]; 170 | } 171 | } 172 | function captureConsole(title) { 173 | function intercept(mode, args) { 174 | restoreConsole(); 175 | console.log(title); 176 | return console[mode].apply(console, args); 177 | } 178 | 179 | capturedConsole = { 180 | log: console.log, 181 | info: console.info, 182 | warn: console.warn, 183 | error: console.error, 184 | canvas: console.canvas, 185 | canvasCollapsed: console.canvasCollapsed, 186 | canvasinfo: console.canvasinfo, 187 | canvaswarn: console.canvaswarn, 188 | canvaserror: console.canvaserror, 189 | group: console.group 190 | }; 191 | 192 | for (let key in capturedConsole) 193 | console[key] = function() { return intercept(key, arguments) }; 194 | } 195 | -------------------------------------------------------------------------------- /es6/code-builder/SourceWriter.js: -------------------------------------------------------------------------------- 1 | /* SourceWriter, by Michael Lucas-Smith (c) 2014 2 | 3 | Very simple tool for writing out lines of source code 4 | while keeping track of tabbing depth and auto-tabbing 5 | based on trailing { and } bracketing. 6 | 7 | */ 8 | 9 | function SourceWriter() { 10 | this.depth = 0; 11 | this.string = ""; 12 | this.last = null; 13 | this.storemode = " = "; 14 | this.newlinemode = ";"; 15 | } 16 | SourceWriter.prototype.write = function(line) { 17 | if (!line) { 18 | console.trace(); 19 | console.error("Expected a line"); 20 | } 21 | 22 | this.last = line[line.length - 1]; 23 | this.string += line; 24 | } 25 | SourceWriter.prototype.writeln = function(line) { 26 | line = line || ""; 27 | if (line.length > 0) { 28 | this.last = line[line.length - 1]; 29 | } 30 | if (this.last == "{") { 31 | this.writeTabs(); 32 | this.string += line; 33 | this.tab(); 34 | } else if (this.last == "}") { 35 | this.untab(); 36 | this.writeTabs(); 37 | this.string += line; 38 | } else if (this.last == "," || this.last == ";") { 39 | this.writeTabs(); 40 | this.string += line; 41 | } else { 42 | this.writeTabs(); 43 | this.string += line + this.newlinemode; 44 | } 45 | this.string += "\n"; 46 | this.last = null; 47 | } 48 | SourceWriter.prototype.forloop = function(indexVariable, lengthVariable) { 49 | this.writeln( 50 | "for (var " + indexVariable + " = 0; " + 51 | indexVariable + " < " + lengthVariable + "; " + 52 | indexVariable + "++) {"); 53 | } 54 | SourceWriter.prototype.store = function(destination, source) { 55 | this.writeln(destination + this.storemode + source); 56 | } 57 | SourceWriter.prototype.tab = function() { 58 | this.depth++; 59 | } 60 | SourceWriter.prototype.untab = function() { 61 | this.depth--; 62 | if (this.depth < 0) { 63 | console.trace(); 64 | console.error("Unbalanced tabs in source writer"); 65 | } 66 | } 67 | SourceWriter.prototype.writeTabs = function() { 68 | for (var i = 0; i < this.depth; i++) { 69 | this.string += "\t"; 70 | } 71 | } 72 | SourceWriter.prototype.assertBalance = function() { 73 | if (this.depth != 0) { console.error("Unbalanced tabs"); } 74 | } 75 | 76 | export default SourceWriter; -------------------------------------------------------------------------------- /es6/code-builder/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SourceWriter: require("./SourceWriter.js"), 3 | CodeBuilder: require("./CodeBuilder.js") 4 | }; -------------------------------------------------------------------------------- /es6/datastructures/Chain.js: -------------------------------------------------------------------------------- 1 | let lastElementId = 0; 2 | 3 | export default class Chain { 4 | constructor () { 5 | this.previous = this; 6 | this.next = this; 7 | this.id = lastElementId; 8 | lastElementId++; 9 | } 10 | 11 | static connect (previous, next) { 12 | previous.next = next; 13 | next.previous = previous; 14 | } 15 | 16 | static disconnect (node) { 17 | this.previous = this; 18 | this.next = this; 19 | } 20 | 21 | length () { 22 | let length = 0; 23 | for (let each of this) length++; 24 | return length; 25 | } 26 | 27 | isSingleElement () { 28 | return this.next === this && this.previous === this; 29 | } 30 | 31 | * [Symbol.iterator] () { 32 | let current = this; 33 | do { 34 | yield current; 35 | current = current.next; 36 | } while (current !== this); 37 | } 38 | } -------------------------------------------------------------------------------- /es6/datastructures/Chain.js_old: -------------------------------------------------------------------------------- 1 | /* A doubly-linked list */ 2 | 3 | var DEBUG_TRACK_CHAIN_ELEMENTS = true; 4 | var DEBUG_PRINT_CHAIN_OPERATIONS = false; 5 | 6 | var Chain; 7 | var isBroken; 8 | if (DEBUG_TRACK_CHAIN_ELEMENTS) { 9 | isBroken = isBrokenDebug; 10 | var ids = {}; 11 | Chain = function() { 12 | if (ids[this.name] === undefined) 13 | ids[this.name] = 0; 14 | this.id = ids[this.name]++; 15 | DEBUG_PRINT_CHAIN_OPERATIONS && console.log("new", this.name, this.id); 16 | 17 | this.previous = this; 18 | this.next = this; 19 | } 20 | } else { 21 | isBroken = isBrokenRuntime; 22 | Chain = function() { 23 | this.previous = this; 24 | this.next = this; 25 | } 26 | } 27 | 28 | Object.defineProperties(exports, { 29 | "createConstructor": {value: Chain}, 30 | 31 | "connect": {value: connect}, 32 | "disconnect": {value: disconnect}, 33 | "isBroken": {value: isBroken} 34 | }); 35 | 36 | Object.defineProperty(Chain.prototype, Symbol.iterator, {value: iterator}); 37 | Object.defineProperties(Chain.prototype, { 38 | "name": {value: "Chain"}, 39 | "toArray": {value: toArray}, 40 | "toComposite": {value: toComposite}, 41 | "length": {get: length}, 42 | "isSingleElement": {get: isSingleElement}, 43 | "clone": {value: clone}, 44 | "cloneElement": {value: cloneElement} 45 | }); 46 | 47 | function connect(previous, next) { 48 | previous.next = next; 49 | next.previous = previous; 50 | } 51 | 52 | function disconnect(node) { 53 | node.next = node; 54 | node.previous = node; 55 | } 56 | 57 | function isBrokenRuntime(node) { 58 | return false; 59 | } 60 | 61 | function isBrokenDebug(node) { 62 | var id = 0; 63 | var bag = [node.id]; 64 | if (!node.id) node.id = 0; 65 | var path = [node.id]; 66 | var current = node.next; 67 | var okay = false; 68 | while(bag.indexOf(current) === -1) { 69 | if (current === node) return false; 70 | bag.push(current); 71 | if (!current.id) current.id = id++; 72 | path.push(current.id); 73 | current = current.next; 74 | } 75 | path.push(current.id); 76 | console.log("broken path", path.join(" -- ")); 77 | throw "Broken Chain"; 78 | return false; 79 | } 80 | 81 | function toArray() { 82 | var array = []; 83 | for (let each of this) 84 | array.push(each); 85 | return array; 86 | } 87 | 88 | function toComposite(colour) { 89 | return { 90 | "colour": colour || "black", 91 | "visuals": this.toArray() 92 | }; 93 | } 94 | 95 | function length() { 96 | var length = 0; 97 | for (let each of this) 98 | length++; 99 | return length; 100 | } 101 | 102 | function isSingleElement() { 103 | return this.next === this && this.previous === this; 104 | } 105 | 106 | function clone() { 107 | var current = this, 108 | clone = current.cloneElement(); 109 | while(true) { 110 | current = current.next; 111 | if (current === this) break; 112 | clone = clone.append(current.cloneElement()); 113 | } 114 | return clone.next; 115 | } 116 | 117 | function cloneElement() { 118 | return new Chain(); 119 | } 120 | 121 | function *iterator() { 122 | var current = this; 123 | while(true) { 124 | yield current; 125 | current = current.next; 126 | if (current === this) break; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /es6/drawing/Drawing2D.js: -------------------------------------------------------------------------------- 1 | import AffineTransform from '../helpers/AffineTransform'; 2 | import DrawingCommand2D from './DrawingCommand2D'; 3 | import DrawingContext2D from './DrawingContext2D'; 4 | import Rectangle, {minmax as rectangleMinMax} from '../primitives/Rectangle'; 5 | import Chance from 'chance'; 6 | 7 | export default Drawing2D; 8 | 9 | function Drawing2D(options, commands) { 10 | this.options = options || {"padding": 10 / 105, "legendArea": 200, "maximumLegendFontSize": 6, "legendFontName": "serif"}; 11 | this.commands = commands || []; 12 | 13 | var elem = document.createElement("canvas"); 14 | elem.width = 2 * 800; 15 | elem.height = 2 * 800; 16 | var defaults = elem.getContext("2d"); 17 | this.style = defaults.strokeStyle; 18 | this.alpha = defaults.globalAlpha; 19 | this.operation = defaults.globalCompositeOperation; 20 | this.lineWidth = defaults.lineWidth * window.devicePixelRatio; 21 | this.lineCap = defaults.lineCap; 22 | this.lineJoin = defaults.lineJoin; 23 | this.miterLimit = defaults.miterLimit; 24 | this.transformation = AffineTransform.create(); 25 | this.font = {"name": "serif", "size": 8 * window.devicePixelRatio}; 26 | this.textAlign = defaults.textAlign; 27 | this.textBaseline = defaults.textBaseline; 28 | } 29 | Object.defineProperties(Drawing2D.prototype, { 30 | "clone": {value: clone}, 31 | "draw": {value: drawDrawing2D}, 32 | 33 | // Font setting 34 | "fontname": { 35 | get: function() {return this.font.name;}, 36 | set: function(name) {this.font = {"name": name, "size": this.font.size};}}, 37 | "fontsize": { 38 | get: function() {return this.font.size;}, 39 | set: function(size) {this.font = {"name": this.font.name, "size": size};}}, 40 | "fontstring": {get: function() {return this.fontsize + "px " + this.fontname;}}, 41 | 42 | // Complex styling 43 | "createPattern": {value: createPattern}, 44 | 45 | // Transforming 46 | "setTransform": {value: function(a, b, c, d, e, f) {this.transformation = [a, b, c, e, d, f];}}, 47 | "transform": {value: function(a, b, c, d, e, f) {this.transformation = AffineTransform.multiply(this.transformation, [a, b, c, e, d, f]);}}, 48 | "scale": {value: function(scale) {this.transformation = AffineTransform.scale(this.transformation, scale);}}, 49 | "rotate": {value: function(angle) {this.transformation = AffineTransform.rotate(this.transformation, angle);}}, 50 | "translate": {value: function(translation) {this.transformation = AffineTransform.translate(this.transformation, translation);}}, 51 | 52 | // Drawing 53 | "clip": {value: function(object) {return DrawingCommand2D.clip(this, object);}}, 54 | "fill": {value: function(object) {return DrawingCommand2D.fill(this, object);}}, 55 | "stroke": {value: function(object) {return DrawingCommand2D.stroke(this, object);}}, 56 | "dot": {value: function(object) {return DrawingCommand2D.dot(this, object);}}, 57 | "arrow": {value: function(position, direction, length) {return DrawingCommand2D.arrow(this, position, direction, length);}}, 58 | "arrowhead": {value: function(position, direction) {return DrawingCommand2D.arrowhead(this, position, direction);}}, 59 | "text": {value: function(string, position, font) {return DrawingCommand2D.text(this, string, position, font);}}, 60 | 61 | // Meta information 62 | legend: {value: function(description) {return DrawingCommand2D.legend(this, description);}} 63 | }); 64 | 65 | var Properties = [ 66 | {"property": "style", "apply": applyStyle}, 67 | {"property": "alpha", "apply": (ctx, v) => ctx.globalAlpha = v}, 68 | {"property": "operation", "apply": (ctx, v) => ctx.globalCompositeOperation = v}, 69 | {"property": "lineWidth", "apply": (ctx, v) => ctx.lineWidth = v}, 70 | {"property": "lineCap", "apply": (ctx, v) => ctx.lineCap = v}, 71 | {"property": "lineJoin", "apply": (ctx, v) => ctx.lineJoin = v}, 72 | {"property": "miterLimit", "apply": (ctx, v) => ctx.miterLimit = v}, 73 | {"property": "font", "apply": (ctx, v) => ctx.font = v.size + "px " + v.name}, 74 | {"property": "textAlign", "apply": (ctx, v) => ctx.textAlign = v}, 75 | {"property": "textBaseline", "apply": (ctx, v) => ctx.textBaseline = v} 76 | ]; 77 | 78 | function clone() { 79 | var clone = new Drawing2D(this.options, this.commands); 80 | clone.transformation = this.transformation.slice(); 81 | for (let mapping of Properties) 82 | clone[mapping.property] = this[mapping.property]; 83 | return clone; 84 | } 85 | 86 | function applyStyle(ctx, style, context) { 87 | if (style.isLinearGradient) { 88 | style.apply(ctx, context); 89 | } else { 90 | ctx.strokeStyle = style; 91 | ctx.fillStyle = style; 92 | } 93 | } 94 | 95 | function createPattern() { 96 | var context = document.createElement("canvas").getContext("2d"); 97 | return context.createPattern.apply(context, arguments); 98 | } 99 | 100 | function drawDrawing2D(canvas) { 101 | // Measure the contents of the drawing 102 | var aabb = rectangleMinMax(); 103 | for (let command of this.commands) 104 | aabb = command.measure(aabb); 105 | 106 | var context = canvas.getContext("2d"), 107 | padding = this.options.padding, 108 | width = canvas.width - this.options.legendArea, 109 | height = canvas.height, 110 | area = new Rectangle( 111 | height * padding, 112 | width * (1 - padding), 113 | height * (1 - padding), 114 | width * padding), 115 | ratio = Math.max(aabb.width, aabb.height), 116 | dimension = Math.max(area.width / ratio, area.height / ratio); 117 | 118 | function decorate() { 119 | // Draw content area border 120 | context.save(); 121 | context.strokeStyle = "#CCC"; 122 | context.setLineDash([4, 16]); 123 | context.rect(area.left, area.top, area.width, area.height); 124 | context.stroke(); 125 | context.restore(); 126 | 127 | // Draw the dimension markers 128 | var MARK_LENGTH = 10, 129 | superior = Math.max(aabb.width, aabb.height), 130 | right = area.width * (aabb.width / superior), 131 | bottom = area.height * (aabb.height / superior); 132 | context.beginPath(); 133 | context.moveTo(area.left - MARK_LENGTH, area.top); 134 | context.lineTo(area.left, area.top); 135 | context.lineTo(area.left, area.top - MARK_LENGTH); 136 | context.moveTo(area.left + right, area.top); 137 | context.lineTo(area.left + right, area.top - MARK_LENGTH); 138 | context.moveTo(area.left, area.top + bottom); 139 | context.lineTo(area.left - MARK_LENGTH, area.top + bottom); 140 | context.stroke(); 141 | 142 | // Draw the dimension values 143 | var precision2 = value => Math.floor(value * 100) / 100; 144 | var GAP = 2; 145 | context.textBaseline = "baseline"; 146 | context.textAlign = "center"; 147 | context.fillText(precision2(aabb.left), area.left, area.top - MARK_LENGTH - GAP); 148 | context.fillText(precision2(aabb.right), area.left + right, area.top - MARK_LENGTH - GAP); 149 | context.textBaseline = "middle"; 150 | context.textAlign = "right"; 151 | context.fillText(precision2(aabb.top), area.left - MARK_LENGTH - GAP, area.top); 152 | context.fillText(precision2(aabb.bottom), area.left - MARK_LENGTH - GAP, area.top + bottom); 153 | } 154 | 155 | // Decorate the canvas with aabb information 156 | context.font = this.fontstring; 157 | decorate(); 158 | 159 | // Apply the drawing to the canvas 160 | let drawingcontext = new DrawingContext2D(context, aabb, area, dimension); 161 | for (let command of this.commands) { 162 | context.save(); 163 | 164 | // Apply state to the context 165 | let state = command.state; 166 | for (let mapping of Properties) 167 | mapping.apply(context, state[mapping.property], drawingcontext); 168 | 169 | // Draw the command 170 | drawingcontext.begin(state.transformation); 171 | command.draw(drawingcontext); 172 | 173 | context.restore(); 174 | } 175 | 176 | // Add the legend 177 | var fontsize = Math.min(this.options.maximumLegendFontSize, Math.floor(area.height / Math.max(drawingcontext.legend.length, 1))), 178 | lineHeight = Math.floor(fontsize * 1.2); 179 | context.font = `${this.options.legendFontName} ${fontsize}px`; 180 | context.textAlign = "left"; 181 | context.textBaseline = "bottom"; 182 | let y = area.top + lineHeight; 183 | for (let item of drawingcontext.legend) { 184 | context.fillStyle = item["style"]; 185 | context.fillText(item.description, area.right + fontsize, y); 186 | y += lineHeight; 187 | } 188 | 189 | return canvas; 190 | } 191 | 192 | if (global['window']) { 193 | var DEFAULT_LEGEND_AREA = 100 * window.devicePixelRatio; 194 | var DEFAULT_CANVAS_EXTENT = [500 * window.devicePixelRatio + DEFAULT_LEGEND_AREA, 500 * window.devicePixelRatio]; 195 | var DRAW_BOUNDING_BOX = true; 196 | var styles = [ 197 | "black", "blue", "red", 198 | "#003300", "#330000", "#0000FF", 199 | "#666600", "#006666", "#660066", 200 | "#330", "#033", "#303"]; 201 | } 202 | 203 | Object.defineProperties(Drawing2D, { 204 | "log": {value: log}, 205 | "warn": {value: warn}, 206 | "error": {value: error}, 207 | "canvas": {value: canvas}, 208 | "draw": {value: drawComposite} 209 | }); 210 | 211 | function DrawingComposite2D() { 212 | this.style = 0; 213 | this.inComposite = false; 214 | } 215 | Object.defineProperties(DrawingComposite2D.prototype, { 216 | "draw": {value: drawComposite}, 217 | "canvas": {value: canvas}, 218 | "log": {value: log}, 219 | "apply": {value: apply}, 220 | "setStyle": {value: setStyle} 221 | }); 222 | 223 | function isComponent(object) { 224 | return isDrawable(object) 225 | || isPoint(object) 226 | || isFunction(object) 227 | || isArray(object) 228 | || isComposite(object); 229 | } 230 | 231 | function isDrawable(object) { 232 | return !!object 233 | && isFunction(object.draw); 234 | } 235 | 236 | function isPoint(object) { 237 | return ((object instanceof Array) || (object instanceof Float32Array)) 238 | && object.length === 2 239 | && (typeof object[0] === "number") 240 | && (typeof object[1] === "number"); 241 | } 242 | 243 | function isFunction(object) { 244 | return (object instanceof Function); 245 | } 246 | 247 | function isArray(object) { 248 | if (!(object instanceof Array)) 249 | return false; 250 | 251 | for (let each of object) 252 | if (!isComponent(each)) 253 | return false; 254 | 255 | return true; 256 | } 257 | 258 | function isComposite(object) { 259 | return !!object 260 | && (object.visuals instanceof Array) 261 | && isArray(object.visuals); 262 | } 263 | 264 | function drawComposite(object, canvas) { 265 | var composite = new DrawingComposite2D(), 266 | drawing = new Drawing2D(); 267 | composite.apply(drawing, object); 268 | drawing.draw(canvas); 269 | return canvas; 270 | } 271 | function canvas(object, extent) { 272 | extent = extent || DEFAULT_CANVAS_EXTENT; 273 | var canvas = document.createElement("canvas"); 274 | canvas.width = extent[0]; 275 | canvas.height = extent[1]; 276 | return this.draw(object, canvas); 277 | } 278 | function log(explanation, object, legend) { 279 | return console.canvaslog(this.canvas(object), explanation); 280 | } 281 | function warn(explanation, object) { 282 | return console.canvaswarn(this.canvas(object), explanation); 283 | } 284 | function error(explanation, object, next) { 285 | return console.canvaserror(this.canvas(object), explanation); 286 | } 287 | 288 | function apply(drawing, object) { 289 | if (isComposite(object)) { 290 | var clone = drawing; 291 | if (object.colour) { 292 | clone = drawing.clone(); 293 | clone.style = object.colour; 294 | } 295 | if (object.alpha) { 296 | if (clone === drawing) 297 | clone = drawing.clone(); 298 | clone.alpha = object.alpha; 299 | } 300 | if (object.legend) { 301 | if (clone === drawing) 302 | clone = drawing.clone(); 303 | clone.legend(object.legend); 304 | } 305 | if (clone === drawing) { 306 | clone = drawing.clone(); 307 | this.setStyle(clone); 308 | } 309 | let inComposite = this.inComposite; 310 | this.inComposite = true; 311 | this.apply(clone, object.visuals); 312 | this.inComposite = inComposite; 313 | } else { 314 | var clone = drawing.clone(); 315 | this.setStyle(clone); 316 | if (isDrawable(object)) { 317 | if (DRAW_BOUNDING_BOX && !this.inComposite && object["boundingBox"]) { 318 | var faded = drawing.clone(); 319 | faded.alpha = 0.1; 320 | faded.stroke(object.boundingBox); 321 | } 322 | object.draw(clone); 323 | } else if (isPoint(object)) { 324 | clone.dot(object); 325 | } else if (isFunction(object)) { 326 | object(clone); 327 | } else if (isArray(object)) { 328 | for (let each of object) 329 | this.apply(drawing, each); 330 | } else throw "Unknown component type"; 331 | } 332 | } 333 | 334 | function setStyle(drawing) { 335 | if (this.inComposite) return; 336 | if (this.style < styles.length) { 337 | drawing.style = styles[this.style]; 338 | this.style++; 339 | } else 340 | drawing.style = randomStyle(); 341 | } 342 | 343 | function randomStyle() { 344 | var random = new Chance() 345 | return "rgb(" + 346 | random.floating({min:0, max:255}) + "," + 347 | random.floating({min:0, max:255}) + "," + 348 | random.floating({min:0, max:255}) + ")"; 349 | } 350 | -------------------------------------------------------------------------------- /es6/drawing/Drawing2DLinearGradient.js: -------------------------------------------------------------------------------- 1 | Object.adopt(exports, { 2 | "createConstructor": Drawing2DLinearGradient 3 | }); 4 | 5 | function Drawing2DLinearGradient(start, end) { 6 | this.start = start; 7 | this.end = end; 8 | this.stops = []; 9 | } 10 | 11 | Object.adopt(Drawing2DLinearGradient.prototype, { 12 | addColorStop, apply, 13 | "isLinearGradient": true 14 | }); 15 | 16 | function addColorStop(offset, color) { 17 | this.stops.push({offset, color}); 18 | } 19 | 20 | var cache = {self: null, context: null, gradient: null}; 21 | function apply(ctx, context) { 22 | if (self === this && cache.context === context) { 23 | ctx.strokeStyle = cache.gradient; 24 | ctx.fillStyle = cache.gradient; 25 | return; 26 | } 27 | let start = context.map(this.start), 28 | end = context.map(this.end); 29 | let gradient = ctx.createLinearGradient(start[0], start[1], end[0], end[1]); 30 | for (let stop of this.stops) 31 | gradient.addColorStop(stop.offset, stop.color); 32 | ctx.strokeStyle = gradient; 33 | ctx.fillStyle = gradient; 34 | cache.context = context; 35 | cache.gradient = gradient; 36 | cache.self = this; 37 | } 38 | -------------------------------------------------------------------------------- /es6/drawing/DrawingCommand2D.js: -------------------------------------------------------------------------------- 1 | import AffineTransform from '../helpers/AffineTransform'; 2 | 3 | var newClip = (state, object) => new DrawingCommand2D(state, object, measureBoundingBox, clip), 4 | newStroke = (state, object) => new DrawingCommand2D(state, object, measureBoundingBox, stroke), 5 | newFill = (state, object) => new DrawingCommand2D(state, object, measureBoundingBox, fill), 6 | newDot = (state, object) => new DrawingCommand2D(state, object, measurePoint, dot), 7 | newText = (state, string, position, font) => new DrawingCommand2D(state, {"string": string, "position": position, "font": font}, measureText, text), 8 | newArrow = (state, position, direction, length) => new DrawingCommand2D(state, {"position": position, "direction": direction, "length": length}, measureArrow, arrow), 9 | newArrowHead = (state, position, direction) => new DrawingCommand2D(state, {"position": position, "direction": direction}, measureArrow, arrowhead), 10 | newLegend = (state, description) => new DrawingCommand2D(state, {"description": description}, measureNothing, legend); 11 | 12 | Object.defineProperties(exports, { 13 | "clip": {value: newClip}, 14 | "stroke": {value: newStroke}, 15 | "fill": {value: newFill}, 16 | "text": {value: newText}, 17 | "dot": {value: newDot}, 18 | "arrow": {value: newArrow}, 19 | "arrowhead": {value: newArrowHead}, 20 | "legend": {value: newLegend} 21 | }); 22 | 23 | function DrawingCommand2D(state, object, measureFunction, drawFunction) { 24 | state.commands.push(this); 25 | this.state = state.clone(); 26 | this.object = object; 27 | this.measure = measureFunction; 28 | this.draw = drawFunction; 29 | 30 | var aabb = this.measure(Rectangle.minmax()); 31 | if (measureFunction !== measureNothing) 32 | if (!isFinite(aabb.width) || !isFinite(aabb.height)) 33 | throw "Invalid object"; 34 | } 35 | 36 | Object.defineProperties(DrawingCommand2D.prototype, { 37 | "transformation": {get: function() {return this.state.transformation;}}, 38 | "map": {value: map} 39 | }); 40 | 41 | function map(rectangle) { 42 | return AffineTransform.mapRectangle(this.state.transformation, rectangle); 43 | } 44 | 45 | function measureBoundingBox(aabb) { 46 | return aabb.expand(this.map(this.object.boundingBox)); 47 | } 48 | function measurePoint(aabb) { 49 | return aabb.expand(this.map(Rectangle.point(this.object))); 50 | } 51 | function measureArrow(aabb) { 52 | return aabb.expand(this.map(Rectangle.point(this.object.position))); 53 | } 54 | function measureText(aabb) { 55 | // We could and would like to measure the extent of the text at this point 56 | // but we'd have to scale it to the same size as the rest of the content, 57 | // which we cannot do until we know the scale of the rest of the content 58 | // at which point we're not really contributing to the size of the content 59 | // and so we just make sure the origin of the text is included, similar to 60 | // how arrows and arrowheads work 61 | return aabb.expand(this.map(Rectangle.point(this.object.position))); 62 | } 63 | function measureNothing(aabb) { 64 | return aabb; 65 | } 66 | 67 | function clip(context) { 68 | if (!context.isClosed(this.object)) 69 | throw "Cannot clip an open object"; 70 | 71 | context.object(this.object); 72 | context.clip(); 73 | } 74 | 75 | function dot(context) { 76 | context.dot(this.object, window.devicePixelRatio * 2.5); 77 | context.fill(); 78 | } 79 | 80 | function fill(context) { 81 | if (!context.isClosed(this.object)) 82 | throw "Cannot clip an open object"; 83 | 84 | context.object(this.object); 85 | context.fill(); 86 | } 87 | 88 | function stroke(context) { 89 | context.object(this.object); 90 | context.stroke(); 91 | } 92 | 93 | function text(context) { 94 | if (this.object.font) { 95 | var substyle = this.object.font; 96 | if (!substyle.name) 97 | substyle.name = this.state.fontname; 98 | if (!substyle.size) 99 | substyle.size = this.state.fontsize; 100 | context.ctx.font = substyle.size + "px " + substyle.name; 101 | if (substyle.baseline) 102 | context.ctx.textBaseline = substyle.baseline; 103 | if (substyle.align) 104 | context.ctx.textAlign = substyle.align; 105 | } 106 | context.text(this.object.string, this.object.position); 107 | } 108 | 109 | function arrow(context) { 110 | var start = context.map(this.object.position), 111 | length = Math.min(context.canvas.width, context.canvas.height) * 0.25 * (this.object.length || 1), 112 | segment = LineSegment.project(start, this.object.direction, length); 113 | context.ctx.moveTo(segment.start[0], segment.start[1]); 114 | context.ctx.lineTo(segment.end[0], segment.end[1]); 115 | pathArrowHead(context, segment.end, this.object.direction); 116 | context.stroke(); 117 | } 118 | 119 | function arrowhead(context) { 120 | pathArrowHead(context, context.map(this.object.position), this.object.direction); 121 | context.stroke(); 122 | } 123 | 124 | function pathArrowHead(context, position, direction) { 125 | var length = Math.min(context.ctx.canvas.width, context.ctx.canvas.height) * 0.25, 126 | wingLength = length * 0.0625 / 2; 127 | 128 | var v = vec2.clone(direction); 129 | vec2.normalize(v, v); 130 | 131 | var wing1 = vec2.fromValues(-v[0] - v[1], -v[1] + v[0]); 132 | vec2.scale(wing1, wing1, wingLength); 133 | vec2.add(wing1, position, wing1); 134 | 135 | var wing2 = vec2.fromValues(-v[0] + v[1], -v[1] - v[0]); 136 | vec2.scale(wing2, wing2, wingLength); 137 | vec2.add(wing2, position, wing2); 138 | 139 | context.ctx.moveTo(wing1[0], wing1[1]); 140 | context.ctx.lineTo(position[0], position[1]); 141 | context.ctx.lineTo(wing2[0], wing2[1]); 142 | } 143 | 144 | function legend(context) { 145 | context.legend.push({ 146 | "style": this.state.style, 147 | "description": this.object.description 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /es6/drawing/DrawingContext2D.js: -------------------------------------------------------------------------------- 1 | import AffineTransform from '../helpers/AffineTransform'; 2 | import Curve from '../primitives/Curve'; 3 | import LineSegment from '../primitives/LineSegment'; 4 | import Circle from '../primitives/Circle'; 5 | import Rectangle from '../primitives/Rectangle'; 6 | import Path from '../shapes/Path'; 7 | import {angleFrom} from '../helpers/Angles'; 8 | 9 | export default createDrawingContext; 10 | 11 | function createDrawingContext(ctx, aabb, area, dimension) { 12 | var metrics = { 13 | "aabb": aabb, 14 | "area": area, 15 | "dimension": dimension, 16 | "map": [area.left - dimension * aabb.left, 17 | area.top - dimension * aabb.top]}; 18 | 19 | return new DrawingContext2D(ctx, metrics); 20 | } 21 | 22 | function DrawingContext2D(ctx, metrics) { 23 | this.ctx = ctx; 24 | this.canvas = ctx.canvas; 25 | this.transformation = AffineTransform.create(); 26 | this.metrics = metrics; 27 | this.legend = []; 28 | } 29 | 30 | Object.assign(DrawingContext2D.prototype, { 31 | // Primitive pathing 32 | begin, moveTo, lineTo, arc, 33 | 34 | // Geometric pathing 35 | line, curve, circle, dot, rectangle, path, arrowhead, object, 36 | 37 | // Transformation 38 | map, 39 | 40 | // Drawing 41 | text, 42 | "clip": function() { return this.ctx.clip(); }, 43 | "fill": function() { return this.ctx.fill(); }, 44 | "stroke": function() { return this.ctx.stroke(); }, 45 | }); 46 | 47 | var Tau = 2 * Math.PI; 48 | 49 | function begin(transformation) { 50 | this.transformation = transformation || AffineTransform.create(); 51 | this.ctx.beginPath(); 52 | } 53 | 54 | function moveTo(position) { 55 | position = this.map(position); 56 | this.ctx.moveTo(position[0], position[1]); 57 | } 58 | 59 | function lineTo(position) { 60 | position = this.map(position); 61 | this.ctx.lineTo(position[0], position[1]); 62 | } 63 | 64 | function arc(center, radius, startAngle, endAngle, isCounterClockwise) { 65 | center = this.map(center); 66 | var decomposed = AffineTransform.decompose(this.transformation), 67 | scale = Math.max(decomposed.scale[0], decomposed.scale[1]); 68 | scale *= this.metrics.dimension; 69 | this.ctx.arc(center[0], center[1], radius * scale, startAngle, endAngle, isCounterClockwise); 70 | } 71 | 72 | function line(segment) { 73 | this.moveTo(segment.start); 74 | this.lineTo(segment.end); 75 | } 76 | 77 | function curve(curve) { 78 | let startAngle = angleFrom(curve.start, curve.center), 79 | endAngle = angleFrom(curve.end, curve.center); 80 | this.arc(curve.center, curve.radius, startAngle * Tau, endAngle * Tau, curve.orientation > 0); 81 | } 82 | 83 | function circle(circle) { 84 | this.arc(circle.center, circle.radius, 0, Tau, false); 85 | } 86 | 87 | function dot(position, radius) { 88 | position = this.map(position); 89 | this.ctx.arc(position[0], position[1], radius, 0, Tau, false); 90 | } 91 | 92 | function rectangle(rectangle) { 93 | this.ctx.rect( 94 | this.map(rectangle.left), 95 | this.map(rectangle.top), 96 | this.map(rectangle.width), 97 | this.map(rectangle.height)); 98 | } 99 | 100 | function path(path) { 101 | var start = this.moveTo(path.segments[0].start); 102 | this.moveTo(path.segments[0].start); 103 | for (let segment of path.segments) { 104 | if (segment instanceof LineSegment) 105 | this.lineTo(segment.end); 106 | else if (segment instanceof Curve) 107 | this.curve(segment); 108 | else 109 | throw "Only line segments and curves supported by paths at the moment"; 110 | } 111 | } 112 | 113 | function arrowhead(position, direction) { 114 | var length = Math.min(this.ctx.canvas.width, this.ctx.canvas.height) * 0.25, 115 | wingLength = length * 0.0625 / 2; 116 | 117 | position = this.map(position); 118 | var v = vec2.clone(direction); 119 | vec2.normalize(v, v); 120 | 121 | var wing1 = vec2.fromValues(-v[0] - v[1], -v[1] + v[0]); 122 | vec2.scale(wing1, wing1, wingLength); 123 | vec2.add(wing1, position, wing1); 124 | 125 | var wing2 = vec2.fromValues(-v[0] + v[1], -v[1] - v[0]); 126 | vec2.scale(wing2, wing2, wingLength); 127 | vec2.add(wing2, position, wing2); 128 | 129 | this.ctx.moveTo(wing1[0], wing1[1]); 130 | this.ctx.lineTo(position[0], position[1]); 131 | this.ctx.lineTo(wing2[0], wing2[1]); 132 | } 133 | 134 | function object(object) { 135 | if (object instanceof Path) 136 | this.path(object); 137 | else if (object instanceof Circle) 138 | this.circle(object); 139 | else if (object instanceof Rectangle) 140 | this.rectangle(object); 141 | else if (object instanceof LineSegment) 142 | this.line(object); 143 | else if (object instanceof Curve) 144 | this.curve(object); 145 | else 146 | throw "Cannot apply a non-closed geometric as a context path"; 147 | } 148 | 149 | function map(position) { 150 | var metrics = this.metrics; 151 | position = AffineTransform.map(this.transformation, position); 152 | return [position[0] * metrics.dimension + metrics.map[0], 153 | position[1] * metrics.dimension + metrics.map[1]]; 154 | } 155 | 156 | function isClosed(object) { 157 | return (object instanceof Path) 158 | || (object instanceof Circle) 159 | || (object instanceof Rectangle); 160 | } 161 | 162 | function text(string, position) { 163 | position = this.map(position); 164 | var composition = AffineTransform.decompose(this.transformation); 165 | var matrix = AffineTransform.rotate(AffineTransform.create(), composition.rotate); 166 | this.ctx.translate(position[0], position[1]); 167 | this.ctx.transform.apply(this.ctx, matrix); 168 | this.ctx.fillText(string, 0, 0); 169 | } 170 | -------------------------------------------------------------------------------- /es6/helpers/AffineTransform.js: -------------------------------------------------------------------------------- 1 | const AffineTransform = { 2 | create, 3 | map, 4 | mapRectangle, 5 | decompose, 6 | compose, 7 | 8 | determinant, 9 | isInversible, 10 | inverse, 11 | 12 | multiply, 13 | translate, 14 | scale, 15 | rotate, 16 | shear 17 | }; 18 | 19 | module.exports = AffineTransform; 20 | 21 | function create() { 22 | return [1, 0, 0, 1, 0, 0]; 23 | } 24 | 25 | function map(matrix, position) { 26 | return [matrix[0] * position[0] + matrix[2] * position[1] + matrix[4], 27 | matrix[1] * position[0] + matrix[3] * position[1] + matrix[5]]; 28 | } 29 | 30 | function mapRectangle(matrix, rectangle) { 31 | return Rectangle.corner( 32 | map(matrix, rectangle.origin), 33 | map(matrix, rectangle.corner)); 34 | } 35 | 36 | function determinant(matrix) { 37 | return matrix[0] * matrix[3] - matrix[1] * matrix[2]; 38 | } 39 | 40 | function isInversible(matrix) { 41 | return isDeterminantInversible(determinant(matrix)); 42 | } 43 | 44 | function isDeterminantInversible(determinant) { 45 | return isFinite(determinant) && determinant !== 0; 46 | } 47 | 48 | function inverse(matrix) { 49 | let det = determinant(matrix); 50 | if (!isDeterminantInversible(det)) 51 | return identity(); 52 | 53 | return [ 54 | matrix[3] / det, 55 | -matrix[1] / det, 56 | -matrix[2] / det, 57 | matrix[0] / det, 58 | (matrix[2] * matrix[5] - matrix[3] * matrix[4]) / det, 59 | (matrix[1] * matrix[4] - matrix[0] * matrix[5]) / det]; 60 | } 61 | 62 | function multiply(a, b) { 63 | return [ 64 | b[0] * a[0] + b[1] * a[2], 65 | b[0] * a[1] + b[1] * a[3], 66 | b[2] * a[0] + b[3] * a[2], 67 | b[2] * a[1] + b[3] * a[3], 68 | b[4] * a[0] + b[5] * a[2] + a[4], 69 | b[4] * a[1] + b[5] * a[3] + a[5]]; 70 | } 71 | 72 | function translate(matrix, translation) { 73 | var transformed = matrix.slice(); 74 | transformed[4] += translation[0] * matrix[0] + translation[1] * matrix[2]; 75 | transformed[5] += translation[0] * matrix[1] + translation[1] * matrix[3]; 76 | return transformed; 77 | } 78 | 79 | function scale(matrix, scale) { 80 | var transformed = matrix.slice(); 81 | transformed[0] *= scale[0]; 82 | transformed[1] *= scale[0]; 83 | transformed[2] *= scale[1]; 84 | transformed[3] *= scale[1]; 85 | return transformed; 86 | } 87 | 88 | function rotate(matrix, radians) { 89 | var cosAngle = Math.cos(radians), 90 | sinAngle = Math.sin(radians); 91 | return multiply(matrix, [cosAngle, sinAngle, -sinAngle, cosAngle, 0, 0]); 92 | } 93 | 94 | function shear(matrix, shearing) { 95 | var transformed = matrix.slice(); 96 | transformed[0] += shearing[1] * matrix[2]; 97 | transformed[1] += shearing[1] * matrix[3]; 98 | transformed[2] += shearing[0] * matrix[0]; 99 | transformed[3] += shearing[0] * matrix[1]; 100 | return transformed; 101 | } 102 | 103 | function decompose(matrix) { 104 | // Remove scaling 105 | var scale = [Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]), 106 | Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3])]; 107 | if ((matrix[0] * matrix[3] - matrix[2] * matrix[1]) < 0) { 108 | if (matrix[0] < matrix[3]) 109 | scale[0] = -scale[0]; 110 | else 111 | scale[1] = -scale[1]; 112 | } 113 | matrix = AffineTransform.scale(matrix, [1 / scale[0], 1 / scale[1]]); 114 | 115 | // Remove rotation 116 | var angle = Math.atan2(matrix[1], matrix[0]); 117 | matrix = AffineTransform.rotate(matrix, -angle); 118 | 119 | return {"scale": scale, 120 | "rotate": angle, 121 | "translate": [matrix[4], matrix[5]], 122 | "remainder": matrix}; 123 | } 124 | 125 | function compose(composition) { 126 | return AffineTransform.scale( 127 | AffineTransform.rotate( 128 | composition.remainder, 129 | composition.rotate), 130 | composition.scale); 131 | } 132 | -------------------------------------------------------------------------------- /es6/helpers/Angles.js: -------------------------------------------------------------------------------- 1 | import vec2 from '../nd-linalg/Vector2'; 2 | 3 | export {theta, angleFrom}; 4 | 5 | function theta(v) { 6 | var angle = Math.atan2(v[1], v[0]); 7 | return (angle < 0 ? angle + (2 * Math.PI) : angle) / (2 * Math.PI); 8 | } 9 | 10 | function angleFrom(p, center) { 11 | return theta(vec2.sub(vec2(0, 0), p, center)); 12 | } -------------------------------------------------------------------------------- /es6/helpers/Pather.js: -------------------------------------------------------------------------------- 1 | import vec2 from '../nd-linalg/Vector2'; 2 | import Path from '../shapes/Path'; 3 | import LineSegment from '../primitives/LineSegment'; 4 | import Curve from '../primitives/Curve'; 5 | 6 | export default function Pather(start) { 7 | this.current = start || vec2.fromValues(0, 0); 8 | this.direction = null; 9 | this.segments = []; 10 | this.orientation = 0; 11 | } 12 | 13 | Object.defineProperties(Pather.prototype, { 14 | "name": {value: "Pather"}, 15 | "path": {get: toPath}, 16 | "isClockwise": {get: isClockwise}, 17 | "isCounterClockwise": {get: isCounterClockwise}, 18 | 19 | "append": {value: append}, 20 | "moveTo": {value: moveTo}, 21 | "lineTo": {value: lineTo}, 22 | "curveTo": {value: curveTo}, 23 | "close": {value: close}, 24 | 25 | "scale": {value: scale}, 26 | "translate": {value: translate} 27 | }); 28 | 29 | function toPath() { 30 | return new Path(this.segments, this.isClockwise); 31 | } 32 | 33 | function isClockwise() { 34 | return this.orientation > 0; 35 | } 36 | 37 | function isCounterClockwise() { 38 | return !this.isClockwise; 39 | } 40 | 41 | function append(pather) { 42 | if (!pather.segments.length) return; 43 | this.segments = this.segments.concat(pather.segments); 44 | this.current = this.segments[this.segments.length - 1].end; 45 | this.direction = pather.direction; 46 | this.orientation += pather.orientation 47 | return this; 48 | } 49 | 50 | function moveTo(position) { 51 | this.current = position; 52 | this.direction = null; 53 | return this; 54 | } 55 | 56 | function lineTo(position) { 57 | var line = new LineSegment(this.current, position); 58 | this.segments.push(line); 59 | this.current = line.end; 60 | this.direction = line.direction; 61 | updateOrientation(this); 62 | return this; 63 | } 64 | 65 | function curveTo(position, direction) { 66 | function NoDirection() { throw "Direction required if no existing segments"; } 67 | var curve = new Curve(this.current, direction || this.direction || NoDirection(), position); 68 | this.segments.push(curve); 69 | this.current = curve.end; 70 | this.direction = curve.endDirection; 71 | updateOrientation(this); 72 | return this; 73 | } 74 | 75 | function close() { 76 | if (!this.segments.length) 77 | throw "Cannot close an empty path"; 78 | if (this.path.isClosed) 79 | return this.path; 80 | 81 | var line = new LineSegment(this.current, this.segments[0].start); 82 | this.segments.push(line); 83 | this.current = line.end; 84 | this.direction = line.direction; 85 | updateOrientation(this); 86 | 87 | // console.log('after closing',this.path.isClosed ); 88 | 89 | return this.path; 90 | } 91 | 92 | function scale(scale) { 93 | return this.path.scale(scale); 94 | } 95 | 96 | function translate(offset) { 97 | return this.path.translate(offset); 98 | } 99 | 100 | function updateOrientation(pather) { 101 | var segment = pather.segments[pather.segments.length - 1], 102 | dx = segment.end[0] - segment.start[0], 103 | dy = segment.end[1] + segment.start[1]; 104 | pather.orientation += dx * dy; 105 | } 106 | -------------------------------------------------------------------------------- /es6/index.js: -------------------------------------------------------------------------------- 1 | // require("babel-core/register")({ 2 | // blacklist: ['regenerator', 'es6.forOf'], 3 | // optional: ['es7.classProperties'] 4 | // }); 5 | 6 | // import Clipper from './es6/clipper/clipper'; 7 | // import Intersections from './es6/intersections/Intersections'; 8 | // import PathCollisionCollection from './es6/shapes/PathCollisionCollection'; 9 | 10 | import * as BabelPolyfill from 'babel-polyfill'; 11 | 12 | import Skeleton from './skeleton/Skeleton'; 13 | // import Circle from './es6/primitives/Circle'; 14 | // import Curve from './es6/primitives/Curve'; 15 | // import Line from './es6/primitives/Line'; 16 | // import LineSegment from './es6/primitives/LineSegment'; 17 | // import Ray from './es6/primitives/Ray'; 18 | // import Rectangle from './es6/primitives/Rectangle'; 19 | // import Triangle from './es6/primitives/Triangle'; 20 | import Path from './shapes/Path'; 21 | import Pather from './helpers/Pather'; 22 | import Shape from './shapes/Shape'; 23 | // import Stroke from './es6/shapes/Stroke'; 24 | 25 | const out = { 26 | Skeleton, 27 | shapes: { 28 | Path, Pather, Shape 29 | } 30 | }; 31 | 32 | module.exports = out; 33 | 34 | // module.exports = { 35 | // Clipper: require('./es6/clipper/Clipper'), 36 | // Intersections: require('./es6/intersections/Intersections').default, 37 | // PathCollisionCollection: require('./es6/shapes/PathCollisionCollection'), 38 | // Skeleton: require('./es6/skeleton/Skeleton').default, 39 | // primitives: { 40 | // Circle: require('./es6/primitives/Circle'), 41 | // Curve: require('./es6/primitives/Curve'), 42 | // Line: require('./es6/primitives/Line'), 43 | // LineSegment: require('./es6/primitives/LineSegment'), 44 | // Ray: require('./es6/primitives/Ray'), 45 | // Rectangle: require('./es6/primitives/Rectangle'), 46 | // Triangle: require('./es6/primitives/Triangle') 47 | // }, 48 | // shapes: { 49 | // Path: require('./es6/shapes/Path').default, 50 | // Pather: require('./es6/helpers/Pather').default, 51 | // Shape: require('./es6/shapes/Shape').default, 52 | // Stroke: require('./es6/shapes/Stroke').default 53 | // }, 54 | // Drawing2D: require('./es6/drawing/Drawing2D') 55 | // }; -------------------------------------------------------------------------------- /es6/iteratorers/index.js: -------------------------------------------------------------------------------- 1 | class It { 2 | constructor () { 3 | this.op = id; 4 | } 5 | 6 | of (iterable) { 7 | // might return normal or iterator function 8 | return this.op(iterable); 9 | } 10 | 11 | chain (second) { 12 | this.op = compose(this.op, second); 13 | return this; 14 | } 15 | 16 | static concat (first, second) { 17 | // this only returns an iterating function, 18 | // it doesn't run it yet! 19 | return function * concatenation () { 20 | yield* first; yield* second; 21 | }; 22 | } 23 | 24 | // Iterators 25 | cycle () {return this.chain(cycler());} 26 | flatten () {return this.chain(flattener());} 27 | map (mapping) {return this.chain(mapper(mapping));} 28 | filter (predicate) {return this.chain(filterer(predicate));} 29 | take (n) {return this.chain(taker(n));} 30 | drop (n) {return this.chain(dropper(n));} 31 | windows (size) {return this.chain(windower(size));} 32 | chunks (size) {return this.chain(chunker(size));} 33 | slice (start, end) {return this.chain(compose(dropper(start - 1), taker(start - end)));} 34 | common (otherIterable) {return this.chain(commoner(otherIterable));} 35 | zip (otherIterable) {return this.chain(zipper(otherIterable));} 36 | append (otherIterable) {return this.chain(appender(otherIterable));} 37 | 38 | // Reducers 39 | reduce (step, initial) {return this.chain(reducerer(step, initial));} 40 | first () {return this.chain(firster());} 41 | find (predicate) {return this.chain(finder(predicate));} 42 | has (item) {return this.chain(haser(item));} 43 | extreme (mapping) {return this.chain(extremer(mapping));} 44 | max (mapping) {return this.chain(maxer(mapping));} 45 | min (mapping) {return this.chain(miner(mapping));} 46 | all (predicate) {return this.chain(aller(predicate));} 47 | any (predicate) {return this.chain(anyer(predicate));} 48 | unique () {return this.chain(uniquer());} 49 | empty () {return this.chain(emptyer());} 50 | } 51 | 52 | function createIteration() {return new It();} 53 | createIteration.concat = It.concat; 54 | export default createIteration; 55 | 56 | function id (iterable) {return iterable;} 57 | 58 | function compose (first, second) { 59 | return function composition (iterable) { 60 | return second(first(iterable)); 61 | }; 62 | } 63 | 64 | // Iterator-ers 65 | 66 | function cycler () { 67 | return function * cycle (iterable) { 68 | let seenValues = []; 69 | 70 | for (let value of iterable) { 71 | seenValues.push(value); 72 | yield value; 73 | } 74 | 75 | while (true) yield* seenValues; 76 | }; 77 | } 78 | 79 | function flattener () { 80 | return function * flatten (iterable) { 81 | // for some reason yield* doesn't compile in here 82 | for (let subIterable of iterable) yield* subIterable; 83 | }; 84 | } 85 | 86 | function mapper (mapping) { 87 | return function * map (iterable) {for (let value of iterable) yield mapping(value);}; 88 | } 89 | 90 | function filterer (predicate) { 91 | return function * filter (iterable) { 92 | for (let value of iterable) if (predicate(value)) yield value; 93 | }; 94 | } 95 | 96 | function taker (n) { 97 | return function * take (iterable) { 98 | let left = n; 99 | for (let value of iterable) { 100 | if (left === 0) return; 101 | left--; yield value; 102 | } 103 | }; 104 | } 105 | 106 | function dropper (n) { 107 | return function * drop (iterable) { 108 | let startIn = n; 109 | for (let value of iterable) { 110 | if (startIn === 0) yield value; 111 | else startIn--; 112 | } 113 | }; 114 | } 115 | 116 | function windower (size) { 117 | return function * windows (iterable) { 118 | let window = []; 119 | 120 | for (let value of iterable) { 121 | window.push(value); 122 | if (window.length === size) { 123 | yield window.slice(); 124 | window.shift(); 125 | } 126 | } 127 | }; 128 | } 129 | 130 | function chunker (size) { 131 | return function * chunk (iterable) { 132 | let group = []; 133 | 134 | for (let value of iterable) { 135 | group.push(value); 136 | if (group.length === size) { 137 | yield group; 138 | group = []; 139 | } 140 | } 141 | }; 142 | } 143 | 144 | function commoner (otherIterable) { 145 | let otherItems = new Set(otherIterable); 146 | return function * common (iterable) { 147 | for (let value of (new Set(iterable))) if (otherItems.has(value)) yield value; 148 | }; 149 | } 150 | 151 | function zipper (otherIterable) { 152 | otherIterable = otherIterable[Symbol.iterator](); 153 | return function * zip (iterable) { 154 | for (let value of iterable) { 155 | let other = otherIterable.next(); 156 | if (other.done) break; 157 | yield [value, other.value]; 158 | } 159 | }; 160 | } 161 | 162 | function appender (otherIterable) { 163 | return function * append (iterable) { 164 | yield* iterable; 165 | yield* otherIterable; 166 | }; 167 | } 168 | 169 | // Reducer-ers 170 | 171 | function reducerer (step, initial) { 172 | return function reduce (iterable) { 173 | let reducedValue = initial; 174 | for (let value of iterable) reducedValue = step(reducedValue, value); 175 | return reducedValue; 176 | }; 177 | } 178 | 179 | function firster () { 180 | return function first (iterable) { 181 | return iterable.next().value; 182 | }; 183 | } 184 | 185 | function finder (predicate) { 186 | return function find (iterable) { 187 | for (let value of iterable) if (predicate(value)) return value; 188 | }; 189 | } 190 | 191 | function haser (item) { 192 | return function has (iterable) { 193 | for (let value of iterable) if (value === item) return true; 194 | }; 195 | } 196 | 197 | function extremer (mapping) { 198 | return function extreme (iterable) { 199 | let bestMetric; 200 | let best; 201 | let rest = []; 202 | 203 | for (let value of iterable) { 204 | let metric = mapping(value); 205 | if (best === undefined || metric > bestMetric) { 206 | if (best !== undefined) rest.push(best); 207 | best = value; 208 | bestMetric = metric; 209 | } else rest.push(value); 210 | } 211 | 212 | return {best, bestMetric, rest}; 213 | }; 214 | } 215 | 216 | function maxer (mapping) { 217 | return function max (iterable) { 218 | let {best, bestMetric: maximum, rest} = extremer(mapping)(iterable); 219 | return {best, max: maximum, rest}; 220 | }; 221 | } 222 | 223 | function miner (mapping) { 224 | let negativeMapping = (i) => - mapping(i); 225 | return function min (iterable) { 226 | let {best, bestMetric: negativeMin, rest} = extremer(negativeMapping)(iterable); 227 | return {best, min: -negativeMin, rest}; 228 | }; 229 | } 230 | 231 | function aller (predicate) { 232 | return function all (iterable) { 233 | for (let value of iterable) if (!predicate(value)) return false; 234 | return true; 235 | }; 236 | } 237 | 238 | function anyer (predicate) { 239 | return function any (iterable) { 240 | for (let value of iterable) if (predicate(value)) return true; 241 | return false; 242 | }; 243 | } 244 | 245 | function uniquer () { 246 | return function unique (iterable) { 247 | 248 | //return new Set(iterable); 249 | let s = new Set(); 250 | for (let value of iterable) s.add(value); 251 | return s; 252 | }; 253 | } 254 | 255 | function emptyer () { 256 | return function empty (iterable) { 257 | return iterable.next().done; 258 | }; 259 | } -------------------------------------------------------------------------------- /es6/meta-eval/index.js: -------------------------------------------------------------------------------- 1 | import {transform} from "babel-core"; 2 | import babelParser from "babel-core/lib/helpers/parse.js"; 3 | 4 | export default function metaEval (source, environment, alias, filename, sourceUrlBase, options) { 5 | 6 | filename = filename || alias; 7 | options = options || {}; 8 | 9 | if (options.transpile) { 10 | try { 11 | source = transform(source, { 12 | blacklist: ["regenerator", "es6.tailCall"], 13 | loose: ["es6.forOf"], 14 | optional: ["es7.classProperties"], 15 | filename: filename 16 | }).code; 17 | } catch (e) { 18 | if (e instanceof SyntaxError) { 19 | logSyntaxError(source, e); 20 | return; 21 | } else throw e; 22 | } 23 | } 24 | 25 | alias = alias || "anonymousMetaProgram" + createKuid(); 26 | source = source + "\n//# sourceURL=" + sourceUrlBase + filename; 27 | 28 | let executable = Object.create(Function.prototype); 29 | let wrapperSource = 30 | ` // this function evaluates ${alias} 31 | 32 | // catch syntax errors early 33 | try {__parse(source);} 34 | catch(e) { 35 | if (e instanceof SyntaxError) { 36 | __logSyntaxError(source, e); 37 | return; 38 | } else throw e; 39 | } 40 | 41 | eval(source); 42 | `; 43 | 44 | if (options.wrapperFileListed) wrapperSource += `//# sourceURL=${sourceUrlBase + "metaEval/" + alias}`; 45 | 46 | environment.__logSyntaxError = logSyntaxError; 47 | environment.__parse = babelParser; 48 | 49 | let wrapperParameters = ["source"].concat(Object.keys(environment)).concat([wrapperSource]); 50 | let wrapperFunction = Function.prototype.constructor.apply(executable, wrapperParameters); 51 | 52 | let environmentValues = Object.keys(environment).map(function (k) {return environment[k];}); 53 | let wrapperArguments = [source].concat(environmentValues); 54 | 55 | wrapperFunction.apply(executable, wrapperArguments); 56 | 57 | return environment; 58 | }; 59 | 60 | function logSyntaxError (source, e) { 61 | if (e.loc) { 62 | console.error( 63 | source.split(/\n/).slice(0, e.loc.line).map((l, i) => i + ":\t" + l).join("\n") + 64 | "\n" + (e.loc.line + "").replace(/./g, "!") + "!\t" + source.split(/\n/)[e.loc.line - 1].slice(0, e.loc.column) 65 | .replace(/[^\t]/g, "-") + "^" 66 | ); 67 | } 68 | console.error(e); 69 | } 70 | 71 | // kinda unique id 72 | function createKuid () { 73 | return 'xxxxxxxx'.replace(/[xy]/g, function(c) { 74 | var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); 75 | return v.toString(16); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /es6/missing-stuff/index.js: -------------------------------------------------------------------------------- 1 | import Vector2 from '../nd-linalg/Vector2'; 2 | 3 | const Mathmin = Math.min, 4 | Mathmax = Math.max, 5 | Mathabs = Math.abs; 6 | 7 | export const ROUGHLY_EPSILON = 1e-4; 8 | 9 | export function clamp(min, number, max) { 10 | return Mathmin(Mathmax(number, min), max); 11 | } 12 | 13 | export function between(min, number, max) { 14 | return min <= number && number <= max; 15 | } 16 | 17 | export function withinRange(target, number, range) { 18 | $Number.between(target - range, number, target + range); 19 | } 20 | 21 | export function betweenModulo(start, number, end, period) { 22 | var numberModulo = (number % period + period) % period; 23 | var startModulo = (start % period + period) % period; 24 | var endModulo = (end % period + period) % period; 25 | if (start <= end) return numberModulo.between(startModulo, end === period ? period : endModulo); 26 | else return numberModulo > startModulo || numberModulo < endModulo; 27 | } 28 | 29 | export function roughlyEqual(number, other, epsilon) { 30 | return Mathabs(number - other) <= (epsilon || ROUGHLY_EPSILON); 31 | } 32 | 33 | export function roughlyEqualVec2( a, b, epsilon ){ 34 | return ( Math.abs(a[0] - b[0]) <= ( epsilon || ROUGHLY_EPSILON) ) 35 | && ( Math.abs(a[1] - b[1]) <= ( epsilon || ROUGHLY_EPSILON) ); 36 | } 37 | 38 | export function assertRoughlyEqual(number, other, epsilon ) { 39 | return assert( Mathabs(number - other) <= ( epsilon || ROUGHLY_EPSILON) ); 40 | } 41 | 42 | export function assertRoughlyEqualVec2(vec, other, epsilon ) { 43 | return assert( ( Math.abs(vec[0] - other[0]) <= ( epsilon || ROUGHLY_EPSILON) ) 44 | && ( Math.abs(vec[1] - other[1]) <= ( epsilon || ROUGHLY_EPSILON) ) ); 45 | } 46 | 47 | export function roughlyBetween(min, number, max, epsilon) { 48 | return (min < number || isRoughly( min, number, epsilon)) 49 | && (number < max || isRoughly( max, number, epsilon)); 50 | } 51 | 52 | export function assert(condition, message) { 53 | if (!condition) { 54 | throw message || "Assertion failed"; 55 | } 56 | } 57 | 58 | export function mapPush( map, key, value ){ 59 | return map.set( key, value ); 60 | } 61 | 62 | function assert(condition, message) { 63 | if (!condition) { 64 | throw message || "Assertion failed"; 65 | } 66 | } 67 | 68 | function isRoughly( a, b, epsilon ){ 69 | return Math.abs( a-b ) <= ( epsilon || ROUGHLY_EPSILON ); 70 | } -------------------------------------------------------------------------------- /es6/nd-linalg/Matrix.js: -------------------------------------------------------------------------------- 1 | import VectorFactory from './VectorFactory'; 2 | // var VectorFactory = require("./VectorFactory"); 3 | var MatrixFactory = require("./MatrixFactory"); 4 | import SourceWriter from '../code-builder/SourceWriter'; 5 | import CodeBuilder from '../code-builder/CodeBuilder'; 6 | // var {SourceWriter, CodeBuilder} = require("../code-builder"); 7 | 8 | export function create(dimensions, destination) { 9 | var length = dimensions * dimensions; 10 | var createArray = new Array(length); 11 | for (var i = 0; i < length; i++) createArray[i] = 0.0; 12 | for (var i = 0; i < dimensions; i++) createArray[i * dimensions + i] = 1.0; 13 | var createSource = ` return [${createArray.join(", ")}];`; 14 | var createFunction = CodeBuilder.compile("create", [], createSource, "Matrix" + dimensions + "x" + dimensions); 15 | 16 | var cloneSource = ` return out.slice();`; 17 | var cloneFunction = CodeBuilder.compile("clone", ["out"], cloneSource, "Matrix" + dimensions + "x" + dimensions); 18 | 19 | var scaleArray = createArray.slice(); 20 | for (var i = 0; i < dimensions; i++) createArray[i * dimensions + i] = "s"; 21 | var scaleSource = ` return [${scaleArray.join(", ")}];`; 22 | var scaleFunction = CodeBuilder.compile("scale", ["s"], scaleSource, "Matrix" + dimensions + "x" + dimensions); 23 | 24 | Object.defineProperties(destination, { 25 | "create": {value: createFunction}, 26 | "clone": {value: cloneFunction}, 27 | "copy": {value: copy(dimensions)}, 28 | "scale": {value: scaleFunction}, 29 | "multiply": {value: multiply(dimensions)}, 30 | "map": {value: map(dimensions)}, 31 | "determinant": {value: determinant(dimensions)}, 32 | "invert": {value: invert(dimensions)}, 33 | "adjoint": {value: adjoint(dimensions)}, 34 | "transpose": {value: transpose(dimensions)} 35 | }); 36 | } 37 | 38 | function copy(dimensions) { 39 | var source = new SourceWriter(), 40 | cb = new CodeBuilder(), 41 | out = cb.matrix(dimensions, "out"), 42 | m = cb.matrix(dimensions, "m"); 43 | cb.assign(out, m); 44 | source.tab(); 45 | cb.write(source, [cb.output(out)]); 46 | source.untab(); 47 | return CodeBuilder.compile("copy", ["out", "m"], source.string, "Matrix" + dimensions + "x" + dimensions); 48 | } 49 | 50 | function multiply(dimensions) { 51 | var source = new SourceWriter(), 52 | cb = new CodeBuilder(), 53 | out = cb.matrix(dimensions, "out"), 54 | m = cb.matrix(dimensions, "m"), 55 | n = cb.matrix(dimensions, "n"), 56 | multiplied = MatrixFactory.getMatrixMultiply(cb, m, n); 57 | 58 | cb.assign(out, multiplied); 59 | source.tab(); 60 | cb.write(source, [cb.output(out)]); 61 | source.untab(); 62 | return CodeBuilder.compile("multiply", ["out", "m", "n"], source.string, "Matrix" + dimensions + "x" + dimensions); 63 | } 64 | 65 | function map(dimensions) { 66 | var source = new SourceWriter(), 67 | cb = new CodeBuilder(), 68 | out = cb.vector(dimensions, "out"), 69 | m = cb.matrix(dimensions, "m"), 70 | v = cb.vector(dimensions, "v"), 71 | multiplied = MatrixFactory.getVectorMultiply(cb, m, v); 72 | 73 | cb.assign(out, multiplied); 74 | source.tab(); 75 | cb.write(source, [cb.output(out)]); 76 | source.untab(); 77 | return CodeBuilder.compile("map", ["out", "m", "v"], source.string, "Matrix" + dimensions + "x" + dimensions); 78 | } 79 | 80 | function determinant(dimensions) { 81 | var cb = new CodeBuilder(), 82 | m = cb.matrix(dimensions, "m"), 83 | det = MatrixFactory.getDeterminant(cb, m), 84 | source = new SourceWriter(); 85 | source.tab(); 86 | cb.write(source, [cb.output(det)]); 87 | source.untab(); 88 | return CodeBuilder.compile("determinant", ["m"], source.string, "Matrix" + dimensions + "x" + dimensions); 89 | } 90 | 91 | function invert(dimensions) { 92 | var source = new SourceWriter(), 93 | cb = new CodeBuilder(), 94 | out = cb.matrix(dimensions, "out"), 95 | m = cb.matrix(dimensions, "m"), 96 | invert = MatrixFactory.getInvertMatrix(cb, m); 97 | cb.assign(out, invert); 98 | source.tab(); 99 | cb.write(source, [cb.output(out)]); 100 | source.untab(); 101 | return CodeBuilder.compile("invert", ["out", "m"], source.string, "Matrix" + dimensions + "x" + dimensions); 102 | } 103 | 104 | function adjoint(dimensions) { 105 | var source = new SourceWriter(); 106 | 107 | source.tab(); 108 | if (dimensions === 2) { 109 | source.writeln("if (out === m) {"); 110 | var cb = new CodeBuilder(), 111 | out = cb.matrix(dimensions, "out"); 112 | adjugate = MatrixFactory.getAdjointMatrix(cb, out); 113 | cb.assign(out, adjugate); 114 | cb.write(source, [cb.output(out)]); 115 | source.writeln("}"); 116 | } 117 | var cb = new CodeBuilder(), 118 | out = cb.matrix(dimensions, "out"), 119 | adjugate = cb.matrix(dimensions, "m"); 120 | adjugate = MatrixFactory.getAdjointMatrix(cb, adjugate); 121 | cb.assign(out, adjugate); 122 | cb.write(source, [cb.output(out)]); 123 | source.untab(); 124 | return CodeBuilder.compile("adjoint", ["out", "m"], source.string, "Matrix" + dimensions + "x" + dimensions); 125 | } 126 | 127 | function transpose(dimensions) { 128 | var source = new SourceWriter(); 129 | source.tab(); 130 | source.writeln(`if (out === m) {`); 131 | { 132 | var cb = new CodeBuilder(); 133 | var out = cb.matrix(dimensions, "out"); 134 | cb.assign(out, MatrixFactory.getTransposeMatrix(cb, out)); 135 | cb.write(source, [cb.output(out)]); 136 | } 137 | source.writeln("}"); 138 | { 139 | var cb = new CodeBuilder(); 140 | var out = cb.matrix(dimensions, "out"); 141 | var m = cb.matrix(dimensions, "m"); 142 | cb.assign(out, MatrixFactory.getTransposeMatrix(cb, m)); 143 | cb.write(source, [cb.output(out)]); 144 | } 145 | source.untab(); 146 | 147 | return CodeBuilder.compile("tranpose", ["out", "m"], source.string, "Matrix" + dimensions + "x" + dimensions); 148 | } -------------------------------------------------------------------------------- /es6/nd-linalg/Matrix2x2.js: -------------------------------------------------------------------------------- 1 | var Matrix2x2 = exports; 2 | 3 | require("./Matrix").create(2, Matrix2x2); 4 | Object.defineProperties(Matrix2x2, { 5 | "rotation": {value: rotation}, 6 | "shear": {value: shear} 7 | }); 8 | 9 | function rotation(angle) { 10 | let c = Math.cos(angle), 11 | s = Math.sin(angle) 12 | return [c, -s, 13 | s, c]; 14 | } 15 | 16 | function shear(k) { 17 | return [1, 0, 18 | k, 1]; 19 | } -------------------------------------------------------------------------------- /es6/nd-linalg/Matrix3x3.js: -------------------------------------------------------------------------------- 1 | var Matrix3x3 = exports; 2 | 3 | require("./Matrix").create(3, Matrix3x3); 4 | Object.defineProperties(Matrix3x3, { 5 | "rotation": {value: rotation}, 6 | "translation": {value: translation} 7 | }); 8 | 9 | // http://en.wikipedia.org/wiki/Transformation_matrix 10 | function rotation(angle, axis) { 11 | vec3.normalize(axis, axis); 12 | let c = Math.cos(angle), c1 = 1 - c, 13 | s = Math.sin(angle), s1 = 1 - s, 14 | t = vec3.scale(vec3(0, 0, 0), axis, 1 - c); 15 | return [axis[0] * t[0] + c, 16 | axis[1] * t[0] - s * axis[2], 17 | axis[2] * t[0] + s * axis[1], 18 | 19 | axis[0] * t[1] + c * axis[2], 20 | axis[1] * t[1] + c, 21 | axis[2] * t[1] - s * axis[0], 22 | 23 | axis[0] * t[2] - s * axis[1], 24 | axis[1] * t[2] + s * axis[0], 25 | axis[2] * t[2] + c]; 26 | } 27 | 28 | function translation(t) { 29 | return [1, 0, t[0], 30 | 0, 1, t[1], 31 | 0, 0, 1]; 32 | } 33 | -------------------------------------------------------------------------------- /es6/nd-linalg/Matrix4x4.js: -------------------------------------------------------------------------------- 1 | var Matrix4x4 = exports; 2 | 3 | require("./Matrix").create(4, Matrix4x4); 4 | Object.defineProperties(Matrix4x4, { 5 | "rotation": {value: rotation}, 6 | "translation": {value: translation}, 7 | "frustrum": {value: frustrum}, 8 | "orthogonal": {value: orthogonal}, 9 | "perspective": {value: perspective}, 10 | "lookAt": {value: lookAt} 11 | }); 12 | 13 | function rotation(angle, axis) { 14 | vec3.normalize(axis, axis); 15 | let c = cos(angle), 16 | s = sin(angle), 17 | t = vec3.scale(vec3(0, 0, 0), axis, 1 - c); 18 | return [axis[0] * t[0] + c, 19 | axis[1] * t[0] - s * axis[2], 20 | axis[2] * t[0] + s * axis[1], 21 | 0, 22 | 23 | axis[0] * t[1] + c * axis[2], 24 | axis[1] * t[1] + c, 25 | axis[2] * t[1] - s * axis[0], 26 | 0, 27 | 28 | axis[0] * t[2] - s * axis[1], 29 | axis[1] * t[2] + s * axis[0], 30 | axis[2] * t[2] + c, 31 | 0, 32 | 33 | 0, 0, 0, 1]; 34 | } 35 | 36 | function translation(t) { 37 | return [1, 0, 0, t[0], 38 | 0, 1, 0, t[1], 39 | 0, 0, 1, t[2], 40 | 0, 0, 0, 1]; 41 | } 42 | 43 | function frustrum(l, r, t, b, n, f) { 44 | let w = r - l, 45 | h = t - b, 46 | d = f - n; 47 | return [2 * n / w, 0, (r + l) / w, 0, 48 | 0, 2 * n / h, (t + b) / h, 0, 49 | 0, 0, -(f + n) / d, -2 * f * n / d, 50 | 0, 0, -1, 0]; 51 | } 52 | 53 | function orthogonal(l, r, t, b, n, f) { 54 | return [2 / (r - l), 0, 0, -((r + l) / (r - l)), 55 | 0, 2 / (t - b), 0, -((t + b) / (t - b)), 56 | 0, 0, -2 / (f - n), ((f + n) / (f - n)), 57 | 0, 0, 0, 1]; 58 | } 59 | 60 | function perspective(fovy, aspect, n, f) { 61 | var f = 1 / Math.tan(fovy / 2); 62 | return [f / aspect, 0, 0, 0, 63 | 0, f, 0, 0, 64 | 0, 0, (n + f) / (n - f), 2 * n * f / (n - f), 65 | 0, 0, -1, 0]; 66 | } 67 | 68 | function lookAt(from, to, up) { 69 | var f = vec3.subtract(vec3(0, 0, 0), to, from); 70 | vec3.normalize(f, f); 71 | var s = vec3.cross(vec3(0, 0, 0), f, up); 72 | vec3.normalize(s, s); 73 | var u = vec3.cross(vec3(0, 0, 0), s, f); 74 | f = -f; 75 | var look = [s[0], s[1], s[2], 0, 76 | u[0], u[1], u[2], 0, 77 | f[0], f[1], f[2], 0, 78 | 0, 0, 0, 1]; 79 | var negfrom = vec3.negate(vec3.create(0, 0, 0), from); 80 | return Matrix4x4.multiply(Matrix4x4.create(), look, translation(negfrom)); 81 | } 82 | -------------------------------------------------------------------------------- /es6/nd-linalg/MatrixFactory.js: -------------------------------------------------------------------------------- 1 | var max = Math.max, 2 | sqrt = Math.sqrt; 3 | 4 | import VectorFactory from "./VectorFactory.js"; 5 | 6 | export function getRow(cb, matrix, ri) { 7 | var dimensions = sqrt(matrix.length); 8 | var vector = []; 9 | for (var i = 0; i < dimensions; i++) 10 | vector[i] = matrix[ri * dimensions + i]; 11 | return cb.vector(vector); 12 | } 13 | 14 | export function getColumn(cb, matrix, ci) { 15 | var dimensions = sqrt(matrix.length); 16 | var vector = []; 17 | for (var i = 0; i < dimensions; i++) 18 | vector[i] = matrix[i * dimensions + ci]; 19 | return cb.vector(vector); 20 | } 21 | 22 | export function removeCell(cb, matrix, ci, ri) { 23 | var dimensions = sqrt(matrix.length); 24 | var m = new Array((dimensions - 1) * (dimensions - 1)); 25 | var i = 0; 26 | for (let r = 0; r < dimensions; r++) { 27 | if (r === ri) continue; 28 | for (let c = 0; c < dimensions; c++) { 29 | if (c === ci) continue; 30 | m[i++] = matrix[r * dimensions + c]; 31 | } 32 | } 33 | return cb.matrix(m); 34 | } 35 | 36 | export function getTransposeMatrix(cb, matrix) { 37 | var dimensions = sqrt(matrix.length); 38 | var m = new Array(dimensions * dimensions); 39 | for (let r = 0; r < dimensions; r++) 40 | for (let c = 0; c < dimensions; c++) 41 | m[c * dimensions + r] = matrix[r * dimensions + c]; 42 | return cb.matrix(m); 43 | } 44 | 45 | export function getDeterminant(cb, matrix, flipSigning) { 46 | var dimensions = sqrt(matrix.length); 47 | if (dimensions === 2) { 48 | // matrix[0] * matrix[3] - matrix[1] * matrix[2] 49 | let a = cb.apply("*", matrix[0], matrix[3]), 50 | b = cb.apply("*", matrix[1], matrix[2]); 51 | return flipSigning 52 | ? cb.apply("-", b, a) 53 | : cb.apply("-", a, b); 54 | } else { 55 | var cofactors = new Array(dimensions); 56 | var negativeColumn = flipSigning ? 0 : 1; 57 | for (let i = 0; i < dimensions; i++) { 58 | let determinant = cb.apply("*", matrix[i], getDeterminant(cb, removeCell(cb, matrix, i, 0))); 59 | if (i % 2 == negativeColumn) determinant = cb.apply("[[negate]]", determinant); 60 | cofactors[i] = determinant; 61 | } 62 | return cb.apply.apply(cb, ["+"].concat(cofactors)); 63 | } 64 | } 65 | 66 | export function getDeterminantSigningMatrix(dimensions) { 67 | let m = new Array(dimensions * dimensions); 68 | for (let r = 0; r < dimensions; r++) { 69 | let sign = r % 2 ? 1 : -1; 70 | for (let c = 0; c < dimensions; c++) 71 | m[r * dimensions + c] = (sign *= -1); 72 | } 73 | return m; 74 | } 75 | 76 | export function getCofactorMatrix(cb, matrix) { 77 | var dimensions = sqrt(matrix.length); 78 | if (dimensions === 2) { 79 | return [ 80 | matrix[3], cb.apply("[[negate]]", matrix[2]), 81 | cb.apply("[[negate]]", matrix[1]), matrix[0]]; 82 | } 83 | 84 | var cofactorMatrix = new Array(matrix.length), 85 | signings = getDeterminantSigningMatrix(dimensions); 86 | for (let r = 0; r < dimensions; r++) { 87 | for (let c = 0; c < dimensions; c++) { 88 | let negative = signings[r * dimensions + c] === -1, 89 | det = getDeterminant(cb, removeCell(cb, matrix, c, r), negative); 90 | cofactorMatrix[r * dimensions + c] = det; 91 | } 92 | } 93 | return cb.matrix(cofactorMatrix); 94 | } 95 | 96 | export function getAdjointMatrix(cb, matrix) { 97 | var cofactorMatrix = getCofactorMatrix(cb, matrix); 98 | return getTransposeMatrix(cb, cofactorMatrix); 99 | } 100 | 101 | export function getInvertMatrix(cb, matrix) { 102 | var determinant = getDeterminant(cb, matrix), 103 | zero = cb.scalar("0"), 104 | condition = [determinant, ">", zero], 105 | invdet = cb.phi(condition, cb.apply("1.0 /", determinant), zero, "invdet"), 106 | adjointMatrix = getAdjointMatrix(cb, matrix); 107 | return cb.map("*", adjointMatrix, invdet); 108 | } 109 | 110 | export function getMatrixMultiply(cb, a, b) { 111 | var dimensions = sqrt(a.length), 112 | output = new Array(dimensions * dimensions); 113 | for (let r = 0; r < dimensions; r++) { 114 | let row = getRow(cb, a, r); 115 | for (let c = 0; c < dimensions; c++) { 116 | let column = getColumn(cb, b, c); 117 | output[r * dimensions + c] = VectorFactory.dot(cb, row, column); 118 | } 119 | } 120 | return cb.matrix(output); 121 | } 122 | 123 | export function getVectorMultiply(cb, a, v) { 124 | var dimensions = v.length, 125 | output = new Array(dimensions); 126 | for (let r = 0; r < dimensions; r++) { 127 | let row = getRow(cb, a, r); 128 | output[r] = VectorFactory.dot(cb, row, v); 129 | } 130 | return cb.vector(output); 131 | } 132 | 133 | export function prettyprint(matrix) { 134 | var dimensions = sqrt(matrix.length); 135 | var rows = []; 136 | var strings = matrix.map(each => each.toString()); 137 | var widths = strings.map(each => each.length); 138 | var maxwidth = 0; 139 | for (let each of widths) 140 | maxwidth = max(maxwidth, each); 141 | var padding = (new Array(maxwidth + 1)).join(" "); 142 | strings = strings.map(each => padding.slice(each.length) + each); 143 | for (let i = 0; i < matrix.length; i += dimensions) 144 | rows.push(strings.slice(i, i + dimensions).join(" ")); 145 | return rows.join("\n"); 146 | } 147 | -------------------------------------------------------------------------------- /es6/nd-linalg/Vector.js: -------------------------------------------------------------------------------- 1 | import VectorFactory from './VectorFactory'; 2 | import SourceWriter from '../code-builder/SourceWriter'; 3 | import CodeBuilder from '../code-builder/CodeBuilder'; 4 | 5 | var aliases = [ 6 | "x", "y", "z", "w", 7 | "q", "r", "s", "t", 8 | "u", "v", 9 | "i", "j", "k", "l", 10 | "m", "n", "o", "p"]; 11 | 12 | export function create(dimensions, destination) { 13 | var createArray = new Array(dimensions); 14 | for (var i = 0; i < dimensions; i++) createArray[i] = 0.0; 15 | var createSource = ` return [${createArray.join(", ")}];`; 16 | var createFunction = CodeBuilder.compile("create", [], createSource, "Vector" + dimensions); 17 | 18 | var cloneSource = ` return out.slice();`; 19 | var cloneFunction = CodeBuilder.compile("clone", ["out"], cloneSource, "Vector" + dimensions); 20 | 21 | var decomposed = aliases.slice(0, dimensions); 22 | var fromSource = ` return [${decomposed.join(", ")}];`; 23 | var fromFunction = CodeBuilder.compile("fromSource", decomposed, fromSource, "Vector" + dimensions); 24 | var averageFunction = function (out, iterable) { 25 | destination.set(out, 0, 0); 26 | let n = 0; 27 | for (let vector of iterable) { 28 | destination.add(out, out, vector); 29 | n++; 30 | } 31 | return destination.scale(out, out, 1 / n); 32 | }; 33 | 34 | var properties = { 35 | "create": {value: createFunction}, 36 | "clone": {value: cloneFunction}, 37 | "fromValues": {value: fromFunction}, 38 | "average": {value: averageFunction} 39 | }; 40 | 41 | Object.defineProperties(destination, properties); 42 | //Object.defineProperties(fromFunction, properties); 43 | 44 | var operations = { 45 | "copy": 46 | {"function": (cb, a) => a, 47 | "arguments": ["out", "a"], 48 | "types": ["vector", "vector"]}, 49 | "set": 50 | {"function": VectorFactory.set(aliases.slice(0, dimensions)), 51 | "arguments": ["out"].concat(aliases.slice(0, dimensions)), 52 | "types": aliases.slice(0, dimensions + 1).map(each => "vector")}, 53 | 54 | "add": 55 | {"function": VectorFactory.operation("+"), 56 | "arguments": ["out", "a", "b"], 57 | "types": ["vector", "vector", "vector"]}, 58 | "sub": 59 | {"function": VectorFactory.operation("-"), 60 | "arguments": ["out", "a", "b"], 61 | "types": ["vector", "vector", "vector"]}, 62 | "mul": 63 | {"function": VectorFactory.operation("*"), 64 | "arguments": ["out", "a", "b"], 65 | "types": ["vector", "vector", "vector"]}, 66 | "div": 67 | {"function": VectorFactory.operation("/"), 68 | "arguments": ["out", "a", "b"], 69 | "types": ["vector", "vector", "vector"]}, 70 | "min": 71 | {"function": VectorFactory.min, 72 | "arguments": ["out", "a", "b"], 73 | "types": ["vector", "vector", "vector"]}, 74 | "max": 75 | {"function": VectorFactory.max, 76 | "arguments": ["out", "a", "b"], 77 | "types": ["vector", "vector", "vector"]}, 78 | 79 | "scale": 80 | {"function": VectorFactory.operation("*"), 81 | "arguments": ["out", "a", "b"], 82 | "types": ["vector", "vector", "scalar"]}, 83 | "scaleAndAdd": 84 | {"function": VectorFactory.scaleAndAdd, 85 | "arguments": ["out", "a", "b", "c"], 86 | "types": ["vector", "vector", "vector", "scalar"]}, 87 | "lerp": 88 | {"function": VectorFactory.lerp, 89 | "arguments": ["out", "a", "b", "t"], 90 | "types": ["vector", "vector", "vector", "scalar"]}, 91 | 92 | "negate": 93 | {"function": VectorFactory.negate, 94 | "arguments": ["out", "a"], 95 | "types": ["vector", "vector"]}, 96 | "inverse": 97 | {"function": VectorFactory.inverse, 98 | "arguments": ["out", "a"], 99 | "types": ["vector", "vector"]}, 100 | "normalize": 101 | {"function": VectorFactory.normalize, 102 | "arguments": ["out", "a"], 103 | "types": ["vector", "vector"]}, 104 | 105 | "dot": 106 | {"function": VectorFactory.dot, 107 | "arguments": ["a", "b"], 108 | "types": ["vector", "vector"]}, 109 | "wellFormed": 110 | {"function": VectorFactory.wellFormed, 111 | "arguments": ["a"], 112 | "types": ["vector"]}, 113 | "squaredLength": 114 | {"function": VectorFactory.squaredLength, 115 | "arguments": ["a"], 116 | "types": ["vector"]}, 117 | "len": 118 | {"function": VectorFactory.length, 119 | "arguments": ["a"], 120 | "types": ["vector"]}, 121 | "squaredDistance": 122 | {"function": VectorFactory.squaredDistance, 123 | "arguments": ["a", "b"], 124 | "types": ["vector", "vector"]}, 125 | "dist": 126 | {"function": VectorFactory.distance, 127 | "arguments": ["a", "b"], 128 | "types": ["vector", "vector"]}, 129 | "sum": 130 | {"function": VectorFactory.sum, 131 | "arguments": ["a"], 132 | "types": ["vector"]} 133 | }; 134 | 135 | for (let operationName in operations) { 136 | let cb = new CodeBuilder(), 137 | source = new SourceWriter(), 138 | operation = operations[operationName], 139 | args = []; 140 | 141 | cb.temporariesPool = aliases.slice(); 142 | 143 | let output = operation["arguments"][0] == "out"; 144 | for (let i = (output ? 1 : 0); i < operation["arguments"].length; i++) { 145 | let name = operation["arguments"][i], 146 | type = operation["types"][i]; 147 | switch(type) { 148 | case "scalar": 149 | args.push(cb.scalar(name)); 150 | break; 151 | case "vector": 152 | args.push(cb.vector(dimensions, name)); 153 | break; 154 | default: 155 | throw "Unknown type for function argument"; 156 | } 157 | } 158 | 159 | let body = operation["function"].apply(null, [cb].concat(args)); 160 | if (output) body = cb.assign(cb.vector(dimensions, "out"), body); 161 | body = cb.output(body); 162 | 163 | source.tab(); 164 | cb.write(source, [body]); 165 | source.untab(); 166 | 167 | let compiled = CodeBuilder.compile(operationName, operation["arguments"], source.string, "Vector" + dimensions); 168 | //fromFunction[operationName] || Object.defineProperty(fromFunction, operationName, {value: compiled}); 169 | destination[operationName] || Object.defineProperty(destination, operationName, {value: compiled}); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /es6/nd-linalg/Vector2.js: -------------------------------------------------------------------------------- 1 | import { clamp } from '../missing-stuff'; 2 | import { create } from './Vector'; 3 | 4 | var Vector2 = function (x, y) { 5 | return Vector2.fromValues(x, y); 6 | }; 7 | create(2, Vector2); 8 | 9 | let properties = { 10 | "crossz": {value: crossz}, 11 | "cross": {value: cross}, 12 | "perpendicular": {value: perpendicular}, 13 | "scalePerpendicularAndAdd": {value: scalePerpendicularAndAdd}, 14 | "angleBetween": {value: angleBetween}, 15 | "angleBetweenWithDirections": {value: angleBetweenWithDirections}, 16 | "rotate": {value: rotate} 17 | } 18 | 19 | Object.defineProperties(Vector2, properties); 20 | // module.exports = Vector2; 21 | export default Vector2 22 | 23 | function crossz(a, b) { 24 | return a[0] * b[1] - b[0] * a[1]; 25 | } 26 | 27 | function cross(output, a, b) { 28 | output[0] = 0; 29 | output[1] = 1; 30 | output[2] = crossz(a, b); 31 | return output; 32 | } 33 | 34 | function perpendicular(output, a) { 35 | output[0] = a[1]; 36 | output[1] = -a[0]; 37 | return output; 38 | } 39 | 40 | function scalePerpendicularAndAdd(output, a, b, scale) { 41 | output[0] = a[0] + b[1] * scale; 42 | output[1] = a[1] - b[0] * scale; 43 | return output; 44 | } 45 | 46 | function angleBetween(a, b) { 47 | var theta = Vector2.dot(a, b) / (Vector2.len(a) * Vector2.len(b)); 48 | return Math.acos(clamp( theta,-1, 1) ); 49 | } 50 | 51 | function angleBetweenWithDirections(a, aDirection, b) { 52 | var simpleAngle = angleBetween(a, b); 53 | var linearDirection = Vector2.sub(Vector2.fromValues(0, 0), b, a); 54 | 55 | if (Vector2.dot(aDirection, linearDirection) >= 0) { 56 | return simpleAngle; 57 | } else { 58 | return 2 * Math.PI - simpleAngle; 59 | } 60 | } 61 | 62 | function rotate(out, v, angle) { 63 | // slow version: 64 | // return Matrix2x2.map(out, Matrix2x2.rotation(angle), v); 65 | 66 | var x = v[0], y = v[1], 67 | c = Math.cos(angle), 68 | s = Math.sin(angle); 69 | 70 | out[0] = c * x - s * y; 71 | out[1] = s * x + c * y; 72 | return out; 73 | } -------------------------------------------------------------------------------- /es6/nd-linalg/Vector3.js: -------------------------------------------------------------------------------- 1 | import { create } from './Vector'; 2 | var Vector3 = function (x, y, z) { 3 | return Vector3.fromValues(x, y, z); 4 | }; 5 | 6 | create(3, Vector3); 7 | Object.defineProperties(Vector3, { 8 | "cross": {value: cross} 9 | }); 10 | export default Vector3 11 | 12 | function cross(output, a, b) { 13 | var ax = a[0], ay = a[1], az = a[2], 14 | bx = b[0], by = b[1], bz = a[2]; 15 | 16 | output[0] = ay * bz - az * by; 17 | output[1] = az * bx - ax * bz; 18 | output[2] = ax * by - ay * bx; 19 | return output; 20 | } 21 | -------------------------------------------------------------------------------- /es6/nd-linalg/Vector4.js: -------------------------------------------------------------------------------- 1 | import { create } from './Vector'; 2 | 3 | var Vector4 = function (x, y, z, w) { 4 | return Vector4.fromValues(x, y, z, w); 5 | }; 6 | 7 | create(4, Vector4); 8 | Object.defineProperties(Vector4, { 9 | }); 10 | export default Vector4 -------------------------------------------------------------------------------- /es6/nd-linalg/VectorFactory.js: -------------------------------------------------------------------------------- 1 | let VectorFactory = {}; 2 | 3 | export default VectorFactory; 4 | 5 | const ROUGHLY_EPSILON = 1e-4; 6 | let isRoughly = function(number, other, epsilon) { 7 | return Math.abs(this - other) <= (epsilon || ROUGHLY_EPSILON); 8 | } 9 | 10 | let min = (cb, a, b) => cb.map("Math.min", a, b), 11 | max = (cb, a, b) => cb.map("Math.max", a, b), 12 | inverse = (cb, a) => cb.map("1.0 /", a), 13 | negate = (cb, a) => cb.map("[[negate]]", a), 14 | sum = (cb, a) => cb.reduce("+", a)[0], 15 | average = (cb, a) => cb.apply("/", sum(cb, a), cb.scalar(a.length)), 16 | dot = (cb, a, b) => sum(cb, cb.map("*", a, b)), 17 | squaredLength = (cb, a) => dot(cb, a, a), 18 | length = (cb, a) => cb.apply("sqrt", squaredLength(cb, a)), 19 | wellFormed = (cb, a) => cb.reduce("&&", cb.map("&&", 20 | cb.map("Number.isFinite", a), 21 | cb.map("!Number.isNaN", a)))[0], 22 | squaredDistance = (cb, a, b) => squaredLength(cb, cb.map("-", b, a)), 23 | distance = (cb, a, b) => cb.apply("sqrt", squaredDistance(cb, a, b)), 24 | scaleAndAdd = (cb, a, b, c) => cb.map("+", a, cb.map("*", b, c)), 25 | lerp = (cb, a, b, c) => cb.map("+", a, cb.map("*", c, cb.map("-", b, a))); 26 | 27 | function set(args) { 28 | return function(cb, out) { 29 | return args.map(each => cb.scalar(each)); 30 | } 31 | } 32 | 33 | function operation(op) { 34 | return function operation(cb, a, b) { 35 | return cb.map(op, a, b); 36 | } 37 | } 38 | 39 | function normalize(cb, a) { 40 | // (||a||^2) > 0 ? a.(1/||a||) : 0 41 | let zero = cb.scalar("0"), 42 | condition = [squaredLength(cb, a), ">", zero], 43 | scalingLength = cb.phi(condition, cb.apply("1.0 /", length(cb, a)), zero, "length"); 44 | return cb.map("*", a, scalingLength); 45 | } 46 | 47 | 48 | function isRoughlyVec( a, b, epsilon ){ 49 | return ( Math.abs(a[0] - b[0]) <= (epsilon || ROUGHLY_EPSILON) ) 50 | && ( Math.abs(a[1] - b[1]) <= (epsilon || ROUGHLY_EPSILON) ); 51 | } 52 | 53 | Object.defineProperties(VectorFactory, { 54 | "set": {value: set}, 55 | "operation": {value: operation}, 56 | "min": {value: min}, 57 | "max": {value: max}, 58 | "scaleAndAdd": {value: scaleAndAdd}, 59 | "inverse": {value: inverse}, 60 | "dot": {value: dot}, 61 | "squaredLength": {value: squaredLength}, 62 | "length": {value: length}, 63 | "wellFormed": {value: wellFormed}, 64 | "squaredDistance": {value: squaredDistance}, 65 | "distance": {value: distance}, 66 | "negate": {value: negate}, 67 | "sum": {value: sum}, 68 | "average": {value: average}, 69 | "normalize": {value: normalize}, 70 | "lerp": {value: lerp}, 71 | "isRoughly": {value:isRoughlyVec} 72 | }); 73 | -------------------------------------------------------------------------------- /es6/primitives/Circle.js: -------------------------------------------------------------------------------- 1 | import intersect, {THICKNESS} from '../intersections/Intersections'; 2 | import vec2 from '../nd-linalg/Vector2'; 3 | import {angleFrom} from '../helpers/Angles'; 4 | 5 | export default Circle; 6 | 7 | function Circle(center, radius) { 8 | this.center = center; 9 | this.radius = radius; 10 | } 11 | 12 | Object.defineProperties(Circle.prototype, { 13 | "name": {value: "Circle"}, 14 | "type": {value: intersect.CircleTypeFunction}, 15 | "isRay": {value: false}, 16 | "isLineSegment": {value: false}, 17 | "isCurve": {value: false}, 18 | "isCircle": {value: true}, 19 | 20 | "boundingBox": {get: boundingBox}, 21 | 22 | "scale": {value: scale}, 23 | "translate": {value: translate}, 24 | 25 | "angle": {value: angle}, 26 | "directionOf": {value: directionOf}, 27 | "containsPoint": {value: containsPoint}, 28 | 29 | "draw": {value: draw} 30 | }); 31 | 32 | function boundingBox() { 33 | var origin = vec2.clone(this.center), 34 | corner = vec2.clone(this.center), 35 | radius = vec2.fromValues(this.radius, this.radius); 36 | vec2.sub(origin, origin, radius); 37 | vec2.add(corner, corner, radius); 38 | return Rectangle.corner(origin, corner); 39 | } 40 | 41 | function scale(scalar) { 42 | var center = vec2.clone(this.center), 43 | radius = radius * scalar; 44 | vec2.scale(center, center, scalar); 45 | return new Circle(center, radius); 46 | } 47 | 48 | function translate(offset) { 49 | var center = vec2.clone(this.center); 50 | vec2.add(center, center, offset); 51 | return new Circle(center, this.radius); 52 | } 53 | 54 | // Return the angle for point p on the circle 55 | function angle(p) { 56 | return angleFrom(p, this.center); 57 | } 58 | 59 | function directionOf(p, asClockwise) { 60 | var vTail = vec2(0, 0); 61 | vec2.sub(vTail, this.center, p); 62 | var dTail = asClockwise 63 | ? vec2.fromValues(-vTail[1], vTail[0]) 64 | : vec2.fromValues(vTail[1], -vTail[0]); 65 | vec2.normalize(dTail, dTail); 66 | return dTail; 67 | } 68 | 69 | function containsPoint(p) { 70 | return vec2.dist(this.center, p) <= this.radius + THICKNESS; 71 | } 72 | 73 | function draw(context) { 74 | context.stroke(this); 75 | } 76 | -------------------------------------------------------------------------------- /es6/primitives/Line.js: -------------------------------------------------------------------------------- 1 | import Intersections from '../intersections/Intersections'; 2 | import vec2 from '../nd-linalg/Vector2'; 3 | 4 | export default Line; 5 | 6 | function Line(rayOrMiddle, direction) { 7 | if (rayOrMiddle.type === Intersections.RayTypeFunction) { 8 | this.middle = rayOrMiddle.start; 9 | this.direction = rayOrMiddle.direction; 10 | return this; 11 | } 12 | this.middle = rayOrMiddle; // LOL! 13 | this.direction = direction; 14 | } 15 | 16 | Object.defineProperties(Line.prototype, { 17 | "name": {value: "Line"}, 18 | "type": {value: Intersections.LineTypeFunction}, 19 | "isRay": {value: true}, 20 | "isLineSegment":{value: false}, 21 | "isCurve": {value: false}, 22 | "isCircle": {value: false}, 23 | 24 | "length": {value: Number.infinity}, 25 | "start": {value: vec2.fromValues(Number.infinity, Number.infinity)}, 26 | "end": {value: vec2.fromValues(Number.infinity, Number.infinity)}, 27 | "midpoint": {value: midpoint}, // ALSO LOL! 28 | "boundingBox": {get: boundingBox}, // TROLLOLOLOL 29 | 30 | "subdivide": {value: subdivide}, 31 | "reverse": {value: reverse}, 32 | "scale": {value: scale}, 33 | "translate": {value: translate}, 34 | 35 | "draw": {value: draw} 36 | }); 37 | 38 | function midpoint() { 39 | return this.middle; 40 | } 41 | 42 | function subdivide(p) { 43 | throw "Cannot subdivide a bidirection ray"; 44 | } 45 | 46 | function reverse() { 47 | return this; 48 | } 49 | 50 | function scale(scalar) { 51 | throw "Cannot scale a bidirectional ray"; 52 | } 53 | 54 | function translate(offset) { 55 | var middle = vec2.clone(this.middle); 56 | vec2.add(middle, middle, offset); 57 | return new Line(middle, this.direction); 58 | } 59 | 60 | function boundingBox() { 61 | return Rectangle.point(this.middle); 62 | } 63 | 64 | function draw(context) { 65 | context.arrow(this.middle, this.direction, 0.5); 66 | context.arrow(this.middle, vec2.scale(vec2(0, 0), this.direction, -1), 0.5); 67 | } 68 | -------------------------------------------------------------------------------- /es6/primitives/LineSegment.js: -------------------------------------------------------------------------------- 1 | import intersect from '../intersections/Intersections'; 2 | import vec2 from '../nd-linalg/Vector2'; 3 | import {corner as RectangleCorner} from './Rectangle'; 4 | import * as Triangle from './Triangle'; 5 | import { roughlyEqual, roughlyBetween } from '../missing-stuff'; 6 | // import { colinear } from triangle; 7 | 8 | export default LineSegment; 9 | 10 | function LineSegment(start, end) { 11 | this.start = start; 12 | this.end = end; 13 | this.direction = vec2.sub(vec2(0, 0), this.end, this.start); 14 | this.length = vec2.len(this.direction); 15 | vec2.normalize(this.direction, this.direction); 16 | } 17 | 18 | function createProjection(start, direction, length) { 19 | var end = vec2.scale(vec2(0, 0), direction, length || 1); 20 | vec2.add(end, start, end); 21 | return new LineSegment(start, end); 22 | } 23 | 24 | LineSegment.project = createProjection; 25 | 26 | Object.defineProperties(LineSegment.prototype, { 27 | "name": {value: "LineSegment"}, 28 | "type": {value: intersect.LineSegmentTypeFunction}, 29 | "isRay": {value: false}, 30 | "isLineSegment": {value: true}, 31 | "isCurve": {value: false}, 32 | "isCircle": {value: false}, 33 | 34 | "midpoint": {get: midpoint}, 35 | "boundingBox": {get: boundingBox}, 36 | "endDirection": {get: endDirection}, 37 | 38 | "subdivide": {value: subdivide}, 39 | "reverse": {value: reverse}, 40 | "scale": {value: scale}, 41 | "translate": {value: translate}, 42 | "offsetPerpendicular": {value: offsetPerpendicular}, 43 | 44 | "offsetPerpendicularLength": {value: offsetPerpendicularLength}, 45 | "mapPerpendicular": {value: mapPerpendicular}, 46 | 47 | "containsPoint": {value: containsPoint}, 48 | "roughlyContainsPoint": {value: roughlyContainsPoint}, 49 | "getAlphaValueAtPosition": {value: getAlphaValueAtPosition}, 50 | "offsetOf": {value: offsetOf}, 51 | "closestPointTo": {value: closestPointTo}, 52 | "positionOf": {value: positionOf}, 53 | "directionOf": {value: directionOf}, 54 | 55 | "vertices": {value: vertices}, 56 | "uvs": {value: uvs}, 57 | "draw": {value: draw} 58 | }); 59 | 60 | function midpoint() { 61 | var midpoint = vec2(0, 0); 62 | vec2.lerp(midpoint, this.start, this.end, 0.5); 63 | return midpoint; 64 | } 65 | 66 | var min = Math.min, 67 | max = Math.max; 68 | function boundingBox() { 69 | var origin = vec2.clone(this.start); 70 | var corner = vec2.clone(this.start); 71 | origin[0] = min(origin[0], this.end[0]); 72 | origin[1] = min(origin[1], this.end[1]); 73 | corner[0] = max(corner[0], this.end[0]); 74 | corner[1] = max(corner[1], this.end[1]); 75 | return RectangleCorner(origin, corner); 76 | } 77 | 78 | function endDirection() { 79 | return this.direction; 80 | } 81 | 82 | function subdivide(p) { 83 | return [new LineSegment(this.start, p), new LineSegment(p, this.end)]; 84 | } 85 | 86 | function reverse() { 87 | return new LineSegment(this.end, this.start); 88 | } 89 | 90 | function scale(scalar) { 91 | var start = vec2.clone(this.start), 92 | end = vec2.clone(this.end); 93 | vec2.scale(start, start, scalar); 94 | vec2.scale(end, end, scalar); 95 | return new LineSegment(start, end); 96 | } 97 | 98 | function translate(offset) { 99 | var start = vec2.clone(this.start), 100 | end = vec2.clone(this.end); 101 | vec2.add(start, start, offset); 102 | vec2.add(end, end, offset); 103 | return new LineSegment(start, end); 104 | } 105 | 106 | function containsPoint(point) { 107 | let perpendicularDirection = vec2.perpendicular(vec2(0, 0), direction); 108 | let startToPoint = vec2.sub(vec2(0, 0), start, p); 109 | let distance = Math.abs(vec2.dot(startToPoint, perpendicularDirection)); 110 | let u = vec2.dot(startToPoint, this.direction); 111 | return distance < Intersections.THICKNESS && u > -THICKNESS && u < this.length + THICKNESS; 112 | 113 | } 114 | 115 | function roughlyContainsPoint(p) { 116 | // http://stackoverflow.com/questions/328107/how-can-you-determine-a-point-is-between-two-other-points-on-a-line-segment 117 | let start = this.start, end = this.end; 118 | return Triangle.colinear(start, p, end) 119 | && ((roughlyEqual(start[0], p[0]) || roughlyEqual(end[0], p[0])) 120 | ? roughlyBetween( start[1], p[1], end[1]) || roughlyBetween( end[1], p[1], start[1]) 121 | : roughlyBetween( start[0], p[0], end[0]) || roughlyBetween( end[0], p[0], start[0])); 122 | } 123 | 124 | 125 | function getAlphaValueAtPosition(p) { 126 | return vec2.dist(this.start, p) / this.length; 127 | } 128 | 129 | function directionOf(p) { 130 | return this.direction; 131 | } 132 | 133 | function offsetOf(p) { 134 | return vec2.dot(this.direction, vec2.sub(vec2(0, 0), p, this.start)); 135 | } 136 | 137 | function closestPointTo(p) { 138 | var offset = this.offsetOf(p); 139 | 140 | if (offset < 0) 141 | return this.start; 142 | if (offset > this.length) 143 | return this.end; 144 | 145 | return vec2.scaleAndAdd(vec2(0, 0), this.start, this.direction, offset); 146 | } 147 | 148 | function positionOf(offset) { 149 | return vec2.scaleAndAdd(vec2(0, 0), this.start, this.direction, offset); 150 | } 151 | 152 | function offsetPerpendicular(offsetToRight) { 153 | return new LineSegment( 154 | vec2.scalePerpendicularAndAdd(vec2(0, 0), this.start, this.direction, offsetToRight), 155 | vec2.scalePerpendicularAndAdd(vec2(0, 0), this.end, this.direction, offsetToRight) 156 | ); 157 | } 158 | 159 | function offsetPerpendicularLength(offsetToRight) { 160 | return this.length; 161 | } 162 | 163 | function mapPerpendicular(offsetA, offsetToRightA, offsetToRightB) { 164 | return offsetA; 165 | } 166 | 167 | function vertices(offsetToRight) { 168 | offsetToRight = offsetToRight || 0; 169 | var start = offsetToRight 170 | ? vec2.scalePerpendicularAndAdd(vec2(0, 0), this.start, this.direction, offsetToRight) 171 | : this.start; 172 | var end = offsetToRight 173 | ? vec2.scalePerpendicularAndAdd(vec2(0, 0), this.end, this.direction, offsetToRight) 174 | : this.end; 175 | return [start, end]; 176 | } 177 | 178 | function uvs(offsetToRight, multiplierAlongPath) { 179 | var startUV = vec2.fromValues(0, offsetToRight); 180 | var endUV = vec2.fromValues(this.length * multiplierAlongPath, offsetToRight); 181 | return [startUV, endUV]; 182 | } 183 | 184 | function draw(context) { 185 | context.stroke(this); 186 | } 187 | -------------------------------------------------------------------------------- /es6/primitives/Ray.js: -------------------------------------------------------------------------------- 1 | import Intersections from '../intersections/Intersections'; 2 | import vec2 from '../nd-linalg/Vector2'; 3 | 4 | export default Ray; 5 | 6 | function Ray(start, direction) { 7 | this.start = start; 8 | this.direction = direction; 9 | } 10 | 11 | Object.defineProperties(Ray.prototype, { 12 | "name": {value: "Ray"}, 13 | "type": {value: Intersections.RayTypeFunction}, 14 | "isRay": {value: true}, 15 | "isLineSegment": {value: false}, 16 | "isCurve": {value: false}, 17 | "isCircle": {value: false}, 18 | 19 | "length": {value: Number.infinity}, 20 | "end": {value: vec2.fromValues(Number.infinity, Number.infinity)}, 21 | "midpoint": {value: Number.infinity}, 22 | 23 | "subdivide": {value: subdivide}, 24 | "reverse": {value: reverse}, 25 | "scale": {value: scale}, 26 | "translate": {value: translate}, 27 | 28 | "draw": {value: draw}, 29 | "boundingBox": {get: boundingBox} 30 | }); 31 | 32 | function subdivide(p) { 33 | return [new LineSegment(this.start, p), new Ray(p, this.direction)]; 34 | } 35 | 36 | function reverse() { 37 | return new Ray(this.start, vec2.negate(vec2(0, 0), this.direction)); 38 | } 39 | 40 | function scale(scalar) { 41 | var start = vec2.clone(this.start); 42 | vec2.scale(start, start, scalar); 43 | return new Ray(start, this.direction); 44 | } 45 | 46 | function translate(offset) { 47 | var start = vec2.clone(this.start); 48 | vec2.add(start, start, offset); 49 | return new Ray(start, this.direction); 50 | } 51 | 52 | function boundingBox() { 53 | return Rectangle.point(this.start); 54 | } 55 | 56 | function draw(context) { 57 | context.arrow(this.start, this.direction); 58 | } 59 | -------------------------------------------------------------------------------- /es6/primitives/Rectangle.js: -------------------------------------------------------------------------------- 1 | import vec2 from '../nd-linalg/Vector2'; 2 | 3 | export default Rectangle; 4 | 5 | export { 6 | newRectangleCorner as corner, 7 | newRectangleExtent as extent, 8 | newRectanglePoint as point, 9 | newRectangleMinMax as minmax, 10 | newRectangleZero as zero 11 | }; 12 | 13 | function Rectangle(top, right, bottom, left) { 14 | this.top = top; 15 | this.bottom = bottom; 16 | this.left = left; 17 | this.right = right; 18 | } 19 | 20 | function newRectangleCorner(origin, corner) { 21 | return new Rectangle(origin[1], corner[0], corner[1], origin[0]); 22 | } 23 | 24 | function newRectangleExtent(origin, extent) { 25 | return new Rectangle( 26 | origin[1], 27 | origin[0] + extent[0], 28 | origin[1] + extent[1], 29 | origin[0]); 30 | } 31 | 32 | function newRectanglePoint(position) { 33 | return newRectangleCorner(position, position); 34 | } 35 | 36 | function newRectangleMinMax() { 37 | return new Rectangle(Infinity, -Infinity, -Infinity, Infinity); 38 | } 39 | 40 | function newRectangleZero() { 41 | return new Rectangle(0, 0, 0, 0); 42 | } 43 | 44 | Object.defineProperties(Rectangle.prototype, { 45 | "name": {value: "Rectangle"}, 46 | "width": {enumerable: true, get: getWidth, set: setWidth}, 47 | "height": {enumerable: true, get: getHeight, set: setHeight}, 48 | "center": {get: center}, 49 | "origin": {get: getOrigin, set: setOrigin}, 50 | "extent": {get: getExtent, set: setExtent}, 51 | "corner": {get: getCorner, set: setCorner}, 52 | "boundingBox": {get: boundingBox}, 53 | 54 | "containsPoint": {value: containsPoint}, 55 | "scale": {value: scale}, 56 | "translate": {value: translate}, 57 | "expand": {value: expand}, 58 | 59 | "draw": {value: draw} 60 | }); 61 | 62 | function getWidth() { 63 | return this.right - this.left; 64 | } 65 | 66 | function setWidth(value) { 67 | this.right = this.left + value; 68 | } 69 | 70 | function getHeight() { 71 | return this.bottom - this.top; 72 | } 73 | 74 | function setHeight(value) { 75 | this.bottom = this.top + value; 76 | } 77 | 78 | function center() { 79 | var center = vec2(0, 0); 80 | vec2.lerp(center, this.origin, this.corner, 0.5); 81 | return center; 82 | } 83 | 84 | function getOrigin() { 85 | return vec2.fromValues(this.left, this.top); 86 | } 87 | 88 | function setOrigin(v) { 89 | this.left = v[0]; 90 | this.top = v[1]; 91 | } 92 | 93 | function getExtent() { 94 | return vec2.fromValues(this.width, this.height); 95 | } 96 | 97 | function setExtent(v) { 98 | this.width = v[0]; 99 | this.height = v[1]; 100 | } 101 | 102 | function getCorner() { 103 | return vec2.fromValues(this.right, this.bottom); 104 | } 105 | 106 | function setCorner(v) { 107 | this.right = v[0]; 108 | this.bottom = v[1]; 109 | } 110 | 111 | function boundingBox() { 112 | return this; 113 | } 114 | 115 | function containsPoint(p) { 116 | return this.left <= p[0] && p[0] < this.right && this.top <= p[1] && p[1] < this.bottom; 117 | } 118 | 119 | function scale(scalar) { 120 | return new Rectangle( 121 | this.top * scalar, 122 | this.right * scalar, 123 | this.bottom * scalar, 124 | this.left * scalar); 125 | } 126 | 127 | function translate(offset) { 128 | return new Rectangle( 129 | this.top + offset[1], 130 | this.right + offset[0], 131 | this.bottom + offset[1], 132 | this.left + offset[0]); 133 | } 134 | 135 | function expand(rectangle) { 136 | return new Rectangle( 137 | Math.min(this.top, rectangle.top), 138 | Math.max(this.right, rectangle.right), 139 | Math.max(this.bottom, rectangle.bottom), 140 | Math.min(this.left, rectangle.left)); 141 | } 142 | 143 | function draw(context) { 144 | context.stroke(this); 145 | } 146 | -------------------------------------------------------------------------------- /es6/primitives/Triangle.js: -------------------------------------------------------------------------------- 1 | import { roughlyEqual } from '../missing-stuff'; 2 | 3 | export default class Triangle { 4 | constructor(a, b, c) { 5 | this.a = a; 6 | this.b = b; 7 | this.c = c; 8 | } 9 | 10 | get isColinear() { return colinear(this.a, this.b, this.c); } 11 | get center() { return center(this.a, this.b, this.c); } 12 | } 13 | 14 | export function colinear(a, b, c) { 15 | const m = (c[0] - a[0]) * (b[1] - a[1]); 16 | const n = ((b[0] - a[0]) * (c[1] - a[1])); 17 | return roughlyEqual( n, m ); 18 | } 19 | 20 | export function center(a, b, c) { 21 | return [(a[0] + b[0] + c[0]) / 3, 22 | (a[1] + b[1] + c[1]) / 3]; 23 | } 24 | -------------------------------------------------------------------------------- /es6/shapes/PathCollisionCollection.js: -------------------------------------------------------------------------------- 1 | export default function PathCollisionCollection () { 2 | this.pathsToObjects = new Map(); 3 | this.objectsToPaths = new Map(); 4 | } 5 | 6 | Object.assign(PathCollisionCollection.prototype, { 7 | add, remove, 8 | collide 9 | }); 10 | 11 | function add (path, object) { 12 | this.pathsToObjects.set(path, object); 13 | this.objectsToPaths.set(object, path); 14 | } 15 | 16 | function remove (object) { 17 | let path = this.objectsToPaths.get(object); 18 | this.objectsToPaths.delete(object); 19 | this.pathsToObjects.delete(path); 20 | } 21 | 22 | function * collide (path) { 23 | for (let otherPath of this.pathsToObjects.keys()) { 24 | if (path === otherPath) continue; 25 | let intersectionPoints = path.intersections(otherPath); 26 | if (intersectionPoints.length) yield { 27 | object: this.pathsToObjects.get(otherPath), 28 | points: intersectionPoints.map(point => point.p), 29 | }; 30 | } 31 | } -------------------------------------------------------------------------------- /es6/shapes/Shape.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { ROUGHLY_EPSILON } from '../missing-stuff'; 3 | import vec3 from '../nd-linalg/Vector3'; 4 | 5 | import cdt2d from 'cdt2d'; 6 | import cleanPSLG from 'clean-pslg'; 7 | 8 | export default Shape; 9 | 10 | function Shape(contourPath, holes) { 11 | this.contour = contourPath; 12 | this.holes = holes || []; 13 | } 14 | 15 | Object.defineProperties(Shape.prototype, { 16 | "contains": {value: contains}, 17 | "union": {value: union}, 18 | "difference": {value: difference}, 19 | "intersection": {value: intersection}, 20 | "not": {value: not}, 21 | "triangulate": {value: triangulate}, 22 | "area": {value: area}, 23 | "split": {value: split}, 24 | "grow": {value: grow}, 25 | "growWithSkeleton": {value: growWithSkeleton} 26 | }); 27 | 28 | function contains(otherShape) { 29 | var intersects = this.contour.intersections(otherShape.contour).length === 0; 30 | if (intersects) return false; 31 | 32 | for (var point of otherShape.contour.segments.map(s => s.start)) { 33 | if (!this.contour.containsPoint(point)) return false; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | function union(otherShape) { 40 | 41 | } 42 | 43 | function difference(otherShape, clipGrowthOffset) { 44 | if (otherShape.contains(this)) return []; 45 | var difference = Clipper.difference(this.contour, otherShape.grow(clipGrowthOffset).contour); 46 | return difference.map(contour => new Shape(contour)); 47 | } 48 | 49 | function intersection(otherShape) { 50 | 51 | } 52 | 53 | function not(otherShape) { 54 | 55 | } 56 | 57 | function validate( vec ){ 58 | if( isNaN( vec[ 0 ] ) || isNaN( vec[ 1 ] ) ){ 59 | return false; 60 | } 61 | if( vec[ 0 ] === Infinity || vec[ 1 ] === Infinity ){ 62 | return false; 63 | } 64 | return true; 65 | } 66 | 67 | function triangulate(minVertexDistance = 0.00001) { 68 | const geometry = new THREE.Geometry(); 69 | 70 | const points = []; 71 | const edges = []; 72 | 73 | const vertices = []; 74 | 75 | let count = 0; 76 | this.contour.segments.forEach( function( {start, end}, index ){ 77 | if( validate( start ) && validate( end ) ){ 78 | vertices.push( start, end ); 79 | points.push( [start[0], start[1]], [end[0], end[1]] ); 80 | edges.push( [count * 2, count * 2 + 1] ); 81 | count++; 82 | } 83 | }); 84 | 85 | const precision = 8; 86 | // preserve z-axis data... 87 | const zs = {}; 88 | vertices.forEach( function( [x,y,z] ){ 89 | if( z === undefined ){ 90 | z = 0; 91 | } 92 | zs[ x.toFixed( precision )+'_'+y.toFixed( precision ) ] = z; 93 | }); 94 | 95 | // clean up skeleton + contour because it's not a valid edge loop 96 | cleanPSLG( points, edges ); 97 | 98 | const triangulation = cdt2d( points, edges, {exterior:false, delaunay:true} ); 99 | 100 | const points3D = points.map( function( [x,y] ){ 101 | const key = x.toFixed( precision )+'_'+y.toFixed( precision ); 102 | const z = zs[key]; 103 | if( z === undefined ){ 104 | console.warn('z not found', x, y, zs ); 105 | return [ x,y,0 ]; 106 | } 107 | return [x,y,z]; 108 | }); 109 | 110 | // console.log( zs, points3D ); 111 | 112 | geometry.vertices = points3D.map( function( [x,y,z] ){ 113 | return new THREE.Vector3( x,y,z ); 114 | }); 115 | 116 | geometry.faces = triangulation.map( function( [ix, iy, iz] ){ 117 | return new THREE.Face3( ix, iy, iz ); 118 | }); 119 | 120 | return geometry; 121 | } 122 | 123 | // function triangulate2(minVertexDistance = 0.00001) { 124 | // var geometry = new THREE.Geometry(); 125 | 126 | // const vs = this.contour.vertices(0, minVertexDistance); 127 | 128 | // // TODO / FIXME 129 | // // this is pretty awful, find a more elegant solution... 130 | // const unique = []; 131 | // for( let i=0; i c).map(c => new Shape(c)); 217 | 218 | for (var shape of results) { 219 | if (!shape.contour.isClosed) { 220 | DebugPaths.add(box1, 0x00ff00, "splitfail", 0, 2); 221 | DebugPaths.add(this.contour, 0x00ffff, "splitfail", 0, 4); 222 | DebugPaths.add(shape.contour, 0xff00ff, "splitfail", 0, 4); 223 | throw("splitfail"); 224 | } 225 | } 226 | 227 | return results; 228 | } 229 | 230 | function grow (amount) { 231 | 232 | 233 | //var offsetContour = this.contour.offsetPerpendicular(this.contour.isCounterClockwise ? amount : -amount); 234 | //DebugPaths.add(offsetContour, 0xffffff, "grow", 0, 1); 235 | //offsetContour = offsetContour.makeContiguous(true); 236 | //DebugPaths.add(offsetContour, 0x888888, "grow", 0, 1.5); 237 | //offsetContour = offsetContour.simplifySelfIntersections(); 238 | //DebugPaths.add(offsetContour, 0x8800ff, "grow", 0, 2); 239 | return new Shape(offsetContour); 240 | } 241 | 242 | function growWithSkeleton (amount) { 243 | var skeleton = new Skeleton(amount > 0 ? this.contour : this.contour.reverse(), Math.abs(amount)); 244 | return new Shape(skeleton.waves[0].path); 245 | } -------------------------------------------------------------------------------- /es6/shapes/Stroke.js: -------------------------------------------------------------------------------- 1 | import Shape from './Shape'; 2 | const attachedRight = Symbol("attachedRight"); 3 | 4 | export {attachedRight}; 5 | 6 | export default class Stroke { 7 | constructor (path, attachment, width) { 8 | if (attachment !== attachedRight) throw "Not yet implemented"; 9 | 10 | this.leftEdge = path; 11 | this.rightEdge = path.offsetPerpendicular(width).simplifySelfIntersections(); 12 | 13 | let capIntersections = intersect( 14 | new LineSegment(this.leftEdge.start, this.rightEdge.start), 15 | new LineSegment(this.leftEdge.end, this.rightEdge.end)); 16 | 17 | // path doesn't have a right edge, caps intersect at a distance closer than width 18 | if (capIntersections[0]) { 19 | this.startCap = new LineSegment(this.leftEdge.start, capIntersections[0].p); 20 | this.endCap = new LineSegment(this.leftEdge.end, capIntersections[0].p); 21 | this.rightEdge = undefined; 22 | return; 23 | } 24 | 25 | // loop artifact at end 26 | let distanceBetweenEnds = vec2.dist(this.leftEdge.closestPointTo(this.rightEdge.end), this.rightEdge.end); 27 | if (distanceBetweenEnds < width - Number.ROUGHLY_EPSILON) { 28 | let shortenedRightEdge = new Path(this.rightEdge.segments.slice(0, -1)); 29 | let newOuterEndOffset = shortenedRightEdge.offsetOf(this.rightEdge.end); 30 | this.rightEdge = this.rightEdge.cut(0, newOuterEndOffset); 31 | } 32 | 33 | // loop artifact at beginning 34 | let distanceBetweenStarts = vec2.dist(this.leftEdge.closestPointTo(this.rightEdge.start), this.rightEdge.start); 35 | if (distanceBetweenStarts < width - Number.ROUGHLY_EPSILON) { 36 | let shortenedRightEdge = new Path(this.rightEdge.segments.slice(1)); 37 | let newOuterStartOffset = shortenedRightEdge.offsetOf(this.rightEdge.start); 38 | newOuterStartOffset += shortenedRightEdge.segments[0].length 39 | this.rightEdge = this.rightEdge.cut(newOuterStartOffset, this.rightEdge.length); 40 | } 41 | 42 | this.startCap = new Path([new LineSegment(this.leftEdge.start, this.rightEdge.start)]); 43 | this.endCap = new Path([new LineSegment(this.leftEdge.end, this.rightEdge.end)]); 44 | } 45 | 46 | get shape () { 47 | return new Shape(this.leftEdge.concat(this.endCap) 48 | .concat(this.rightEdge.reverse()).concat(this.startCap.reverse())); 49 | } 50 | } -------------------------------------------------------------------------------- /es6/skeleton/Skeleton.js: -------------------------------------------------------------------------------- 1 | /* TODO 2 | * Curves 3 | 1) For curves that are shrinking, only a wavefront that is within its original 4 | circle can have a collision event with it 5 | 2) For curves that are expanding, all vertices have a potential future event 6 | with the circle? can we limit this some how to only !acute vertices? 7 | 3) A shrinking curve can collapse in on itself (radius=0) 8 | * Order the pathways from the center outward, return an array of paths rather than 9 | * haphazard segments, building them up from outside in, merging as it intersects 10 | * 11 | * Find the center line: 12 | * 1) Find the longest spoke-tospoke 13 | * 2) Remove tiny segments 14 | * 3) Remove the first and last segment 15 | * 4) Extent the first and last segment to intersect the nearest original edge 16 | */ 17 | 18 | import vec2 from '../nd-linalg/Vector2'; 19 | import vec3 from '../nd-linalg/Vector3'; 20 | import LineSegment from '../primitives/LineSegment'; 21 | import * as SkeletonEdge from './SkeletonEdge'; 22 | import SkeletonWavefront from './SkeletonWavefront'; 23 | import Pather from '../../es6/helpers/Pather'; 24 | import Ray from '../primitives/Ray'; 25 | import Line from '../primitives/Line'; 26 | import intersect from '../intersections/Intersections'; 27 | 28 | var DeadEdge = Symbol("DeadEdge"); 29 | var infinity = Infinity; 30 | export default class StraightSkeleton { 31 | constructor(path, length, options) { 32 | options = options || {}; 33 | this.length = length || infinity; 34 | this.spokes = []; 35 | this.waves = []; 36 | 37 | this.DEBUG_DRAW_INITIAL = options.DEBUG_DRAW_INITIAL || false; 38 | this.DEBUG_DRAW_SKIPPED_EVENTS = options.DEBUG_DRAW_SKIPPED_EVENTS || false; 39 | this.DEBUG_DRAW_STEPS = options.DEBUG_DRAW_STEPS || false; 40 | this.DEBUG_DRAW_MOVE = options.DEBUG_DRAW_MOVE || this.DEBUG_DRAW_STEPS; 41 | this.DEBUG_DRAW_OBTUSE_EVENTS_EACH_STEP = options.DEBUG_DRAW_OBTUSE_EVENTS_EACH_STEP || false; 42 | 43 | if ("capWeight" in options) { 44 | let startDirection = vec2.scale(vec2(0, 0), path.segments[0].direction, -1), 45 | startNormal = [-startDirection[1], startDirection[0]], 46 | endDirection = path.segments[path.segments.length - 1].direction, 47 | endNormal = [-endDirection[1], endDirection[0]]; 48 | 49 | let a = vec2.lerp(vec2(0, 0), startDirection, startNormal, 1 - options.capWeight); 50 | let b = vec2.lerp(vec2(0, 0), startDirection, startNormal, options.capWeight); 51 | let c = vec2.lerp(vec2(0, 0), endDirection, endNormal, 1 - options.capWeight); 52 | let d = vec2.lerp(vec2(0, 0), endDirection, endNormal, options.capWeight); 53 | 54 | this.caps = [ 55 | new Ray(path.start, [b[1], -b[0]]), 56 | new Ray(path.start, a), 57 | 58 | new Ray(path.end, [d[1], -d[0]]), 59 | new Ray(path.end, c), 60 | ]; 61 | } 62 | 63 | this.wavefronts = SkeletonEdge 64 | .create(path, this.length === infinity, this.cap) 65 | .map(each => new SkeletonWavefront(this, each, 0).initialise()); 66 | 67 | //this.process(); 68 | options.DEBUG ? this.debugprocess() : this.process(); 69 | } 70 | 71 | get name() { return "StraightSkeleton"; } 72 | 73 | process() { 74 | while(this.wavefronts.length > 0) { 75 | let wavefront = this.wavefronts[0]; 76 | if (!wavefront.process(this.length)) { 77 | // if (this.length === infinity) throw "EventsExhaustedPrematurely"; 78 | this.commitWavefront(wavefront); 79 | wavefront.remove(); 80 | } 81 | } 82 | } 83 | 84 | debugprocess() { 85 | while(this.wavefronts.length > 0) { 86 | let wavefront = this.wavefronts[0]; 87 | if (!wavefront.debugprocess(this.length)) { 88 | if (this.length === infinity) throw "EventsExhaustedPrematurely"; 89 | this.commitWavefront(wavefront); 90 | wavefront.remove(); 91 | } 92 | } 93 | } 94 | 95 | addWavefront(wavefront) { 96 | this.wavefronts.push(wavefront); 97 | } 98 | 99 | removeWavefront(wavefront) { 100 | this.wavefronts.splice(this.wavefronts.indexOf(wavefront), 1); 101 | } 102 | 103 | commitSkeletonSpoke(start, end) { 104 | this.spokes.push(new LineSegment(vec3.clone(start), vec3.clone(end))); 105 | } 106 | 107 | commitSkeletonVertex(vertex) { 108 | let beginning = vec3(vertex.position[0], vertex.position[1], vertex.wavefront.time); 109 | this.commitSkeletonSpoke(vertex.beginning, beginning); 110 | vertex.beginning = beginning; 111 | } 112 | 113 | commitWavefront(wavefront) { 114 | for (let edge of wavefront.root) 115 | this.commitSkeletonVertex(edge.start); 116 | 117 | if (!("caps" in this)) 118 | return this.commitFullWavefront(wavefront); 119 | 120 | // Find all the caps 121 | let caps = []; 122 | for (let edge of wavefront.root) 123 | if (edge.isCap) caps.push(edge); 124 | 125 | if (caps.length === 0) 126 | return this.commitFullWavefront(wavefront); 127 | 128 | // iterate forward, then backward, marking edges as 'dead' until one 129 | // intersects the cut, which gets split at the cutting point, or another 130 | // cap is reached 131 | for (let cap of caps) { 132 | let cut = cap.side === SkeletonEdge.StartCapEdge ? this.caps[0] : this.caps[2]; 133 | let current = cap; 134 | let dead = []; 135 | while(true) { 136 | let intersections = intersect(current.segment, cut); 137 | if (intersections.length !== 0) { 138 | current.cutsegment = new LineSegment(intersections[0].p, (current.cutsegment || current.segment).end); 139 | break; 140 | } 141 | 142 | if (!current.isCap) current.side = DeadEdge; 143 | dead.push(current); 144 | 145 | current = current.next; 146 | if (current.isCap) break; 147 | } 148 | 149 | cut = cap.side === SkeletonEdge.EndCapEdge ? this.caps[3] : this.caps[1]; 150 | current = cap; 151 | dead = []; 152 | while(true) { 153 | let intersections = intersect(current.segment, cut); 154 | if (intersections.length !== 0) { 155 | if (current.cutsegment) current.cutreverse = true; 156 | current.cutsegment = new LineSegment((current.cutsegment || current.segment).start, intersections[0].p); 157 | break; 158 | } 159 | 160 | if (!current.isCap) current.side = DeadEdge; 161 | dead.push(current); 162 | 163 | current = current.previous; 164 | if (current.isCap) break; 165 | } 166 | } 167 | 168 | for (let cap of caps) 169 | if (!cap.cutsegment) 170 | cap.side = DeadEdge; 171 | 172 | this.commitFullWavefront(wavefront); 173 | } 174 | 175 | commitFullWavefront(wavefront) { 176 | let side = null, pather = null, segment = null; 177 | var commit = () => { 178 | if (pather && side !== DeadEdge) 179 | this.waves.push({"path": pather.path, "side": side}); 180 | }; 181 | 182 | for (let edge of wavefront.root) { 183 | segment = edge.cutsegment || edge.segment; 184 | if (side !== edge.side) { 185 | commit(); 186 | side = edge.side; 187 | pather = new Pather(); 188 | 189 | if (edge.cutreverse) { 190 | pather.moveTo(vec2.clone(edge.start.position)); 191 | pather.lineTo(vec2.clone(edge.cutsegment.end)); 192 | commit(); 193 | pather = new Pather(); 194 | pather.moveTo(vec2.clone(edge.cutsegment.start)); 195 | } else { 196 | pather.moveTo(vec2.clone(segment.start)); 197 | } 198 | } 199 | if (edge.cutreverse) { 200 | pather.lineTo(vec2.clone(edge.segment.end)); 201 | } else { 202 | pather.lineTo(vec2.clone(segment.end)); 203 | } 204 | } 205 | commit(); 206 | } 207 | 208 | draw(context) { 209 | for (let wavefront of this.wavefronts) 210 | wavefront.draw(context); 211 | for (let spoke of this.spokes) 212 | spoke.draw(context); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /es6/skeleton/SkeletonCollapseEvent.js: -------------------------------------------------------------------------------- 1 | import Ray from '../primitives/Ray'; 2 | import Line from '../primitives/Line'; 3 | import * as Triangle from '../primitives/Triangle'; 4 | import { roughlyEqual, roughlyEqualVec2 } from '../missing-stuff'; 5 | import intersect from '../intersections/Intersections'; 6 | import vec2 from '../nd-linalg/Vector2'; 7 | import vec3 from '../nd-linalg/Vector3'; 8 | 9 | var id = 0; 10 | var infinity = Infinity; 11 | export default class SkeletonCollapseEvent { 12 | constructor(edge) { 13 | this.id = id++; 14 | this.edge = edge; 15 | 16 | let wavefront = edge.wavefront, 17 | intersections = intersect(edge.start.projection, edge.end.projection); 18 | 19 | if (intersections.length === 0) 20 | return this.time = infinity; 21 | 22 | this.position = intersections[0].p; 23 | this.time = edge.lengthAt(this.position) + wavefront.time; 24 | 25 | //if (this.time === 0) 26 | // return this.time = infinity; 27 | } 28 | 29 | name() { 30 | if (!this.edge.wavefront || this.edge.wavefront.length < 3) return "SkeletonCollapseEvent(dead)"; 31 | return this.edge.wavefront.length === 3 32 | ? "Triangle" 33 | : "Collapse"; 34 | } 35 | 36 | isValid() { 37 | if (!this.edge.wavefront) 38 | return false; 39 | 40 | // No events are allowed to happen immediately, that would require a self-intersecting source shape and that is not allowed for this algorithm. The only other way this can happen is with an open-path and no events should happen at instance-zero then either 41 | if (this.time === 0) 42 | return false; 43 | 44 | switch(this.edge.wavefront.length) { 45 | case 0: 46 | case 1: 47 | case 2: 48 | return false; 49 | case 3: 50 | return true; 51 | default: { 52 | // an edge cannot collapse if either of its projections are parallel to the edge 53 | const az = vec2.crossz(this.edge.lineDirection, this.edge.start.direction); 54 | const bz = vec2.crossz(this.edge.lineDirection, this.edge.end.direction); 55 | return !roughlyEqual(az,0) 56 | && !roughlyEqual(bz,0); 57 | } 58 | } 59 | } 60 | 61 | remove() { 62 | this.time = infinity; 63 | } 64 | 65 | description() { 66 | return `${this.name()}:${this.id} edge ${this.edge.id} at ${this.time}`; 67 | } 68 | 69 | process() { 70 | let wavefront = this.edge.wavefront; 71 | wavefront.processor.commitSkeletonVertex(this.edge.start); 72 | wavefront.processor.commitSkeletonVertex(this.edge.end); 73 | 74 | if (wavefront.length === 3) { 75 | let position = this.time === infinity ? this.edge.start.position : this.position; 76 | 77 | // commit the third vertex of the triangle 78 | wavefront.processor.commitSkeletonVertex(this.edge.next.end); 79 | 80 | // we know that -one- of the sides of the triangle has collapsed, but it's possible 81 | // the other two have not, they might now be parallel lines that overlap. 82 | // If that is the case, find one of the uncollapsed sides and connect it to the 83 | // triangle center 84 | let a = this.edge.start.position, 85 | b = this.edge.end.position, 86 | c = this.edge.next.end.position, 87 | center = Triangle.center(a, b, c); 88 | 89 | let start = a, end = b; 90 | if (roughlyEqualVec2(a, b)) { 91 | if (roughlyEqualVec2(b, c)) { 92 | start = c; 93 | end = a; 94 | } else { 95 | start = b; 96 | end = c; 97 | } 98 | } 99 | 100 | center = vec3(center[0], center[1], wavefront.time); 101 | start = vec3(start[0], start[1], wavefront.time); 102 | end = vec3(end[0], end[1], wavefront.time); 103 | wavefront.processor.commitSkeletonSpoke(start, center); 104 | wavefront.processor.commitSkeletonSpoke(center, end); 105 | 106 | wavefront.remove(); 107 | return true; 108 | } 109 | 110 | let previous = this.edge.previous, 111 | next = this.edge.next; 112 | 113 | // collapse this.edge 114 | this.edge.collapse(); 115 | 116 | // compute the direction and speed for the newly connected vertex 117 | next.start.computeDirectionAndSpeed(); 118 | 119 | // compute the collapse events for the neighbours 120 | previous.computeCollapseEvent(); 121 | next.computeCollapseEvent(); 122 | 123 | // compute the split events for the connected vertex which may now be obtuse 124 | next.start.computeSplitEvents(); 125 | 126 | return true; 127 | } 128 | 129 | draw(context) { 130 | if (this.wavefront.length === 3) { 131 | context.dot(this.position); 132 | return this.wavefront.draw(context); 133 | } 134 | 135 | this.edge.draw(context); 136 | context.dot(this.position); 137 | 138 | context.alpha = 0.25; 139 | 140 | this.edge.start.projection.draw(context); 141 | this.edge.end.projection.draw(context); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /es6/skeleton/SkeletonEdge.js: -------------------------------------------------------------------------------- 1 | var infinity = Infinity; 2 | import SkeletonVertex from './SkeletonVertex'; 3 | import SkeletonCollapseEvent from './SkeletonCollapseEvent'; 4 | import SkeletonSplitEvent from './SkeletonSplitEvent'; 5 | import vec2 from '../nd-linalg/Vector2'; 6 | import Chain from '../datastructures/Chain'; 7 | import Ray from '../primitives/Ray'; 8 | import Line from '../primitives/Line'; 9 | import intersect from '../intersections/Intersections'; 10 | import LineSegment from '../primitives/LineSegment'; 11 | 12 | export var InnerEdge = Symbol("InnerEdge"), 13 | OuterEdge = Symbol("OuterEdge"), 14 | StartCapEdge = Symbol("StartCapEdge"), 15 | EndCapEdge = Symbol("EndCapEdge"); 16 | 17 | export function create(path, isInfinite) { 18 | // console.log('isclosed', path.isClosed); 19 | return path.isClosed 20 | ? createClosedPath(path, isInfinite) 21 | : createOpenPath(path, isInfinite); 22 | } 23 | 24 | function createClosedPath(path, isInfinite) { 25 | let segments = path.segments, 26 | first = new SkeletonEdge(segments[0].start, InnerEdge), 27 | previous = first; 28 | for (let i = 1; i < segments.length; i++) { 29 | let next = new SkeletonEdge(segments[i].start, InnerEdge); 30 | connect([previous, next]); 31 | previous = next; 32 | } 33 | connect([previous, first]); 34 | //Chain.isBroken(first); 35 | 36 | if (isInfinite) return [first]; 37 | 38 | let second = new SkeletonEdge(segments[segments.length-1].end, OuterEdge); 39 | previous = second; 40 | for (let i = segments.length - 2; i >= 0; i--) { 41 | let next = new SkeletonEdge(segments[i].end, OuterEdge); 42 | connect([previous, next]); 43 | previous = next; 44 | } 45 | connect([previous, second]); 46 | //Chain.isBroken(second); 47 | 48 | return [first, second]; 49 | } 50 | 51 | function createOpenPath(path, isInfinite) { 52 | if (isInfinite) 53 | throw "Cannot propagate an open path wavefront infinitely"; 54 | 55 | let segments = path.segments, 56 | edges = new Array(segments.length * 2 + 2), 57 | i = 0; 58 | 59 | for (let segment of segments.slice().reverse()) 60 | edges[i++] = new SkeletonEdge(segment.end, OuterEdge); 61 | edges[i++] = new SkeletonEdge(segments[0].start, StartCapEdge); 62 | 63 | for (let segment of segments) 64 | edges[i++] = new SkeletonEdge(segment.start, InnerEdge); 65 | edges[i] = new SkeletonEdge(segments[segments.length-1].end, EndCapEdge); 66 | 67 | connect(edges); 68 | connect([edges[edges.length-1], edges[0]]); 69 | //Chain.isBroken(edges[0]); 70 | 71 | return [edges[0]]; 72 | } 73 | 74 | export function connect(edges) { 75 | if (edges.length < 2) 76 | throw "Need at least two edges to connect"; 77 | 78 | // connect the list of edges 79 | let previous = edges[0]; 80 | for (var i = 1; i < edges.length; i++) { 81 | let next = edges[i]; 82 | Chain.connect(previous, next); 83 | previous = next; 84 | } 85 | } 86 | 87 | export function isBroken(root) { 88 | let length = 0; 89 | Chain.isBroken(root); 90 | for (let edge of root) { 91 | length++; 92 | if (edge.start.nextEdge !== edge) 93 | throw "Edge vertex inconsistent"; 94 | if (edge.segment.start !== edge.start.position) 95 | throw "Edge segment.start inconsistent"; 96 | if (edge.segment.end !== edge.end.position) 97 | throw "Edge segment.end inconsistent"; 98 | } 99 | if (root.wavefront.length != length) 100 | throw "What?"; 101 | } 102 | 103 | var id = 0; 104 | class SkeletonEdge { 105 | constructor(start, side) { 106 | this.id = id++; 107 | this.side = side; 108 | this.start = new SkeletonVertex(start, this); 109 | this.wavefront = null; 110 | this.next = this; 111 | this.previous = this; 112 | this.isCap = this.side === StartCapEdge || this.side === EndCapEdge; 113 | 114 | this.direction = null; 115 | this.lineDirection = null; 116 | this.collapseEvent = null; 117 | } 118 | 119 | get name() { return "SkeletonEdge"; } 120 | get end() { return this.next.start; } 121 | get segment() { return new LineSegment(this.start.position, this.end.position); } 122 | get line() { return new Line(this.start.position, this.lineDirection); } 123 | 124 | computeDirection() { 125 | let startPosition = this.start.position, 126 | endPosition = this.end.position; 127 | 128 | if (this.isCap && 129 | startPosition[0] == endPosition[0] && 130 | startPosition[1] == endPosition[1]) { 131 | let previousDirection = this.previous.direction; 132 | this.direction = vec2(previousDirection[1], -previousDirection[0]); 133 | this.lineDirection = vec2.clone(previousDirection); 134 | return; 135 | } 136 | 137 | this.lineDirection = vec2.sub(vec2(0, 0), endPosition, startPosition); 138 | vec2.normalize(this.lineDirection, this.lineDirection); 139 | this.direction = vec2(-this.lineDirection[1], this.lineDirection[0]); 140 | } 141 | 142 | checkSanity() { 143 | let sane = false; 144 | let ray = new Ray(this.segment.midpoint, this.direction); 145 | for (let edge of this.next) { 146 | if (edge !== this) { 147 | if (intersect(edge.segment, ray).length > 0) { 148 | sane = true; 149 | break; 150 | } 151 | } 152 | } 153 | if (!sane) throw "Hangon..."; 154 | } 155 | 156 | checkOverlap() { 157 | if (roughlyEqual(this.segment.length, 0)) return; 158 | intersect(this.previous.segment, this.next.segment).length > 0 && thisshouldnotbehappening(); 159 | } 160 | 161 | computeCollapseEvent() { 162 | this.collapseEvent = new SkeletonCollapseEvent(this); 163 | } 164 | 165 | computeSplitEvents() { 166 | let edge = this.next.next.next, 167 | end = this.previous; 168 | 169 | while(true) { 170 | if (edge === end) break; 171 | let vertex = edge.start; 172 | vertex.isAcute || vertex.events.push(new SkeletonSplitEvent(this, vertex)); 173 | edge = edge.next; 174 | } 175 | } 176 | 177 | * [Symbol.iterator]() { 178 | var current = this; 179 | while(true) { 180 | yield current; 181 | current = current.next; 182 | if (current === this) break; 183 | } 184 | } 185 | 186 | // At a position p, assuming p is between start.projection and end.projection 187 | // return how long it will be before the edge reaches p 188 | lengthAt(p) { 189 | var measuring = vec2(-this.direction[0], -this.direction[1]); 190 | var intersections = intersect(new Ray(p, measuring), this.line); 191 | 192 | // If the point is behind the moving wavefront, return an infinite length 193 | return intersections.length === 0 ? infinity : intersections[0].u; 194 | } 195 | 196 | projectBy(amount) { 197 | return new LineSegment(this.start.projectBy(amount), this.end.projectBy(amount)); 198 | } 199 | 200 | collapse() { 201 | // if we're the root edge, cycle to another edge 202 | if (this.wavefront.root === this) 203 | this.wavefront.root = this.next; 204 | 205 | let previous = this.previous, 206 | next = this.next; 207 | 208 | connect([previous, next]); 209 | this.wavefront.length--; 210 | //isBroken(previous); 211 | 212 | // delete the edge and therefore any split/cut events for this edge 213 | this.wavefront = null; 214 | } 215 | 216 | split(position) { 217 | // if we're the root edge, cycle to another edge 218 | if (this.wavefront.root === this) 219 | this.wavefront.root = this.next; 220 | 221 | var previous = this.previous, 222 | middle1 = new SkeletonEdge(this.start.position, previous.side), 223 | middle2 = new SkeletonEdge(position, previous.side), 224 | next = this.next; 225 | 226 | // keep the current start 227 | middle1.start = this.start; 228 | middle1.start.nextEdge = middle1; 229 | 230 | // Copy the properties 231 | middle1.wavefront = this.wavefront; 232 | middle2.wavefront = this.wavefront; 233 | middle1.direction = vec2.clone(this.direction); 234 | middle2.direction = vec2.clone(this.direction); 235 | middle1.lineDirection = vec2.clone(this.lineDirection); 236 | middle2.lineDirection = vec2.clone(this.lineDirection); 237 | 238 | // Quick set the new vertex 239 | middle2.start.isAcute = false; 240 | middle2.start.isParallel = true; 241 | middle2.start.speed = 1; 242 | middle2.start.direction = vec2.clone(this.direction); 243 | middle2.start.projection = new Ray(middle2.start.position, middle2.start.direction); 244 | 245 | connect([previous, middle1, middle2, next]); 246 | this.wavefront.length++; 247 | //isBroken(previous); 248 | 249 | // delete the edge and therefore any split/cut events for this edge 250 | this.wavefront = null; 251 | 252 | return [middle1, middle2]; 253 | } 254 | 255 | draw(context) { 256 | if (!this.wavefront) return; 257 | if (this.cutreverse) { 258 | context.stroke(new LineSegment(this.segment.start, this.cutsegment.end)); 259 | context.stroke(new LineSegment(this.cutsegment.start, this.segment.end)); 260 | } else { 261 | context.stroke(this.cutsegment || this.segment); 262 | //context.arrowhead(this.segment.end, this.lineDirection); 263 | } 264 | context.dot(this.start.position); 265 | context.fontsize = 13; 266 | context.textBaseline = "center"; 267 | context.textAlign = "center"; 268 | context.text(this.id, this.segment.midpoint); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /es6/skeleton/SkeletonSplitEvent.js: -------------------------------------------------------------------------------- 1 | import Ray from '../primitives/Ray'; 2 | import Line from '../primitives/Line'; 3 | import intersect from '../intersections/Intersections'; 4 | import vec2 from '../nd-linalg/Vector2'; 5 | import vec3 from '../nd-linalg/Vector3'; 6 | import { roughlyEqual } from '../missing-stuff'; 7 | import * as SkeletonEdge from './SkeletonEdge'; 8 | import SkeletonWavefront from './SkeletonWavefront'; 9 | 10 | var id = 0; 11 | var infinity = Infinity; 12 | var ROUGHLY_EPSILON = Number.ROUGHLY_EPSILON; 13 | export default class SkeletonSplitEvent { 14 | constructor(edge, vertex) { 15 | this.id = id++; 16 | this.edge = edge; 17 | this.vertex = vertex; 18 | 19 | // if (this.edge.isSelfIntersectionCap) 20 | // return this.time = infinity; 21 | 22 | let intersections = intersect(vertex.projection, edge.line); 23 | if (intersections.length == 0) 24 | return this.time = infinity; 25 | 26 | // Find the bisector(s) 27 | let projectionToEdgePosition = intersections[0].p; 28 | intersections = intersect(edge.line, vertex.previousEdge.line); 29 | if (intersections.length == 0) { 30 | // the two edges are parallel to each other, find the midpoint between them for the bisector 31 | let midpoint = vec2.lerp(vec2(0, 0), edge.start.position, vertex.previousEdge.start.position, 0.5); 32 | var bisector = new Line(midpoint, vertex.previousEdge.lineDirection); 33 | } else { 34 | let edgeToEdgePosition = intersections[0].p, 35 | dir1 = vec2.sub(vec2(0, 0), vertex.position, edgeToEdgePosition), 36 | dir2 = vec2.sub(vec2(0, 0), projectionToEdgePosition, edgeToEdgePosition); 37 | 38 | vec2.normalize(dir1, dir1); 39 | vec2.normalize(dir2, dir2); 40 | let direction = vec2.add(vec2(0, 0), dir1, dir2); 41 | vec2.normalize(direction, direction); 42 | var bisector = new Ray(edgeToEdgePosition, direction); 43 | } 44 | 45 | // Find the point of split 46 | intersections = intersect(vertex.projection, bisector); 47 | if (intersections.length === 0) 48 | return this.time = infinity; 49 | 50 | // How far from the vertex to the bisector 51 | let length = edge.lengthAt(intersections[0].p); 52 | if (length < 0 || length === infinity) 53 | return this.time = infinity; 54 | 55 | this.time = length + this.edge.wavefront.time; 56 | //Drawing2D.log("created " + this.description(), [this.edge.wavefront, this, {colour: "pink", legend: "bisector", visuals: [bisector]}]); 57 | } 58 | 59 | name() { 60 | if (!this.edge.wavefront || this.edge.wavefront != this.vertex.wavefront) 61 | return "Split(dead)"; 62 | let projected = this.edge.projectBy(this.time - this.edge.wavefront.time), 63 | position = this.vertex.projectBy(this.time - this.edge.wavefront.time); 64 | let distance1 = vec2.dist(this.edge.start.position, this.vertex.position), 65 | distance2 = vec2.dist(this.edge.end.position, this.vertex.position); 66 | if (roughlyEqual(distance1, 0)) 67 | return "Cut.start"; 68 | if (roughlyEqual(distance2, 0)) 69 | return "Cut.end"; 70 | else 71 | return "Split"; 72 | } 73 | 74 | isValid() { 75 | // The event must be on an existing wavefront and both the edge and vertex must also be on the same wavefront 76 | if (!this.edge.wavefront || this.edge.wavefront != this.vertex.wavefront) 77 | return false; 78 | 79 | // No events are allowed to happen immediately, that would require a self-intersecting source shape and that is not allowed for this algorithm. The only other way this can happen is with an open-path and no events should happen at instance-zero then either 80 | if (this.time === 0) 81 | return false; 82 | 83 | // No split or cut can happen with < 4 edges 84 | if (this.edge.wavefront.length < 4) 85 | return false; 86 | 87 | // Is the vertex still acute? 88 | if (this.vertex.isAcute) 89 | return false; 90 | 91 | // Is the event time infinite? 92 | if (this.time === infinity) 93 | return false; 94 | 95 | // Is the event actually happening? 96 | let futureEdge = this.edge.projectBy(this.time - this.edge.wavefront.time), 97 | futureVertex = this.vertex.projectBy(this.time - this.edge.wavefront.time); 98 | return futureEdge.roughlyContainsPoint(futureVertex); 99 | } 100 | 101 | remove() { 102 | const _this = this; 103 | this.vertex.events = this.vertex.events.filter( function( ele ){ 104 | return ele !== _this; 105 | }); 106 | // this.vertex.events = this.vertex.events.without(this); 107 | } 108 | 109 | description() { 110 | return `${this.name()}:${this.id} edge ${this.edge.id} from between ${this.vertex.previousEdge.id} and ${this.vertex.nextEdge.id} at ${this.time}`; 111 | } 112 | 113 | process() { 114 | let distance1 = vec2.dist(this.edge.start.position, this.vertex.position); 115 | if (roughlyEqual(distance1, 0)) 116 | return cut(this.edge.start, this.vertex); 117 | 118 | let distance2 = vec2.dist(this.edge.end.position, this.vertex.position); 119 | if (roughlyEqual(distance2, 0)) 120 | return cut(this.edge.end, this.vertex); 121 | 122 | // The vertex.position was roughly contained within the edge, but not close 123 | // enough to have a roughly close distance to either end and when we test 124 | // if it's really contained and it is not, then it must still be treated as 125 | // a cut 126 | if (!this.edge.segment.boundingBox.containsPoint(this.vertex.position)) 127 | return distance1 < distance2 128 | ? cut(this.edge.start, this.vertex) 129 | : cut(this.edge.end, this.vertex); 130 | 131 | return split(this.edge, this.vertex); 132 | } 133 | 134 | draw(context) { 135 | context.style = "#F00"; 136 | context.legend("this.edge"); 137 | this.edge.draw(context); 138 | 139 | context.style = "#F66"; 140 | context.legend("this.edge - future"); 141 | this.edge.projectBy(this.time - this.edge.wavefront.time).draw(context); 142 | 143 | context.style = "#00F"; 144 | context.legend("this.vertex"); 145 | this.vertex.previousEdge.draw(context); 146 | this.vertex.nextEdge.draw(context); 147 | 148 | context.style = "#66F"; 149 | context.legend("this.vertex - future"); 150 | this.vertex.previousEdge.projectBy(this.time - this.edge.wavefront.time).draw(context); 151 | this.vertex.nextEdge.projectBy(this.time - this.edge.wavefront.time).draw(context); 152 | 153 | context.style = "rgba(0, 0, 255, 0.25)"; 154 | context.legend("this.vertex - projection"); 155 | this.vertex.projection.draw(context); 156 | 157 | context.style = "#300"; 158 | context.legend("intersection"); 159 | context.dot(this.vertex.projectBy(this.time - this.edge.wavefront.time)); 160 | } 161 | } 162 | 163 | function join(previous, next, length) { 164 | // find the point at which these two edges connect and 'extend' or 'contract' the edges until they meet to preserve the direction of both edges 165 | let intersections = intersect(previous.line, next.line), 166 | moved = intersections.length > 0; 167 | if (moved) { 168 | next.start.position = intersections[0].p; 169 | next.wavefront.processor.commitSkeletonVertex(next.start); 170 | } 171 | 172 | // connect the edges 173 | SkeletonEdge.connect([previous, next]); 174 | 175 | // did we twist an edge trying to do this, if so, try again skipping the twisted edge 176 | if (moved) { 177 | let twist1 = intersect(previous.previous.segment, next.segment); 178 | if (twist1.length > 0 && !twist1[0].isDegenerate) 179 | return join(previous.previous, next, length + 1); 180 | let twist2 = intersect(previous.segment, next.next.segment); 181 | if (twist2.length > 0 && !twist2[0].isDegenerate) 182 | return join(previous, next.next, length + 1); 183 | } 184 | 185 | // compute direction and speed 186 | next.start.computeDirectionAndSpeed(); 187 | 188 | // compute collapse events 189 | previous.computeCollapseEvent(); 190 | next.computeCollapseEvent(); 191 | 192 | // compute split events 193 | next.start.computeSplitEvents(); 194 | 195 | return [previous, next, length]; 196 | } 197 | 198 | function cut(edgeVertex, cutVertex) { 199 | if (edgeVertex === cutVertex) 200 | return false; 201 | 202 | edgeVertex.wavefront.processor.commitSkeletonVertex(edgeVertex); 203 | cutVertex.wavefront.processor.commitSkeletonVertex(cutVertex); 204 | 205 | let wavefront0 = edgeVertex.wavefront, 206 | wavefront1 = null, 207 | previous0 = edgeVertex.previousEdge, 208 | previous1 = cutVertex.previousEdge, 209 | next0 = cutVertex.nextEdge, 210 | next1 = edgeVertex.nextEdge, 211 | length0 = 0, 212 | length1 = 0; 213 | 214 | [previous0, next0, length0] = join(previous0, next0, length0); 215 | [previous1, next1, length1] = join(previous1, next1, length1); 216 | 217 | // create the new wavefront 218 | wavefront0.root = previous0; 219 | wavefront1 = new SkeletonWavefront( 220 | wavefront0.processor, 221 | previous1, 222 | wavefront0.time); 223 | wavefront0.length -= wavefront1.length + length0 + length1; 224 | 225 | //SkeletonEdge.isBroken(previous0); 226 | //SkeletonEdge.isBroken(previous1); 227 | 228 | // it's possible at this point that one of the two wavefronts 229 | // has < 3 vertices. If that's the case, we might as well 230 | // delete it now and add its spokes, otherwise make sure the 231 | // two wavefronts exist in the processor 232 | function endit(wavefront) { 233 | wavefront.processor.commitSkeletonVertex(wavefront.root.start); 234 | wavefront.processor.commitSkeletonVertex(wavefront.root.end); 235 | wavefront.processor.commitSkeletonSpoke( 236 | vec3(wavefront.root.start.position[0], wavefront.root.start.position[1], wavefront.time), 237 | vec3(wavefront.root.end.position[0], wavefront.root.end.position[1], wavefront.time)); 238 | } 239 | if (wavefront0.length < 3) { 240 | endit(wavefront0); 241 | wavefront0.remove(); 242 | } else { 243 | // wavefront[0] already exists in the processor 244 | } 245 | if (wavefront1.length < 3) { 246 | endit(wavefront1) 247 | wavefront1.root.next.wavefront = null; 248 | wavefront1.root.wavefront = null; 249 | } else { 250 | wavefront1.processor.addWavefront(wavefront1); 251 | } 252 | return true; 253 | } 254 | 255 | function split(edge, vertex) { 256 | let pair = edge.split(vertex.position); 257 | 258 | // compute direction and speed 259 | pair[1].start.computeDirectionAndSpeed(); 260 | 261 | // compute collapse events 262 | // SKIPPED: cut will do it 263 | 264 | // compute split events for the two halves 265 | // pair[1].start is not acute and we do not have to compute its split events 266 | pair[0].computeSplitEvents(); 267 | pair[1].computeSplitEvents(); 268 | 269 | return cut(pair[1].start, vertex); 270 | } 271 | -------------------------------------------------------------------------------- /es6/skeleton/SkeletonVertex.js: -------------------------------------------------------------------------------- 1 | import vec2 from '../nd-linalg/Vector2'; 2 | import vec3 from '../nd-linalg/Vector3'; 3 | import { roughlyEqual } from '../missing-stuff'; 4 | import Ray from '../primitives/Ray'; 5 | import Line from '../primitives/Line'; 6 | import intersect from '../intersections/Intersections'; 7 | import SkeletonSplitEvent from './SkeletonSplitEvent'; 8 | 9 | var id = 0; 10 | export default class SkeletonVertex { 11 | constructor(position, nextEdge) { 12 | if (!isFinite(position[0]) || !isFinite(position[1])) 13 | throw "Bad vertex"; 14 | 15 | this.id = id++; 16 | this.position = vec2.clone(position); 17 | this.beginning = vec3(position[0], position[1], 0); 18 | this.nextEdge = nextEdge; 19 | 20 | this.events = []; 21 | this.isParallel = null; 22 | this.isAcute = null; 23 | this.direction = null; 24 | this.projection = null; 25 | this.speed = null; 26 | } 27 | 28 | get name() { return "SkeletonVertex"; } 29 | get wavefront() { return this.nextEdge.wavefront; } 30 | get previousEdge() { return this.nextEdge.previous; } 31 | get next() { return this.nextEdge.end; } 32 | get previous() { return this.previousEdge.start; } 33 | 34 | computeDirectionAndSpeed() { 35 | // if direction and speed change, any split/cut events for this vertex 36 | // are now invalid 37 | this.events = []; 38 | 39 | let position = this.position; 40 | let nextEdge = this.nextEdge; 41 | let previousEdge = this.previousEdge; 42 | let orientation = vec2.crossz(previousEdge.direction, nextEdge.direction); 43 | let isParallel = this.isParallel = roughlyEqual( vec2.crossz(previousEdge.direction, nextEdge.direction), 0 ); 44 | this.isAcute = isParallel || orientation > 0; 45 | 46 | // direction & projection 47 | let direction = this.direction = vec2.add(vec2(0, 0), previousEdge.direction, nextEdge.direction); 48 | vec2.normalize(direction, direction); 49 | this.projection = new Ray(position, direction); 50 | 51 | // speed 52 | if (isParallel) return this.speed = 1; 53 | let previousLine = new Line( 54 | vec2.add(vec2(0, 0), position, previousEdge.lineDirection), 55 | previousEdge.direction), 56 | nextLine = new Line( 57 | vec2.add(vec2(0, 0), position, nextEdge.lineDirection), 58 | nextEdge.direction), 59 | intersections = intersect(previousLine, nextLine); 60 | 61 | if (intersections.length === 0) 62 | return this.speed = 0; 63 | 64 | this.speed = vec2.dist(position, intersections[0].p); 65 | } 66 | 67 | computeSplitEvents() { 68 | this.events = []; 69 | if (this.isAcute) return; 70 | 71 | let start = this.nextEdge.next.next, 72 | end = this.previousEdge; 73 | while(true) { 74 | if (start === end) break; 75 | this.events.push(new SkeletonSplitEvent(start, this)); 76 | start = start.next; 77 | } 78 | } 79 | 80 | movementBy(amount) { 81 | return vec2.scale(vec2(0, 0), this.direction, this.speed * amount); 82 | } 83 | 84 | projectBy(amount) { 85 | return vec2.add(vec2(0, 0), this.position, this.movementBy(amount)); 86 | } 87 | 88 | move(amount) { 89 | vec2.add(this.position, this.position, this.movementBy(amount)); 90 | } 91 | 92 | draw(context) { 93 | context.dot(this.position); 94 | 95 | context.style = "#AAF"; 96 | this.projection && this.projection.draw(context); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /es6/skeleton/SkeletonWavefront.js: -------------------------------------------------------------------------------- 1 | import { roughlyEqual } from '../missing-stuff'; 2 | 3 | const TIME_EPSILON = 1e-8; 4 | var max = Math.max; 5 | var infinity = Infinity; 6 | var weakmap = WeakMap; 7 | 8 | var id = 0; 9 | export default class SkeletonWavefront { 10 | constructor(processor, root, time) { 11 | this.id = id++; 12 | this.processor = processor; 13 | this.root = root; 14 | this.time = time; 15 | 16 | // take ownership of edges and compute length 17 | var length = 0; 18 | for (let edge of root) { 19 | length++; 20 | edge.wavefront = this; 21 | } 22 | this.length = length; 23 | } 24 | 25 | initialise() { 26 | // compute initial edge direction 27 | let edge = this.root; 28 | while(true) { 29 | edge.computeDirection(); 30 | edge = edge.next; 31 | if (edge === this.root) break; 32 | } 33 | 34 | // compute initial vertex direction and speed 35 | while(true) { 36 | edge.start.computeDirectionAndSpeed(); 37 | edge = edge.next; 38 | if (edge === this.root) break; 39 | } 40 | 41 | // compute initial collapse events 42 | while(true) { 43 | edge.computeCollapseEvent(); 44 | edge = edge.next; 45 | if (edge === this.root) break; 46 | } 47 | 48 | // compute initial cut/split events 49 | while(true) { 50 | edge.computeSplitEvents(); 51 | edge = edge.next; 52 | if (edge === this.root) break; 53 | } 54 | 55 | return this; 56 | } 57 | 58 | process(maximum) { 59 | let events = []; 60 | while(true) { 61 | // compute events 62 | var nextEvents = this.nextEvents(); 63 | 64 | // shortcut if we're done processing 65 | if (nextEvents.events.length === 0 || nextEvents.time > maximum) { 66 | this.move(maximum); 67 | return false; 68 | } 69 | 70 | // filter events 71 | for (let i = 0; i < nextEvents.events.length; i++) { 72 | let event = nextEvents.events[i]; 73 | if (!event.isValid()) 74 | event.remove(); 75 | else 76 | events.push(event); 77 | } 78 | if (events.length > 0) 79 | break; 80 | 81 | events = []; 82 | } 83 | 84 | // process events 85 | this.move(nextEvents.time); 86 | for (let i = 0; i < nextEvents.events.length; i++) { 87 | let event = nextEvents.events[i]; 88 | event.isValid() && event.process(); 89 | event.remove(); 90 | } 91 | 92 | return true; 93 | } 94 | 95 | debugprocess(maximum) { 96 | console.group("step"); 97 | Drawing2D.log("before", this); 98 | 99 | let events; 100 | let depth = 0; 101 | while(true) { 102 | depth++; 103 | if (depth == 100) debugger; 104 | 105 | // compute events 106 | var nextEvents = this.nextEvents(); 107 | 108 | // shortcut if we're done processing 109 | if (nextEvents.events.length === 0 || nextEvents.time > maximum) { 110 | this.move(maximum); 111 | Drawing2D.log("after move", this); 112 | console.groupEnd("step"); 113 | return false; 114 | } 115 | 116 | // filter events 117 | events = []; 118 | for (let i = 0; i < nextEvents.events.length; i++) { 119 | let event = nextEvents.events[i]; 120 | if (!event.isValid()) { 121 | console.log("removing", event.description()); 122 | event.remove(); 123 | } 124 | else 125 | events.push(event); 126 | } 127 | if (events.length > 0) 128 | break; 129 | } 130 | 131 | // process events 132 | this.move(nextEvents.time); 133 | Drawing2D.log("after move", this); 134 | for (let event of events) { 135 | if (event.isValid()) { 136 | console.log(event.description()); 137 | event.process(); 138 | this.processor.wavefronts.length > 0 && Drawing2D.log("after event", this.processor.wavefronts.concat([{colour: "#F0F", visuals: this.processor.spokes}])); 139 | } else { 140 | console.log("late removal of", event.description()); 141 | } 142 | event.remove(); 143 | } 144 | 145 | console.groupEnd("step"); 146 | return true; 147 | } 148 | 149 | remove() { 150 | this.length = 0; 151 | for (let edge of this.root) 152 | edge.wavefront = null; 153 | this.root = null; 154 | this.processor.removeWavefront(this); 155 | } 156 | 157 | nextEvents() { 158 | var events = [], 159 | time = infinity, 160 | maxtime = -infinity; 161 | 162 | var wavefront = this; 163 | function testEvent(event) { 164 | let eventTime = event.time; 165 | if (eventTime < wavefront.time) 166 | throw "Time machine?"; 167 | if (eventTime < time) { 168 | //console.log("testing", event.description(), "<", time); 169 | let old = events; 170 | events = [event]; 171 | maxtime = time = eventTime; 172 | for (let i = 0; i < old.length; i++) 173 | testEvent(old[i]); 174 | } else if (eventTime < infinity && roughlyEqual(eventTime, time, TIME_EPSILON)) { 175 | //console.log("testing", event.description(), "<", "~" + time); 176 | events.push(event); 177 | maxtime = max(maxtime, eventTime); 178 | } else { 179 | //console.log("testing", event.description(), "skipped"); 180 | return false; 181 | } 182 | return true; 183 | } 184 | 185 | let edge = this.root; 186 | while(true) { 187 | testEvent(edge.collapseEvent); 188 | let vertexEvents = edge.start.events; 189 | for (let i = 0; i < vertexEvents.length; i++) 190 | testEvent(vertexEvents[i]); 191 | 192 | edge = edge.next; 193 | if (edge === this.root) break; 194 | } 195 | 196 | if (time === infinity) 197 | maxtime = infinity; 198 | 199 | events.sort((a, b) => b - a); 200 | return {"time": maxtime, "events": events}; 201 | } 202 | 203 | move(time) { 204 | let delta = time - this.time; 205 | let edge = this.root; 206 | while(true) { 207 | edge.start.move(delta); 208 | edge = edge.next; 209 | if (edge === this.root) break; 210 | } 211 | this.time = time; 212 | } 213 | 214 | toPath() { 215 | var pather = new Pather(); 216 | pather.moveTo(vec2.clone(this.root.start.position)); 217 | for (let edge of this.root) 218 | pather.lineTo(vec2.clone(edge.end.position)); 219 | return pather.path; 220 | } 221 | 222 | draw(context) { 223 | if (!this.root) return; 224 | var dimmer = context.clone(); 225 | dimmer.alpha = 0.2; 226 | for (let edge of this.root) { 227 | edge.draw(context); 228 | //LineSegment.project(edge.start.position, edge.start.direction, edge.start.speed / 10).draw(dimmer); 229 | new Ray(edge.segment.midpoint, edge.direction).draw(dimmer); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import * as CompGeo from '../es6/index'; 4 | import * as TestPath from './testpath'; 5 | 6 | const createOrbitViewer = require('three-orbit-viewer')(THREE); 7 | 8 | const app = createOrbitViewer({ 9 | clearColor: 0x181819, 10 | clearAlpha: 1.0, 11 | fov: 65, 12 | position: new THREE.Vector3(1, 1, -2) 13 | }); 14 | 15 | const lightGroup = new THREE.Group(); 16 | app.scene.add( lightGroup ); 17 | 18 | const light1 = new THREE.PointLight( 0xfffcff, 1 ); 19 | light1.position.set( 0, 5, 0 ); 20 | const light2 = new THREE.PointLight( 0xfdffff, 0.6 ); 21 | light2.position.set( -2, -2, 0 ); 22 | const light3 = new THREE.PointLight( 0xffffab, 0.4 ); 23 | light3.position.set( 2, -2, 1 ); 24 | lightGroup.add( light1, light2, light3 ); 25 | 26 | let meshGroup = new THREE.Group(); 27 | 28 | function generate( pathFunc ){ 29 | app.scene.remove( meshGroup ); 30 | 31 | meshGroup = new THREE.Group(); 32 | meshGroup.rotation.x = -Math.PI * 0.5; 33 | app.scene.add( meshGroup ); 34 | 35 | const path = pathFunc(); 36 | const lineGeometry = segmentsToGeometry( path.segments ); 37 | const line = new THREE.LineSegments( lineGeometry, new THREE.LineBasicMaterial({color:0xffffff}) ); 38 | meshGroup.add( line ); 39 | 40 | const skeleton = new CompGeo.Skeleton( path, Infinity ); 41 | const skeletonPath = new CompGeo.shapes.Path( skeleton.spokes ); 42 | const skeletonGeometry = segmentsToGeometry( skeletonPath.segments ); 43 | const skeletonLine = new THREE.LineSegments( skeletonGeometry, new THREE.LineBasicMaterial({color:0xff0000}) ); 44 | meshGroup.add( skeletonLine ); 45 | 46 | const shape = new CompGeo.shapes.Shape( path.concat( skeletonPath ) ); 47 | const geometry = shape.triangulate(); 48 | const mat = new THREE.MeshStandardMaterial({ wireframe: false, color: 0xffffff }); 49 | const mesh = new THREE.Mesh( geometry, mat ); 50 | geometry.computeFaceNormals(); 51 | meshGroup.add( mesh ); 52 | 53 | meshGroup.add( new THREE.Points( geometry, new THREE.PointsMaterial({size:0.02}) ) ); 54 | 55 | } 56 | 57 | const pathList = Object.keys( TestPath ); 58 | let pathIndex = 0; 59 | generate( TestPath[ pathList[ pathIndex ] ] ); 60 | 61 | window.addEventListener('keydown', function(){ 62 | pathIndex++; 63 | if( pathIndex >= pathList.length ){ 64 | pathIndex = 0; 65 | } 66 | generate( TestPath[ pathList[ pathIndex ] ] ); 67 | }, true) 68 | ; 69 | 70 | function gameShapeToPath( shape ){ 71 | // game shape here is an array of array of {x,y,z}s 72 | const first = shape[0][0]; 73 | const pather = new CompGeo.shapes.Pather( [first.x, first.y] ); 74 | shape.forEach( function( path ){ 75 | path.forEach( function( vertex, vi ){ 76 | if( vi === 0 ){ 77 | return; 78 | } 79 | pather.lineTo( [ vertex.x, vertex.y ] ); 80 | }); 81 | pather.close(); 82 | pather.moveTo( path[ 0 ].x, path[ 0 ].y ); 83 | }); 84 | return pather.path; 85 | } 86 | 87 | 88 | function segmentsToGeometry( segments ){ 89 | return segments.reduce( function( geometry, { start, end } ){ 90 | geometry.vertices.push( new THREE.Vector3( start[ 0 ], start[ 1 ], start[ 2 ] ), new THREE.Vector3( end[ 0 ], end[ 1 ], end[ 2 ] ) ); 91 | return geometry; 92 | }, new THREE.Geometry() ) 93 | } 94 | 95 | app.on('tick', function(){ 96 | if( meshGroup ){ 97 | // meshGroup.rotation.z += 0.01; 98 | } 99 | }); -------------------------------------------------------------------------------- /example/testpath.js: -------------------------------------------------------------------------------- 1 | import * as CompGeo from '../es6/index'; 2 | 3 | export function h(){ 4 | const sh = [ 5 | [ 6 | { 7 | "x": 5, 8 | "y": 2, 9 | "z": 0 10 | }, 11 | { 12 | "x": 4, 13 | "y": 2, 14 | "z": 0 15 | }, 16 | { 17 | "x": 4, 18 | "y": 3, 19 | "z": 0 20 | }, 21 | { 22 | "x": 5, 23 | "y": 3, 24 | "z": 0 25 | }, 26 | { 27 | "x": 5, 28 | "y": 4, 29 | "z": 0 30 | }, 31 | { 32 | "x": 2, 33 | "y": 4, 34 | "z": 0 35 | }, 36 | { 37 | "x": 2, 38 | "y": 3, 39 | "z": 0 40 | }, 41 | { 42 | "x": 3, 43 | "y": 3, 44 | "z": 0 45 | }, 46 | { 47 | "x": 3, 48 | "y": 2, 49 | "z": 0 50 | }, 51 | { 52 | "x": 2, 53 | "y": 2, 54 | "z": 0 55 | }, 56 | { 57 | "x": 2, 58 | "y": 1, 59 | "z": 0 60 | }, 61 | { 62 | "x": 5, 63 | "y": 1, 64 | "z": 0 65 | } 66 | ] 67 | ]; 68 | 69 | return gameShapeToCompGeoPath( sh.map( (path)=>path.map( (v)=>({x:v.x*0.25,y:v.y*0.25,z:v.z*0.25}))) ); 70 | } 71 | 72 | function gameShapeToCompGeoPath( shape ){ 73 | const first = shape[0][0]; 74 | const pather = new CompGeo.shapes.Pather( [first.x, first.y] ); 75 | shape.forEach( function( path, pathIndex ){ 76 | path.forEach( function( vertex, vi ){ 77 | if( vi === 0 ){ 78 | return; 79 | } 80 | pather.lineTo( [ vertex.x, vertex.y ] ); 81 | }); 82 | pather.close(); 83 | 84 | const nextPath = shape[ pathIndex + 1 ]; 85 | if( nextPath ){ 86 | pather.moveTo( nextPath[ 0 ].x, nextPath[ 0 ].y ); 87 | } 88 | }); 89 | 90 | // console.log( JSON.stringify( pather.path, null, 2 ) ); 91 | return pather.path; 92 | } 93 | 94 | export function convex() { 95 | return new CompGeo.shapes.Pather([0, 0.25]) 96 | .lineTo([0.1, 0]) 97 | .lineTo([0.7, 0.15]) 98 | .lineTo([1, 0.6]) 99 | .lineTo([0.75, 0.75]) 100 | .lineTo([0.55, 0.8]) 101 | .lineTo([0.25, 0.85]) 102 | .close(); 103 | } 104 | 105 | export function concave() { 106 | return new CompGeo.shapes.Pather([0, 0.25]) 107 | .lineTo([0.1, 0]) 108 | .lineTo([0.7, 0.15]) 109 | .lineTo([1, 0.6]) 110 | .lineTo([0.75, 0.75]) 111 | .lineTo([0.55, 0.6]) 112 | .lineTo([0.25, 0.85]) 113 | .close(); 114 | } 115 | 116 | export function square() { 117 | return new CompGeo.shapes.Pather() 118 | .lineTo([1, 0]) 119 | .lineTo([1, 1]) 120 | .lineTo([0, 1]) 121 | .close(); 122 | } 123 | 124 | export function box() { 125 | return new CompGeo.shapes.Pather([0.25, 0]) 126 | .lineTo([0.75, 0]) 127 | .lineTo([0.75, 1]) 128 | .lineTo([0.25, 1]) 129 | .close(); 130 | } 131 | 132 | export function plus() { 133 | return new CompGeo.shapes.Pather([0, 1 / 3]) 134 | .lineTo([1 / 3, 1 / 3]) 135 | .lineTo([1 / 3, 0]) 136 | .lineTo([2 / 3, 0]) 137 | .lineTo([2 / 3, 1 / 3]) 138 | .lineTo([1, 1 / 3]) 139 | .lineTo([1, 2 / 3]) 140 | .lineTo([2 / 3, 2 / 3]) 141 | .lineTo([2 / 3, 1]) 142 | .lineTo([1 / 3, 1]) 143 | .lineTo([1 / 3, 2 / 3]) 144 | .lineTo([0, 2 / 3]) 145 | .close(); 146 | } 147 | 148 | export function l() { 149 | return new CompGeo.shapes.Pather() 150 | .lineTo([0.5, 0]) 151 | .lineTo([0.5, 0.5]) 152 | .lineTo([1, 0.5]) 153 | .lineTo([1, 1]) 154 | .lineTo([0, 1]) 155 | .close(); 156 | } 157 | 158 | export function u() { 159 | return new CompGeo.shapes.Pather() 160 | .lineTo([1 / 3, 0]) 161 | .lineTo([1 / 3, 2 / 3]) 162 | .lineTo([2 / 3, 2 / 3]) 163 | .lineTo([2 / 3, 0]) 164 | .lineTo([1, 0]) 165 | .lineTo([1, 1]) 166 | .lineTo([0, 1]) 167 | .close(); 168 | } 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comp-geo", 3 | "version": "0.0.10", 4 | "description": "2D Computational Geometry for Javascript", 5 | "main": "build/compgeo.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Anselm Eickhoff & Michael Chang", 9 | "email": "flux.blackcat@gmail.com", 10 | "url": "https://github.com/mflux" 11 | }, 12 | "dependencies": { 13 | "babel-core": "^5.1.10", 14 | "babel-polyfill": "^6.23.0", 15 | "cdt2d": "^1.0.0", 16 | "clean-pslg": "^1.1.2", 17 | "three": "^0.84.0" 18 | }, 19 | "devDependencies": { 20 | "babel-plugin-external-helpers": "^6.22.0", 21 | "babel-preset-es2015": "^6.24.1", 22 | "babel-preset-es2015-rollup": "^3.0.0", 23 | "babel-preset-latest": "^6.24.0", 24 | "babelify": "^7.3.0", 25 | "browserify": "^14.3.0", 26 | "budo": "^9.4.7", 27 | "chance": "^1.0.6", 28 | "rollup": "^0.41.6", 29 | "rollup-plugin-babel": "^2.7.1", 30 | "rollup-plugin-local-resolve": "^1.0.7", 31 | "tap-spec": "https://github.com/citybound/tap-spec.git", 32 | "tape": "^4.2.2", 33 | "tape-catch": "^1.0.4", 34 | "three-orbit-viewer": "^69.3.1" 35 | }, 36 | "scripts": { 37 | "test": "node --harmony test.js", 38 | "dev": "budo example/app.js --live -- -t [ babelify --presets [ es2015 ] ]", 39 | "build": "rollup -c" 40 | }, 41 | "keywords": [], 42 | "repository": { 43 | "type": "git", 44 | "url": "git://github.com/mflux/comp-geo.git" 45 | }, 46 | "homepage": "https://github.com/mflux/comp-geo", 47 | "bugs": { 48 | "url": "https://github.com/mflux/comp-geo/issues" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import babel from 'rollup-plugin-babel'; 3 | import localResolve from 'rollup-plugin-local-resolve'; 4 | // import * as path from 'path'; 5 | 6 | export default { 7 | 8 | entry : 'es6/index.js', 9 | dest : 'build/compgeo.js', 10 | format : 'cjs', 11 | sourceMap : true, 12 | 13 | // external dependencies that modules will reference 14 | external: [ 15 | 'three', 16 | 'cdt2d', 17 | 'clean-pslg', 18 | 'babel-polyfill', 19 | 'babel-core', 20 | 'babel-core/lib/helpers/parse.js' 21 | ], 22 | 23 | plugins: [ 24 | 25 | // transpile, but exclude node modules 26 | babel({ 27 | exclude: [ 'node_modules/**' ], 28 | presets: [ 'es2015-rollup' ], 29 | }), 30 | 31 | // help rollup resolve local paths 32 | localResolve() 33 | ] 34 | }; -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require("babel-core/register")({ 2 | blacklist: ['regenerator', 'es6.forOf'], 3 | optional: ['es7.classProperties'] 4 | }); 5 | 6 | const TestRunner = require('./TestRunner'); 7 | const fs = require("fs"); 8 | const path = require("path") 9 | 10 | const primitiveTests = []; 11 | 12 | ["primitives", "skeleton"].forEach(function (testGroup) { 13 | var tests = []; 14 | 15 | fs.readdirSync(path.join(__dirname, "./tests/" + testGroup)).forEach(function (file) { 16 | tests.push(require("./tests/" + testGroup + "/" + file)); 17 | }); 18 | 19 | TestRunner.test(tests); 20 | }); -------------------------------------------------------------------------------- /tests/intersections/IntersectionsCircleCircleTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Circle from '../../es6/primitives/Circle'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 5 | 6 | function drawIntersections(a, b, i) { 7 | return [a, b].concat(i); 8 | } 9 | 10 | var tests = [ 11 | { 12 | name: "circle-circle intersection", 13 | draw: drawIntersections, 14 | setup: function() { 15 | var a = new Circle([0.25, 0.5], 0.25); 16 | var b = new Circle([0.5, 0.5], 0.25); 17 | var i = intersect(a, b); 18 | return [a, b, i]; 19 | }, 20 | assert: function(a, b, i) { 21 | assert.equal(i.length, 2); 22 | var i0 = i[0]; 23 | var i1 = i[1]; 24 | 25 | assertRoughlyEqualVec2(i0.p, [0.375, 1 - 0.28349363803863525], "p0"); 26 | assertRoughlyEqualVec2(i1.p, [0.375, 0.28349363803863525], "p1"); 27 | assertRoughlyEqual(i0.u, 0.16666667017293113, "u0"); 28 | assertRoughlyEqual(i0.v, 0.3333333298270689, "v0"); 29 | assertRoughlyEqual(i1.u, 0.8333333298270689, "u1"); 30 | assertRoughlyEqual(i1.v, 0.6666666701729311, "v1"); 31 | } 32 | }, 33 | { 34 | name: "circle-circle tip", 35 | draw: drawIntersections, 36 | setup: function() { 37 | var a = new Circle([0.25, 0.5], 0.25); 38 | var b = new Circle([0.75, 0.5], 0.25); 39 | var i = intersect(a, b); 40 | return [a, b, i]; 41 | }, 42 | assert: function(a, b, i) { 43 | assert.equal(i.length, 1); 44 | var i0 = i[0]; 45 | assertRoughlyEqualVec2(i0.p, [0.5, 0.5], "p"); 46 | assertRoughlyEqual(i0.u, 0, "u"); 47 | assertRoughlyEqual(i0.v, 0.5, "v"); 48 | } 49 | }, 50 | { 51 | name: "circle-circle inside-outside", 52 | draw: drawIntersections, 53 | setup: function() { 54 | var a = new Circle([0.5, 0.5], 0.5); 55 | var b = new Circle([0.5, 0.5], 0.25); 56 | var i = intersect(a, b); 57 | return [a, b, i]; 58 | }, 59 | assert: function(a, b, i) { 60 | assert.equal(i.length, 0); 61 | } 62 | }, 63 | { 64 | name: "circle-circle same", 65 | draw: drawIntersections, 66 | setup: function() { 67 | var a = new Circle([0.5, 0.5], 0.5); 68 | var b = new Circle([0.5, 0.5], 0.5); 69 | var i = intersect(a, b); 70 | return [a, b, i]; 71 | }, 72 | assert: function(a, b, i) { 73 | assert.equal(i.length, 0); 74 | } 75 | } 76 | ]; 77 | 78 | Object.defineProperties(exports, { 79 | name: {value: "intersections circle-circle tests"}, 80 | tests: {value: tests} 81 | }); 82 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsCurveCurveTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Curve from '../../es6/primitives/Curve'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 5 | import {Vector2 as vec2} from '../../es6/nd-linalg'; 6 | 7 | function drawIntersections(a, b, i) { 8 | return [a, b].concat(i); 9 | } 10 | 11 | var tests = [ 12 | { 13 | name: "curve-curve intersection1", 14 | draw: drawIntersections, 15 | setup: function() { 16 | var a = new Curve([0, 0], [0, 1], [1, 1]); 17 | var b = new Curve([1, 0], [0, 1], [0, 1]); 18 | var i = intersect(a, b); 19 | return [a, b, i]; 20 | }, 21 | assert: function(a, b, i) { 22 | assert.equal(i.length, 1); 23 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.8660253882408142]); 24 | } 25 | }, 26 | { 27 | name: "curve-curve intersection2", 28 | draw: drawIntersections, 29 | setup: function() { 30 | var a = new Curve([0.25, 0], [1, 0], [0.25, 1]); 31 | var b = new Curve([0.75, 0], [-1, 0], [0.75, 1]); 32 | var i = intersect(a, b); 33 | return [a, b, i]; 34 | }, 35 | assert: function(a, b, i) { 36 | assert.equal(i.length, 2); 37 | assertRoughlyEqualVec2(i[0].p, [0.5, 1 - 0.0669872984290123], "p0"); 38 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.0669872984290123], "p1"); 39 | } 40 | }, 41 | { 42 | name: "curve-curve tip1", 43 | draw: drawIntersections, 44 | setup: function() { 45 | var a = new Curve([0, 0], [1, 0], [0, 1]); 46 | var b = new Curve([1, 0], [-1, 0], [1, 1]); 47 | var i = intersect(a, b); 48 | return [a, b, i]; 49 | }, 50 | assert: function(a, b, i) { 51 | assert.equal(i.length, 1); 52 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5], "p"); 53 | } 54 | }, 55 | { 56 | name: "curve-curve tip1 variant1", 57 | draw: drawIntersections, 58 | setup: function() { 59 | var a = new Curve([0, 0], [1, 0], [0.5, 0.5]); 60 | var b = new Curve([0.5, 0.5], [0, 1], [1, 1]); 61 | var i = intersect(a, b); 62 | return [a, b, i]; 63 | }, 64 | assert: function(a, b, i) { 65 | assert.equal(i.length, 1); 66 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5], "p"); 67 | } 68 | }, 69 | { 70 | name: "curve-curve tip1 variant2", 71 | draw: drawIntersections, 72 | setup: function() { 73 | var a = new Curve([0, 0], [1, 0], [0.5, 0.5]); 74 | var b = new Curve([0.5, 0.5], [1, 0], [1, 1]); 75 | var i = intersect(a, b); 76 | return [a, b, i]; 77 | }, 78 | assert: function(a, b, i) { 79 | assert.equal(i.length, 1); 80 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5], "p"); 81 | } 82 | }, 83 | { 84 | name: "curve-curve tip2", 85 | draw: drawIntersections, 86 | setup: function() { 87 | var a = new Curve([0, 0], [0, 1], [1, 0]); 88 | var b = new Curve([0, 0], [0.5, 0.5], [1, 0]); 89 | var i = intersect(a, b); 90 | return [a, b, i]; 91 | }, 92 | assert: function(a, b, i) { 93 | assert.equal(i.length, 2); 94 | assertRoughlyEqualVec2(i[0].p, [1, 0], "p0"); 95 | assertRoughlyEqualVec2(i[1].p, [0, 0], "p1"); 96 | assertRoughlyEqual(i[0].u, 1, "u0"); 97 | assertRoughlyEqual(i[0].v, 1, "v0"); 98 | assertRoughlyEqual(i[1].u, 0, "u1"); 99 | assertRoughlyEqual(i[1].v, 0, "v1"); 100 | } 101 | }, 102 | { 103 | name: "curve-curve tip2 variant", 104 | draw: drawIntersections, 105 | setup: function() { 106 | var a = new Curve([0, 0.5], [0, 1], [1, 0.5]); 107 | var b = new Curve([0, 0.5], [0, -1], [1, 0.5]); 108 | var i = intersect(a, b); 109 | return [a, b, i]; 110 | }, 111 | assert: function(a, b, i) { 112 | assert.equal(i.length, 2); 113 | assertRoughlyEqualVec2(i[0].p, [0, 0.5], "p0"); 114 | assertRoughlyEqualVec2(i[1].p, [1, 0.5], "p1"); 115 | assertRoughlyEqual(i[0].u, 0, "u0"); 116 | assertRoughlyEqual(i[0].v, 0, "v0"); 117 | assertRoughlyEqual(i[1].u, 1, "u1"); 118 | assertRoughlyEqual(i[1].v, 1, "v1"); 119 | } 120 | }, 121 | { 122 | name: "curve-curve tip end to end", 123 | draw: drawIntersections, 124 | setup: function() { 125 | var a = new Curve([0, 0], [1, 0], [0, 1]); 126 | var b = new Curve([0.5, 0.5], [0, 1], [0, 1]); 127 | var i = intersect(a, b); 128 | return [a, b, i]; 129 | }, 130 | assert: function(a, b, i) { 131 | assert.equal(i.length, 2); 132 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5], "p0"); 133 | assertRoughlyEqualVec2(i[1].p, [0, 1], "p1"); 134 | assertRoughlyEqual(i[0].u, a.getAlphaValueAtPosition([0.5, 0.5]), "u0"); 135 | assertRoughlyEqual(i[0].v, 0, "v0"); 136 | assertRoughlyEqual(i[1].u, 1, "u1"); 137 | assertRoughlyEqual(i[1].v, 1, "v1"); 138 | } 139 | }, 140 | { 141 | name: "curve-curve tip start to start", 142 | draw: drawIntersections, 143 | setup: function() { 144 | var a = new Curve([0, 0], [1, 0], [0, 1]); 145 | var b = new Curve([0, 0], [1, 0], [0.5, 0.5]); 146 | var i = intersect(a, b); 147 | return [a, b, i]; 148 | }, 149 | assert: function(a, b, i) { 150 | assert.equal(i.length, 2); 151 | assertRoughlyEqualVec2(i[0].p, [0, 0], "p0"); 152 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.5], "p1"); 153 | assertRoughlyEqual(i[0].u, 0, "u0"); 154 | assertRoughlyEqual(i[0].v, 0, "v0"); 155 | assertRoughlyEqual(i[1].u, a.getAlphaValueAtPosition([0.5, 0.5]), "u1"); 156 | assertRoughlyEqual(i[1].v, 1, "v1"); 157 | } 158 | }, 159 | { 160 | name: "curve-curve contained", 161 | draw: drawIntersections, 162 | setup: function() { 163 | var a = new Curve([0, 0], [1, 0], [0, 1]); 164 | var start = [0.5, -0.5]; 165 | vec2.normalize(start, start); 166 | vec2.scale(start, start, 0.5); 167 | start[1] += 0.5; 168 | var end = [0.5, 0.5]; 169 | vec2.normalize(end, end); 170 | vec2.scale(end, end, 0.5); 171 | end[1] += 0.5; 172 | var b = new Curve(start, [0.5, 0.5], end); 173 | var i = intersect(a, b); 174 | return [a, b, i]; 175 | }, 176 | assert: function(a, b, i) { 177 | assert.equal(i.length, 2); 178 | assertRoughlyEqualVec2(i[0].p, b.start, "p0"); 179 | assertRoughlyEqualVec2(i[1].p, b.end, "p1"); 180 | assertRoughlyEqual(i[0].u, a.getAlphaValueAtPosition(b.start), "u0"); 181 | assertRoughlyEqual(i[0].v, 0, "v0"); 182 | assertRoughlyEqual(i[1].u, a.getAlphaValueAtPosition(b.end), "u1"); 183 | assertRoughlyEqual(i[1].v, 1, "v1"); 184 | } 185 | }, 186 | { 187 | name: "curve-curve overlap", 188 | draw: drawIntersections, 189 | setup: function() { 190 | var start = [0.5, -0.5]; 191 | vec2.normalize(start, start); 192 | vec2.scale(start, start, 0.5); 193 | start[1] += 0.5; 194 | var end = [0.5, 0.5]; 195 | vec2.normalize(end, end); 196 | vec2.scale(end, end, 0.5); 197 | end[1] += 0.5; 198 | var a = new Curve([0, 0], [1, 0], end); 199 | var b = new Curve(start, [0.5, 0.5], [0, 1]); 200 | var i = intersect(a, b); 201 | return [a, b, i]; 202 | }, 203 | assert: function(a, b, i) { 204 | assert.equal(i.length, 2); 205 | assertRoughlyEqualVec2(i[0].p, b.start, "p0"); 206 | assertRoughlyEqualVec2(i[1].p, a.end, "p1"); 207 | assertRoughlyEqual(i[0].u, a.getAlphaValueAtPosition(b.start), "u0"); 208 | assertRoughlyEqual(i[0].v, 0, "v0"); 209 | assertRoughlyEqual(i[1].u, 1, "u1"); 210 | assertRoughlyEqual(i[1].v, b.getAlphaValueAtPosition(a.end), "v1"); 211 | } 212 | } 213 | ]; 214 | 215 | Object.defineProperties(exports, { 216 | name: {value: "intersections curve-curve tests"}, 217 | tests: {value: tests} 218 | }); 219 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsLineLineTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Line from '../../es6/primitives/Line'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 5 | 6 | function drawIntersections(a, b, i) { 7 | return [a, b].concat(i); 8 | } 9 | 10 | var tests = [ 11 | { 12 | name: "line-line intersection", 13 | draw: drawIntersections, 14 | setup: function() { 15 | var a = new Line([0.5, 0.5], [-1, -1]); 16 | var b = new Line([0.5, 1], [-1, 0]); 17 | var i = intersect(a, b); 18 | return [a, b, i]; 19 | }, 20 | assert: function(a, b, i) { 21 | assert.equal(i.length, 1); 22 | var i0 = i[0]; 23 | assertRoughlyEqualVec2(i0.p, [1, 1], "p"); 24 | } 25 | }, 26 | { 27 | name: "line-line intersection 2", 28 | draw: drawIntersections, 29 | setup: function() { 30 | var a = new Line([0, 0], [1, 1]); 31 | var b = new Line([0, 1], [1, 0]); 32 | var i = intersect(a, b); 33 | return [a, b, i]; 34 | }, 35 | assert: function(a, b, i) { 36 | assert.equal(i.length, 1); 37 | var i0 = i[0]; 38 | assertRoughlyEqualVec2(i0.p, [1, 1], "p"); 39 | } 40 | }, 41 | { 42 | name: "line-line intersection 3", 43 | draw: drawIntersections, 44 | setup: function() { 45 | var a = new Line([0.33, 0], [-0.1, 1]); 46 | var b = new Line([0.66, 1], [-0.1, -1]); 47 | var i = intersect(a, b); 48 | return [a, b, i]; 49 | }, 50 | assert: function(a, b, i) { 51 | assert.equal(i.length, 1); 52 | } 53 | }, 54 | { 55 | name: "line-line parallel", 56 | draw: drawIntersections, 57 | setup: function() { 58 | var a = new Line([0, 0.1], [1, 0]); 59 | var b = new Line([0, 0.9], [1, 0]); 60 | var i = intersect(a, b); 61 | return [a, b, i]; 62 | }, 63 | assert: function(a, b, i) { 64 | assert.equal(i.length, 0); 65 | } 66 | }, 67 | { 68 | name: "line-line contained", 69 | draw: drawIntersections, 70 | setup: function() { 71 | var a = new Line([0, 0.5], [1, 0]); 72 | var b = new Line([0.5, 0.5], [1, 0]); 73 | var i = intersect(a, b); 74 | return [a, b, i]; 75 | }, 76 | assert: function(a, b, i) { 77 | assert.equal(i.length, 1); 78 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5]); 79 | } 80 | } 81 | ]; 82 | 83 | Object.defineProperties(exports, { 84 | name: {value: "intersections line-line tests"}, 85 | tests: {value: tests} 86 | }); 87 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsLineSegmentCircleTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import LineSegment from '../../es6/primitives/LineSegment'; 3 | import Circle from '../../es6/primitives/Circle'; 4 | import intersect from '../../es6/intersections/Intersections'; 5 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 6 | 7 | function drawIntersections(a, b, i) { 8 | return [a, b].concat(i); 9 | } 10 | 11 | var tests = [ 12 | { 13 | name: "lineSegment-circle intersection", 14 | draw: drawIntersections, 15 | setup: function() { 16 | var a = new LineSegment([0.5, 0], [0.5, 1]); 17 | var b = new Circle([0.5, 0.5], 0.25); 18 | var i = intersect(a, b); 19 | return [a, b, i]; 20 | }, 21 | assert: function(a, b, i) { 22 | assert.equal(i.length, 2); 23 | 24 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.75], "p0"); 25 | assertRoughlyEqual(i[0].u, 0.75, "u0"); 26 | assertRoughlyEqual(i[0].v, 0.25, "v0"); 27 | 28 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.25], "p1"); 29 | assertRoughlyEqual(i[1].u, 0.25, "u1"); 30 | assertRoughlyEqual(i[1].v, 0.75, "v1"); 31 | } 32 | }, 33 | { 34 | name: "lineSegment-circle tip", 35 | draw: drawIntersections, 36 | setup: function() { 37 | var a = new LineSegment([0.5, 0], [0.5, 1]); 38 | var b = new Circle([0.5, 0.5], 0.5); 39 | var i = intersect(a, b); 40 | return [a, b, i]; 41 | }, 42 | assert: function(a, b, i) { 43 | assert.equal(i.length, 2); 44 | assertRoughlyEqualVec2(i[0].p, [0.5, 1], "p0"); 45 | assertRoughlyEqual(i[0].u, 1, "u0"); 46 | assertRoughlyEqualVec2(i[1].p, [0.5, 0], "p1"); 47 | assertRoughlyEqual(i[1].u, 0, "u1"); 48 | } 49 | }, 50 | { 51 | name: "lineSegment-circle inside", 52 | draw: drawIntersections, 53 | setup: function() { 54 | var a = new LineSegment([0.5, 0.25], [0.5, 0.75]); 55 | var b = new Circle([0.5, 0.5], 0.5); 56 | var i = intersect(a, b); 57 | return [a, b, i]; 58 | }, 59 | assert: function(a, b, i) { 60 | assert.equal(i.length, 0); 61 | } 62 | }, 63 | { 64 | name: "lineSegment-circle outside", 65 | draw: drawIntersections, 66 | setup: function() { 67 | var a = new LineSegment([0.1, 0], [0.1, 1]); 68 | var b = new Circle([0.5, 0.5], 0.25); 69 | var i = intersect(a, b); 70 | return [a, b, i]; 71 | }, 72 | assert: function(a, b, i) { 73 | assert.equal(i.length, 0); 74 | } 75 | } 76 | ]; 77 | 78 | Object.defineProperties(exports, { 79 | name: {value: "intersections lineSegment-circle tests"}, 80 | tests: {value: tests} 81 | }); 82 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsLineSegmentCurveTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import LineSegment from '../../es6/primitives/LineSegment'; 3 | import Curve from '../../es6/primitives/Curve'; 4 | import intersect from '../../es6/intersections/Intersections'; 5 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 6 | 7 | function drawIntersections(a, b, i) { 8 | return [a, b].concat(i); 9 | } 10 | 11 | var tests = [ 12 | { 13 | name: "lineSegment-curve intersection1", 14 | draw: drawIntersections, 15 | setup: function() { 16 | var a = new LineSegment([0, 0.5], [0.5, 0.5]); 17 | var b = new Curve([0.5, 0.25], [-1, 0], [0.5, 0.75]); 18 | var i = intersect(a, b); 19 | return [a, b, i]; 20 | }, 21 | assert: function(a, b, i) { 22 | assert.equal(i.length, 1); 23 | assertRoughlyEqualVec2(i[0].p, [0.25, 0.5], "p0"); 24 | assertRoughlyEqual(i[0].u, 0.5, "u0"); 25 | } 26 | }, 27 | { 28 | name: "lineSegment-curve intersection2", 29 | draw: drawIntersections, 30 | setup: function() { 31 | var a = new LineSegment([0.5, 0], [0.5, 1]); 32 | var b = new Curve([0.5, 0.25], [-1, 0], [0.5, 0.75]); 33 | var i = intersect(a, b); 34 | return [a, b, i]; 35 | }, 36 | assert: function(a, b, i) { 37 | assert.equal(i.length, 2); 38 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.75], "p0"); 39 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.25], "p1"); 40 | assertRoughlyEqual(i[0].u, 0.75, "u0"); 41 | assertRoughlyEqual(i[0].v, 1, "v0"); 42 | assertRoughlyEqual(i[1].u, 0.25, "u1"); 43 | assertRoughlyEqual(i[1].v, 0, "v1"); 44 | } 45 | }, 46 | { 47 | name: "lineSegment-curve tip1", 48 | draw: drawIntersections, 49 | setup: function() { 50 | var a = new LineSegment([0.25, 0.5], [0.5, 0.5]); 51 | var b = new Curve([0.5, 0.25], [-1, 0], [0.5, 0.75]); 52 | var i = intersect(a, b); 53 | return [a, b, i]; 54 | }, 55 | assert: function(a, b, i) { 56 | assert.equal(i.length, 1); 57 | assertRoughlyEqualVec2(i[0].p, [0.25, 0.5], "p0"); 58 | assertRoughlyEqual(i[0].u, 0, "u0"); 59 | } 60 | }, 61 | { 62 | name: "lineSegment-curve tip2", 63 | draw: drawIntersections, 64 | setup: function() { 65 | var a = new LineSegment([0.5, 0.25], [0.5, 0.75]); 66 | var b = new Curve([0.5, 0.25], [-1, 0], [0.5, 0.75]); 67 | var i = intersect(a, b); 68 | return [a, b, i]; 69 | }, 70 | assert: function(a, b, i) { 71 | assert.equal(i.length, 2); 72 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.75], "p0"); 73 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.25], "p1"); 74 | assertRoughlyEqual(i[0].u, 1, "u0"); 75 | assertRoughlyEqual(i[1].u, 0, "u1"); 76 | } 77 | }, 78 | { 79 | name: "lineSegment-curve miss", 80 | draw: drawIntersections, 81 | setup: function() { 82 | var a = new LineSegment([0.5, 0.5], [1, 0.5]); 83 | var b = new Curve([0.5, 0.25], [-1, 0], [0.5, 0.75]); 84 | var i = intersect(a, b); 85 | return [a, b, i]; 86 | }, 87 | assert: function(a, b, i) { 88 | assert.equal(i.length, 0); 89 | } 90 | } 91 | ]; 92 | 93 | Object.defineProperties(exports, { 94 | name: {value: "intersections lineSegment-curve tests"}, 95 | tests: {value: tests} 96 | }); 97 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsLineSegmentLineSegmentTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import LineSegment from '../../es6/primitives/LineSegment'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 5 | 6 | function drawIntersections(a, b, i) { 7 | return [a, b].concat(i); 8 | } 9 | 10 | var tests = [ 11 | { 12 | name: "lineSegment-lineSegment intersection", 13 | draw: drawIntersections, 14 | setup: function() { 15 | var a = new LineSegment([0, 0], [1, 1]); 16 | var b = new LineSegment([0, 1], [1, 0]); 17 | var i = intersect(a, b); 18 | return [a, b, i]; 19 | }, 20 | assert: function(a, b, i) { 21 | assert.equal(i.length, 1); 22 | var i0 = i[0]; 23 | assertRoughlyEqualVec2(i0.p, [0.5, 0.5], "p"); 24 | assertRoughlyEqual(i0.u, 0.5, "v"); 25 | assertRoughlyEqual(i0.v, 0.5, "u"); 26 | } 27 | }, 28 | { 29 | name: "lineSegment-lineSegment no-intersection", 30 | draw: drawIntersections, 31 | setup: function() { 32 | var a = new LineSegment([0, 0], [-1, -1]); 33 | var b = new LineSegment([0, 1], [1, 0]); 34 | var i = intersect(a, b); 35 | return [a, b, i]; 36 | }, 37 | assert: function(a, b, i) { 38 | assert.equal(i.length, 0); 39 | } 40 | }, 41 | { 42 | name: "lineSegment-lineSegment same", 43 | draw: drawIntersections, 44 | setup: function() { 45 | var a = new LineSegment([0, 0], [1, 1]); 46 | var b = a; 47 | var i = intersect(a, b); 48 | return [a, b, i]; 49 | }, 50 | assert: function(a, b, i) { 51 | assert.equal(i.length, 2); 52 | assertRoughlyEqualVec2(i[0].p, [0, 0]); 53 | assertRoughlyEqualVec2(i[1].p, [1, 1]); 54 | assertRoughlyEqual(i[0].u, 0); 55 | assertRoughlyEqual(i[0].v, 0); 56 | assertRoughlyEqual(i[1].u, 1); 57 | assertRoughlyEqual(i[1].v, 1); 58 | } 59 | }, 60 | { 61 | name: "lineSegment-lineSegment parallel", 62 | draw: drawIntersections, 63 | setup: function() { 64 | var a = new LineSegment([0, 0], [0, 1]); 65 | var b = new LineSegment([1, 0], [1, 1]); 66 | var i = intersect(a, b); 67 | return [a, b, i]; 68 | }, 69 | assert: function(a, b, i) { 70 | assert.equal(i.length, 0); 71 | } 72 | }, 73 | { 74 | name: "lineSegment-lineSegment overlap", 75 | draw: drawIntersections, 76 | setup: function() { 77 | var a = new LineSegment([0, 0.5], [0.75, 0.5]), 78 | b = new LineSegment([0.25, 0.5], [1, 0.5]), 79 | i = intersect(a, b); 80 | return [a, b, i]; 81 | }, 82 | assert: function(a, b, i) { 83 | assert.equal(i.length, 2); 84 | assertRoughlyEqualVec2(i[0].p, [0.25, 0.5]); 85 | assertRoughlyEqualVec2(i[1].p, [0.75, 0.5]); 86 | assertRoughlyEqual(i[0].u, 0.3333333333333333); 87 | assertRoughlyEqual(i[0].v, 0); 88 | assertRoughlyEqual(i[1].u, 1); 89 | assertRoughlyEqual(i[1].v, 0.6666666666666666); 90 | } 91 | }, 92 | { 93 | name: "lineSegment-lineSegment contained touching start tip", 94 | draw: drawIntersections, 95 | setup: function() { 96 | var a = new LineSegment([0, 0.5], [1, 0.5]), 97 | b = new LineSegment([0, 0.5], [0.5, 0.5]), 98 | i = intersect(a, b); 99 | return [a, b, i]; 100 | }, 101 | assert: function(a, b, i) { 102 | assert.equal(i.length, 2); 103 | assertRoughlyEqualVec2(i[0].p, [0.0, 0.5]); 104 | assertRoughlyEqualVec2(i[1].p, [0.5, 0.5]); 105 | assertRoughlyEqual(i[0].u, 0); 106 | assertRoughlyEqual(i[0].v, 0); 107 | assertRoughlyEqual(i[1].u, 0.5); 108 | assertRoughlyEqual(i[1].v, 1); 109 | } 110 | }, 111 | { 112 | name: "lineSegment-lineSegment contained touching end tip", 113 | draw: drawIntersections, 114 | setup: function() { 115 | var a = new LineSegment([0, 0.5], [1, 0.5]), 116 | b = new LineSegment([0.5, 0.5], [1, 0.5]), 117 | i = intersect(a, b); 118 | return [a, b, i]; 119 | }, 120 | assert: function(a, b, i) { 121 | assert.equal(i.length, 2); 122 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5]); 123 | assertRoughlyEqualVec2(i[1].p, [1, 0.5]); 124 | assertRoughlyEqual(i[0].u, 0.5); 125 | assertRoughlyEqual(i[0].v, 0); 126 | assertRoughlyEqual(i[1].u, 1); 127 | assertRoughlyEqual(i[1].v, 1); 128 | } 129 | }, 130 | { 131 | name: "lineSegment-lineSegment contained", 132 | draw: drawIntersections, 133 | setup: function() { 134 | var a = new LineSegment([0, 0.5], [1, 0.5]), 135 | b = new LineSegment([0.25, 0.5], [0.75, 0.5]), 136 | i = intersect(a, b); 137 | return [a, b, i]; 138 | }, 139 | assert: function(a, b, i) { 140 | assert.equal(i.length, 2); 141 | assertRoughlyEqualVec2(i[0].p, [0.25, 0.5]); 142 | assertRoughlyEqualVec2(i[1].p, [0.75, 0.5]); 143 | assertRoughlyEqual(i[0].u, 0.25); 144 | assertRoughlyEqual(i[0].v, 0); 145 | assertRoughlyEqual(i[1].u, 0.75); 146 | assertRoughlyEqual(i[1].v, 1); 147 | } 148 | }, 149 | { 150 | name: "lineSegment-lineSegment tip start to end", 151 | draw: drawIntersections, 152 | setup: function() { 153 | var a = new LineSegment([0, 0], [1, 1]); 154 | var b = new LineSegment([1, 1], [1, 0]); 155 | var i = intersect(a, b); 156 | return [a, b, i]; 157 | }, 158 | assert: function(a, b, i) { 159 | assert.equal(i.length, 1); 160 | var i0 = i[0]; 161 | assertRoughlyEqualVec2(i0.p, [1, 1], "p"); 162 | assertRoughlyEqual(i0.u, 1, "u"); 163 | assertRoughlyEqual(i0.v, 0, "v"); 164 | } 165 | }, 166 | { 167 | name: "lineSegment-lineSegment tip end to end", 168 | draw: drawIntersections, 169 | setup: function() { 170 | var a = new LineSegment([0, 0], [0.5, 0.5]); 171 | var b = new LineSegment([1, 1], [0.5, 0.5]); 172 | var i = intersect(a, b); 173 | return [a, b, i]; 174 | }, 175 | assert: function(a, b, i) { 176 | assert.equal(i.length, 1); 177 | var i0 = i[0]; 178 | assertRoughlyEqualVec2(i0.p, [0.5, 0.5], "p"); 179 | assertRoughlyEqual(i0.u, 1, "u"); 180 | assertRoughlyEqual(i0.v, 1, "v"); 181 | } 182 | }, 183 | { 184 | name: "lineSegment-lineSegment tip start to start", 185 | draw: drawIntersections, 186 | setup: function() { 187 | var a = new LineSegment([0.5, 0.5], [0, 0]); 188 | var b = new LineSegment([0.5, 0.5], [1, 1]); 189 | var i = intersect(a, b); 190 | return [a, b, i]; 191 | }, 192 | assert: function(a, b, i) { 193 | assert.equal(i.length, 1); 194 | var i0 = i[0]; 195 | assertRoughlyEqualVec2(i0.p, [0.5, 0.5], "p"); 196 | assertRoughlyEqual(i0.u, 0, "u"); 197 | assertRoughlyEqual(i0.v, 0, "v"); 198 | } 199 | } 200 | ]; 201 | 202 | Object.defineProperties(exports, { 203 | name: {value: "intersections lineSegment-lineSegment tests"}, 204 | tests: {value: tests} 205 | }); 206 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsRayLineSegmentTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Ray from '../../es6/primitives/Ray'; 3 | import LineSegment from '../../es6/primitives/LineSegment'; 4 | import intersect from '../../es6/intersections/Intersections'; 5 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 6 | 7 | function drawIntersections(a, b, i) { 8 | return [a, b].concat(i); 9 | } 10 | 11 | var tests = [ 12 | { 13 | name: "ray-lineSegment intersection", 14 | draw: drawIntersections, 15 | setup: function() { 16 | var a = new Ray([0, 0.5], [1, 0]); 17 | var b = new LineSegment([1, 0.25], [1, 0.75]); 18 | var i = intersect(a, b); 19 | return [a, b, i]; 20 | }, 21 | assert: function(a, b, i) { 22 | assert.equal(i.length, 1); 23 | var i0 = i[0]; 24 | assertRoughlyEqualVec2(i0.p, [1, 0.5], "p"); 25 | } 26 | }, 27 | { 28 | name: "ray-lineSegment no intersection up", 29 | draw: drawIntersections, 30 | setup: function() { 31 | var a = new Ray([0, 0.5], [1, 0]); 32 | var b = new LineSegment([1, 0.75], [1, 1]); 33 | var i = intersect(a, b); 34 | return [a, b, i]; 35 | }, 36 | assert: function(a, b, i) { 37 | assert.equal(i.length, 0); 38 | } 39 | }, 40 | { 41 | name: "ray-lineSegment no intersection down", 42 | draw: drawIntersections, 43 | setup: function() { 44 | var a = new Ray([0, 0.5], [1, 0]); 45 | var b = new LineSegment([1, 0], [1, 0.25]); 46 | var i = intersect(a, b); 47 | return [a, b, i]; 48 | }, 49 | assert: function(a, b, i) { 50 | assert.equal(i.length, 0); 51 | } 52 | } 53 | ]; 54 | 55 | Object.defineProperties(exports, { 56 | name: {value: "intersections ray-lineSegment tests"}, 57 | tests: {value: tests} 58 | }); 59 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsRayLineTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Ray from '../../es6/primitives/Ray'; 3 | import Line from '../../es6/primitives/Line'; 4 | import intersect from '../../es6/intersections/Intersections'; 5 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 6 | 7 | function drawIntersections(a, b, i) { 8 | return [a, b].concat(i); 9 | } 10 | 11 | var tests = [ 12 | { 13 | name: "ray-line intersection", 14 | draw: drawIntersections, 15 | setup: function() { 16 | var a = new Ray([0, 0], [1, 1]); 17 | var b = new Line([0, 1], [1, 0]); 18 | var i = intersect(a, b); 19 | return [a, b, i]; 20 | }, 21 | assert: function(a, b, i) { 22 | assert.equal(i.length, 1); 23 | var i0 = i[0]; 24 | assertRoughlyEqualVec2(i0.p, [1, 1], "p"); 25 | } 26 | }, 27 | { 28 | name: "ray-line no intersection", 29 | draw: drawIntersections, 30 | setup: function() { 31 | var a = new Ray([0.5, 1], [-1, 0]); 32 | var b = new Line([0.5, 0.5], [1, 1]); 33 | var i = intersect(a, b); 34 | return [a, b, i]; 35 | }, 36 | assert: function(a, b, i) { 37 | assert.equal(i.length, 0); 38 | } 39 | }, 40 | ]; 41 | 42 | Object.defineProperties(exports, { 43 | name: {value: "intersections ray-line tests"}, 44 | tests: {value: tests} 45 | }); 46 | -------------------------------------------------------------------------------- /tests/intersections/IntersectionsRayRayTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Ray from '../../es6/primitives/Ray'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 5 | 6 | function drawIntersections(a, b, i) { 7 | return [a, b].concat(i); 8 | } 9 | 10 | var tests = [ 11 | { 12 | name: "ray-ray intersection", 13 | draw: drawIntersections, 14 | setup: function() { 15 | var a = new Ray([0, 0], [1, 1]); 16 | var b = new Ray([0, 1], [1, 0]); 17 | var i = intersect(a, b); 18 | return [a, b, i]; 19 | }, 20 | assert: function(a, b, i) { 21 | assert.equal(i.length, 1); 22 | var i0 = i[0]; 23 | assertRoughlyEqualVec2(i0.p, [1, 1], "p"); 24 | } 25 | }, 26 | { 27 | name: "ray-ray tip", 28 | draw: drawIntersections, 29 | setup: function() { 30 | var a = new Ray([0, 0], [0, 1]); 31 | var b = new Ray([0, 0], [1, 0]); 32 | var i = intersect(a, b); 33 | return [a, b, i]; 34 | }, 35 | assert: function(a, b, i) { 36 | assert.equal(i.length, 1); 37 | assertRoughlyEqualVec2(i[0].p, [0, 0], "p"); 38 | assertRoughlyEqual(i[0].u, 0, "u"); 39 | assertRoughlyEqual(i[0].v, 0, "v"); 40 | } 41 | }, 42 | { 43 | name: "ray-ray parallel", 44 | draw: drawIntersections, 45 | setup: function() { 46 | var a = new Ray([0, 0.1], [1, 0]); 47 | var b = new Ray([0, 0.9], [1, 0]); 48 | var i = intersect(a, b); 49 | return [a, b, i]; 50 | }, 51 | assert: function(a, b, i) { 52 | assert.equal(i.length, 0); 53 | } 54 | }, 55 | { 56 | name: "ray-ray contained", 57 | draw: drawIntersections, 58 | setup: function() { 59 | var a = new Ray([0, 0.5], [1, 0]); 60 | var b = new Ray([0.5, 0.5], [1, 0]); 61 | var i = intersect(a, b); 62 | return [a, b, i]; 63 | }, 64 | assert: function(a, b, i) { 65 | assert.equal(i.length, 1); 66 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5]); 67 | } 68 | }, 69 | { 70 | name: "ray-ray contained variant", 71 | draw: drawIntersections, 72 | setup: function() { 73 | var a = new Ray([0.5, 0.5], [1, 0]); 74 | var b = new Ray([0, 0.5], [1, 0]); 75 | var i = intersect(a, b); 76 | return [a, b, i]; 77 | }, 78 | assert: function(a, b, i) { 79 | assert.equal(i.length, 1); 80 | assertRoughlyEqualVec2(i[0].p, [0.5, 0.5]); 81 | } 82 | }, 83 | { 84 | name: "ray-ray no intersection", 85 | draw: drawIntersections, 86 | setup: function() { 87 | var a = new Ray([0.33, 0], [-0.1, 1]); 88 | var b = new Ray([0.66, 1], [-0.1, -1]); 89 | var i = intersect(a, b); 90 | return [a, b, i]; 91 | }, 92 | assert: function(a, b, i) { 93 | assert.equal(i.length, 0); 94 | } 95 | }, 96 | { 97 | name: "ray-ray intersection up", 98 | draw: drawIntersections, 99 | setup: function() { 100 | var a = new Ray([0.33, 1], [0.1, -1]); 101 | var b = new Ray([0.66, 1], [-0.1, -1]); 102 | var i = intersect(a, b); 103 | return [a, b, i]; 104 | }, 105 | assert: function(a, b, i) { 106 | assert.equal(i.length, 1); 107 | } 108 | }, 109 | { 110 | name: "ray-ray intersection down", 111 | draw: drawIntersections, 112 | setup: function() { 113 | var a = new Ray([0.33, 0], [0.1, 1]); 114 | var b = new Ray([0.66, 0], [-0.1, 1]); 115 | var i = intersect(a, b); 116 | return [a, b, i]; 117 | }, 118 | assert: function(a, b, i) { 119 | assert.equal(i.length, 1); 120 | } 121 | } 122 | ]; 123 | 124 | Object.defineProperties(exports, { 125 | name: {value: "intersections ray-ray tests"}, 126 | tests: {value: tests} 127 | }); 128 | -------------------------------------------------------------------------------- /tests/primitives/CircleTests.js: -------------------------------------------------------------------------------- 1 | import Circle from '../../es6/primitives/Circle'; 2 | import {assertRoughlyEqual} from '../../es6/missing-stuff'; 3 | 4 | Object.defineProperties(exports, { 5 | name: {value: "circle tests"}, 6 | tests: {value: [ 7 | { 8 | name: "circle angles", 9 | setup: function() { 10 | return [new Circle([0, 0], 1)]; 11 | }, 12 | assert: function(c) { 13 | assertRoughlyEqual(c.angle([0, 1]), 0.25); 14 | assertRoughlyEqual(c.angle([-1, 0]), 0.5); 15 | assertRoughlyEqual(c.angle([0, -1]), 0.75); 16 | assertRoughlyEqual(c.angle([1, 0]), 0); 17 | } 18 | } 19 | ]} 20 | }); 21 | -------------------------------------------------------------------------------- /tests/primitives/CurveTests.js: -------------------------------------------------------------------------------- 1 | import Curve from '../../es6/primitives/Curve'; 2 | import LineSegment from '../../es6/primitives/LineSegment'; 3 | import intersect from '../../es6/intersections/Intersections'; 4 | import assert from 'assert'; 5 | import {assertRoughlyEqual, assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 6 | 7 | Object.defineProperties(exports, { 8 | name: {value: "curve tests"}, 9 | tests: {value: [ 10 | { 11 | name: "curve circles", 12 | setup: function() { 13 | var a = new Curve([0, 0], [0, 1], [2, 0]); 14 | var b = new Curve([1, 0], [0, 1], [3, 0]); 15 | return [a, b]; 16 | }, 17 | assert: function(a, b) { 18 | assertRoughlyEqualVec2(a.center, [1, 0]); 19 | assertRoughlyEqual(a.radius, 1); 20 | 21 | assertRoughlyEqualVec2(b.center, [2, 0]); 22 | assertRoughlyEqual(b.radius, 1); 23 | } 24 | }, 25 | { 26 | name: "curve degenerate flat circle to line", 27 | setup: function() { 28 | var c = new Curve([0, 0], [0, 1], [0, 1]); 29 | return [c]; 30 | }, 31 | assert: function(c) { 32 | assert(c.type() == intersect.LineSegmentTypeFunction()); 33 | assertRoughlyEqualVec2(c.start, [0, 0]); 34 | assertRoughlyEqualVec2(c.end, [0, 1]); 35 | } 36 | }, 37 | { 38 | name: "curve degenerate infinite circle exception", 39 | setup: function() {}, 40 | assert: function() { 41 | assert.throws( 42 | function() { 43 | new Curve([0, 0], [0, -1], [0, 1]); 44 | }, 45 | "Not a valid curve, infinite circle found"); 46 | } 47 | }, 48 | { 49 | name: "curve subdivide", 50 | setup: function() { 51 | var curve = new Curve([0, 0], [0, 1], [1, 1]); 52 | var line = new LineSegment([0, 1], [1, 0]); 53 | var i = intersect(line, curve); 54 | return curve.subdivide(i[0].p).concat([i[0].p]); 55 | }, 56 | assert: function(a, b, middle, nothing) { 57 | assert(nothing === undefined); 58 | assertRoughlyEqualVec2(a.start, [0, 0]); 59 | assertRoughlyEqualVec2(a.end, middle); 60 | assertRoughlyEqualVec2(b.start, middle); 61 | assertRoughlyEqualVec2(b.end, [1, 1]); 62 | } 63 | } 64 | ]} 65 | }); 66 | -------------------------------------------------------------------------------- /tests/primitives/LineTests.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import LineSegment from '../../es6/primitives/LineSegment'; 3 | import {assertRoughlyEqualVec2} from '../../es6/missing-stuff'; 4 | 5 | Object.defineProperties(exports, { 6 | name: {value: "line tests"}, 7 | tests: {value: [ 8 | { 9 | name: "line subdivide", 10 | setup: function() { 11 | var line = new LineSegment([0, 0], [1, 1]); 12 | return line.subdivide([0.25, 0.25]).concat([[0.25, 0.25]]); 13 | }, 14 | assert: function(a, b, c, d) { 15 | assert(d === undefined); 16 | assertRoughlyEqualVec2(a.start, [0, 0]); 17 | assertRoughlyEqualVec2(a.end, [0.25, 0.25]); 18 | assertRoughlyEqualVec2(b.start, [0.25, 0.25]); 19 | assertRoughlyEqualVec2(b.end, [1, 1]); 20 | } 21 | } 22 | ]} 23 | }); 24 | -------------------------------------------------------------------------------- /tests/skeleton/SkeletonClosedPathTests.js: -------------------------------------------------------------------------------- 1 | import Pather from '../../es6/helpers/pather'; 2 | import Skeleton from '../../es6/skeleton/skeleton'; 3 | import SkeletonVertex from '../../es6/skeleton/skeletonvertex'; 4 | import SkeletonEdge from '../../es6/skeleton/skeletonedge'; 5 | import { assert } from '../../es6/missing-stuff'; 6 | import Chance from 'chance'; 7 | import vec2 from '../../es6/nd-linalg/Vector2'; 8 | import Path from '../../es6/shapes/path'; 9 | import Shape from '../../es6/shapes/shape'; 10 | 11 | function draw(path, skeleton) { 12 | function drawspokes(context) { 13 | let min = 0.1, max = 0; 14 | for (let spoke of skeleton.spokes) 15 | max = Math.max(max, spoke.end[2]); 16 | 17 | for (let spoke of skeleton.spokes) { 18 | let style = new Drawing2DLinearGradient(spoke.start, spoke.end), 19 | start = (spoke.start[2] / max) / (1 - min) + min, 20 | end = (spoke.end[2] / max) / (1 - min) + min; 21 | style.addColorStop(0, `rgba(255, 0, 255, ${start})`); 22 | style.addColorStop(1, `rgba(255, 0, 255, ${end})`); 23 | context.style = style; 24 | spoke.draw(context); 25 | } 26 | } 27 | 28 | return [{colour: "#AAA", visuals: [path]}, drawspokes].concat(skeleton.waves.map(e => e.path)); 29 | } 30 | 31 | var RunStressTest = false; 32 | var seed = 0; 33 | var numberOfEdges = RunStressTest ? 15 : 50; 34 | 35 | //seed = 8173838342671996; 36 | //seed = 4149836918388686; 37 | //seed = 4413312592155855; 38 | //seed = 3778586571241709; 39 | //seed = 6414226216441014; 40 | var RunRandomOnly = seed > 0; 41 | RunStressTest = RunStressTest && (seed === 0); 42 | 43 | if (!RunRandomOnly) { 44 | seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 45 | console.log("SkeletonTests random seed", seed); 46 | } 47 | 48 | //RunRandomOnly = true; 49 | 50 | function convex() { 51 | return new Pather([0, 0.25]) 52 | .lineTo([0.1, 0]) 53 | .lineTo([0.7, 0.15]) 54 | .lineTo([1, 0.6]) 55 | .lineTo([0.75, 0.75]) 56 | .lineTo([0.55, 0.8]) 57 | .lineTo([0.25, 0.85]) 58 | .close(); 59 | } 60 | 61 | function concave() { 62 | return new Pather([0, 0.25]) 63 | .lineTo([0.1, 0]) 64 | .lineTo([0.7, 0.15]) 65 | .lineTo([1, 0.6]) 66 | .lineTo([0.75, 0.75]) 67 | .lineTo([0.55, 0.6]) 68 | .lineTo([0.25, 0.85]) 69 | .close(); 70 | } 71 | 72 | function square() { 73 | return new Pather() 74 | .lineTo([1, 0]) 75 | .lineTo([1, 1]) 76 | .lineTo([0, 1]) 77 | .close(); 78 | } 79 | 80 | function box() { 81 | return new Pather([0.25, 0]) 82 | .lineTo([0.75, 0]) 83 | .lineTo([0.75, 1]) 84 | .lineTo([0.25, 1]) 85 | .close(); 86 | } 87 | 88 | function plus() { 89 | return new Pather([0, 1 / 3]) 90 | .lineTo([1 / 3, 1 / 3]) 91 | .lineTo([1 / 3, 0]) 92 | .lineTo([2 / 3, 0]) 93 | .lineTo([2 / 3, 1 / 3]) 94 | .lineTo([1, 1 / 3]) 95 | .lineTo([1, 2 / 3]) 96 | .lineTo([2 / 3, 2 / 3]) 97 | .lineTo([2 / 3, 1]) 98 | .lineTo([1 / 3, 1]) 99 | .lineTo([1 / 3, 2 / 3]) 100 | .lineTo([0, 2 / 3]) 101 | .close(); 102 | } 103 | 104 | function l() { 105 | return new Pather() 106 | .lineTo([0.5, 0]) 107 | .lineTo([0.5, 0.5]) 108 | .lineTo([1, 0.5]) 109 | .lineTo([1, 1]) 110 | .lineTo([0, 1]) 111 | .close(); 112 | } 113 | 114 | function u() { 115 | return new Pather() 116 | .lineTo([1 / 3, 0]) 117 | .lineTo([1 / 3, 2 / 3]) 118 | .lineTo([2 / 3, 2 / 3]) 119 | .lineTo([2 / 3, 0]) 120 | .lineTo([1, 0]) 121 | .lineTo([1, 1]) 122 | .lineTo([0, 1]) 123 | .close(); 124 | } 125 | 126 | var tests = [ 127 | { 128 | name: "skeleton convex infinite", 129 | draw: draw, 130 | setup: function() { 131 | return [convex(), new Skeleton(convex(), Infinity, options)]; 132 | }, 133 | assert: function(path, skeleton) { 134 | // console.log( JSON.stringify( new Path( skeleton.spokes ).vertices(), null, 2 ) ); 135 | // console.log( JSON.stringify( skeleton.spokes, null, 2 ) ); 136 | 137 | path = path.concat( new Path( skeleton.spokes ) ); 138 | const shape = new Shape( path ); 139 | 140 | assert(skeleton.spokes.length > 0); 141 | } 142 | }, 143 | { 144 | name: "skeleton convex 0.3", 145 | draw: draw, 146 | setup: function() { 147 | return [convex(), new Skeleton(convex(), 0.3, options)]; 148 | }, 149 | assert: function(path, skeleton) { 150 | assert(skeleton.spokes.length > 0); 151 | } 152 | }, 153 | 154 | { 155 | name: "skeleton concave infinite", 156 | draw: draw, 157 | setup: function() { 158 | return [concave(), new Skeleton(concave(), Infinity, options)]; 159 | }, 160 | assert: function(path, skeleton) { 161 | assert(skeleton.spokes.length > 0); 162 | } 163 | }, 164 | { 165 | name: "skeleton concave 0.2", 166 | draw: draw, 167 | setup: function() { 168 | return [concave(), new Skeleton(concave(), 0.2, options)]; 169 | }, 170 | assert: function(path, skeleton) { 171 | assert(skeleton.spokes.length > 0); 172 | } 173 | }, 174 | 175 | {// profile: true, 176 | name: "skeleton random infinite", 177 | draw: draw, 178 | only: RunRandomOnly, 179 | seed: seed, 180 | setup: function() { 181 | let shape = scenario(numberOfEdges, seed); 182 | //Drawing2D.log("skeleton random shape", [shape]); 183 | let skeleton = new Skeleton(shape, Infinity, options); 184 | return [shape, skeleton]; 185 | }, 186 | assert: function(shape, skeleton) { 187 | assert(skeleton.spokes.length > 0) 188 | } 189 | }, 190 | { 191 | name: "skeleton random 0.075", 192 | draw: draw, 193 | seed: seed, 194 | setup: function() { 195 | let shape = scenario(numberOfEdges, seed); 196 | //Drawing2D.log("skeleton random shape", [shape]); 197 | let skeleton = new Skeleton(shape, 0.075, options); 198 | return [shape, skeleton]; 199 | }, 200 | assert: function(shape, skeleton) { 201 | assert(skeleton.spokes.length > 0) 202 | } 203 | }, 204 | 205 | { 206 | name: "skeleton square infinite", 207 | draw: draw, 208 | setup: function() { 209 | var shape = square(); 210 | return [shape, new Skeleton(shape, Infinity, options)]; 211 | }, 212 | assert: function(shape, skeleton) { 213 | assert(skeleton.spokes.length > 0) 214 | } 215 | }, 216 | { 217 | name: "skeleton square 0.25", 218 | draw: draw, 219 | setup: function() { 220 | var shape = square(); 221 | return [shape, new Skeleton(shape, 0.25, options)]; 222 | }, 223 | assert: function(shape, skeleton) { 224 | assert(skeleton.spokes.length > 0) 225 | } 226 | }, 227 | 228 | { 229 | name: "skeleton box infinite", 230 | draw: draw, 231 | setup: function() { 232 | var shape = box(); 233 | return [shape, new Skeleton(shape, Infinity, options)]; 234 | }, 235 | assert: function(shape, skeleton) { 236 | assert(skeleton.spokes.length > 0) 237 | } 238 | }, 239 | { 240 | name: "skeleton box 0.1", 241 | draw: draw, 242 | setup: function() { 243 | var shape = box(); 244 | return [shape, new Skeleton(shape, 0.1, options)]; 245 | }, 246 | assert: function(shape, skeleton) { 247 | assert(skeleton.spokes.length > 0) 248 | } 249 | }, 250 | 251 | { 252 | name: "skeleton plus infinite", 253 | draw: draw, 254 | setup: function() { 255 | var shape = plus(); 256 | return [shape, new Skeleton(shape, Infinity, options)]; 257 | }, 258 | assert: function(shape, skeleton) { 259 | assert(skeleton.spokes.length > 0) 260 | } 261 | }, 262 | { 263 | name: "skeleton plus 0.1", 264 | draw: draw, 265 | setup: function() { 266 | var shape = plus(); 267 | return [shape, new Skeleton(shape, 0.1, options)]; 268 | }, 269 | assert: function(shape, skeleton) { 270 | assert(skeleton.spokes.length > 0) 271 | } 272 | }, 273 | 274 | { 275 | name: "skeleton L infinite", 276 | draw: draw, 277 | setup: function() { 278 | var shape = l(); 279 | return [shape, new Skeleton(shape, Infinity, options)]; 280 | }, 281 | assert: function(shape, skeleton) { 282 | assert(skeleton.spokes.length > 0) 283 | } 284 | }, 285 | { 286 | name: "skeleton L 0.1", 287 | draw: draw, 288 | setup: function() { 289 | var shape = l(); 290 | return [shape, new Skeleton(shape, 0.1, options)]; 291 | }, 292 | assert: function(shape, skeleton) { 293 | assert(skeleton.spokes.length > 0) 294 | } 295 | }, 296 | 297 | { 298 | name: "skeleton U infinite", 299 | draw: draw, 300 | setup: function() { 301 | var shape = u(); 302 | return [shape, new Skeleton(shape, Infinity, options)]; 303 | }, 304 | assert: function(shape, skeleton) { 305 | assert(skeleton.spokes.length > 0) 306 | } 307 | }, 308 | { 309 | name: "skeleton U 0.1", 310 | draw: draw, 311 | setup: function() { 312 | var shape = u(); 313 | return [shape, new Skeleton(shape, 0.1, options)]; 314 | }, 315 | assert: function(shape, skeleton) { 316 | assert(skeleton.spokes.length > 0) 317 | } 318 | }, 319 | 320 | { 321 | name: "skeleton stress test", 322 | only: RunStressTest, 323 | save: false, 324 | skip: !RunStressTest, 325 | draw: () => [], 326 | setup: function() { 327 | var i = 0; 328 | var random = new Chance(); 329 | while(true) { 330 | let sd = random.floating({min:0, max:Number.MAX_SAFE_INTEGER}); 331 | if (i % 10 == 0) console.clear(); 332 | console.log(i++, sd); 333 | let shape = scenario(numberOfEdges, sd); 334 | new Skeleton(shape); 335 | } 336 | return []; 337 | }, 338 | assert: function() {} 339 | }, 340 | ]; 341 | 342 | var scenarios = {}; 343 | function scenario(numberOfPoints, seed) { 344 | if (scenarios[seed]) 345 | return scenarios[seed]; 346 | 347 | var step = 2 * Math.PI / numberOfPoints, 348 | angle = step, 349 | pather = new Pather([1, 0.5]), 350 | random = new Chance(seed); 351 | 352 | for (var i = 0; i < (numberOfPoints - 1); i++) { 353 | var x = Math.cos(angle), 354 | y = Math.sin(angle), 355 | scale = random.floating({min:0,max:1}) * 0.9 + 0.1; 356 | var vector = vec2.scale(vec2(0, 0), [x, y], scale * 0.5); 357 | vec2.add(vector, vector, [0.5, 0.5]); 358 | pather.lineTo(vector); 359 | angle += step; 360 | } 361 | return scenarios[seed] = pather.close(); 362 | } 363 | 364 | var DebugOptions = false; 365 | for (let test of tests) { 366 | if (test.only === true) { 367 | DebugOptions = true; 368 | break; 369 | } 370 | } 371 | 372 | function newoptions() { 373 | if (RunRandomOnly || DebugOptions) { 374 | return { 375 | DEBUG: true, 376 | DEBUG_DRAW_INITIAL: true, 377 | DEBUG_DRAW_INITIAL_EVENTS: false, 378 | DEBUG_DRAW_SKIPPED_EVENTS: true, 379 | DEBUG_DRAW_STEPS: true, 380 | DEBUG_DRAW_MOVE: true, 381 | DEBUG_DRAW_OBTUSE_EVENTS_EACH_STEP: false 382 | }; 383 | } 384 | return {}; 385 | } 386 | 387 | var options = newoptions(); 388 | 389 | Object.defineProperties(exports, { 390 | name: {value: "skeleton closed path tests"}, 391 | tests: {value: tests}, 392 | seed: {value: seed} 393 | }); 394 | -------------------------------------------------------------------------------- /tests/skeleton/SkeletonOpenPathTests.js: -------------------------------------------------------------------------------- 1 | import Pather from '../../es6/helpers/pather'; 2 | import Skeleton from '../../es6/skeleton/skeleton'; 3 | import SkeletonVertex from '../../es6/skeleton/skeletonvertex'; 4 | import * as SkeletonEdge from '../../es6/skeleton/skeletonedge'; 5 | import { assert } from '../../es6/missing-stuff'; 6 | import Chance from 'chance'; 7 | import vec2 from '../../es6/nd-linalg/Vector2'; 8 | import Path from '../../es6/shapes/path'; 9 | import LineSegment from '../../es6/primitives/linesegment'; 10 | 11 | function drawWithoutSpokes(path, skeleton) { 12 | return [{colour: "#AAA", visuals: [path]}].concat(skeleton.waves.map(e => e.path)); 13 | } 14 | 15 | var tests = [ 16 | { 17 | name: "straight path test", 18 | draw: drawWithoutSpokes, 19 | setup: function() { 20 | let path = new Path([new LineSegment(vec2(0, 0), vec2(1, 1))]); 21 | return [path, new Skeleton(path, 0.5, options)]; 22 | }, 23 | assert: function(path, skeleton) { 24 | assert(skeleton.waves.length == 4); 25 | } 26 | }, 27 | { 28 | name: "Z path test", 29 | draw: drawWithoutSpokes, 30 | setup: function() { 31 | let path = new Path([ 32 | new LineSegment(vec2(0, 0), vec2(1, 0)), 33 | new LineSegment(vec2(1, 0), vec2(0, 1)), 34 | new LineSegment(vec2(0, 1), vec2(1, 1))]); 35 | return [path, new Skeleton(path, 1, options)]; 36 | }, 37 | assert: function(path, skeleton) { 38 | assert(skeleton.waves.length == 4); 39 | } 40 | }, 41 | { 42 | name: "∑ path test", 43 | draw: drawWithoutSpokes, 44 | setup: function() { 45 | let path = new Path([ 46 | new LineSegment([1, 1], [0, 1]), 47 | new LineSegment([0, 1], [0.5, 0.5]), 48 | new LineSegment([0.5, 0.5], [0, 0]), 49 | new LineSegment([0, 0], [1, 0]) 50 | ]); 51 | return [path, new Skeleton(path, 1, options)]; 52 | }, 53 | assert: function(path, skeleton) { 54 | assert(skeleton.waves.length === 2); 55 | for (let wave of skeleton.waves) 56 | assert(wave.side !== SkeletonEdge.InnerEdge); 57 | } 58 | }, 59 | { 60 | name: "Ω path test", 61 | draw: drawWithoutSpokes, 62 | setup: function() { 63 | let path = new Pather([0, 1]) 64 | .lineTo([0.25, 1]) 65 | .lineTo([0, 0.5]) 66 | .lineTo([0.5, 0]) 67 | .lineTo([1, 0.5]) 68 | .lineTo([0.75, 1]) 69 | .lineTo([1, 1]) 70 | .path; 71 | return [path, new Skeleton(path, 0.25, options)]; 72 | }, 73 | assert: function(path, skeleton) { 74 | assert(skeleton.waves.length == 5); 75 | } 76 | }, 77 | { 78 | name: "Ω path test cap=0.25", 79 | draw: drawWithoutSpokes, 80 | setup: function() { 81 | let path = new Pather([0, 1]) 82 | .lineTo([0.25, 1]) 83 | .lineTo([0, 0.5]) 84 | .lineTo([0.5, 0]) 85 | .lineTo([1, 0.5]) 86 | .lineTo([0.75, 1]) 87 | .lineTo([1, 1]) 88 | .path; 89 | let opts = newoptions(); 90 | opts.capWeight = 0.25; 91 | return [path, new Skeleton(path, 0.25, opts)]; 92 | }, 93 | assert: function(path, skeleton) { 94 | assert(skeleton.waves.length == 3); 95 | } 96 | }, 97 | { 98 | name: "Ω path test cap=0.75", 99 | draw: drawWithoutSpokes, 100 | setup: function() { 101 | let path = new Pather([0, 1]) 102 | .lineTo([0.25, 1]) 103 | .lineTo([0, 0.5]) 104 | .lineTo([0.5, 0]) 105 | .lineTo([1, 0.5]) 106 | .lineTo([0.75, 1]) 107 | .lineTo([1, 1]) 108 | .path; 109 | let opts = newoptions(); 110 | opts.capWeight = 0.75; 111 | return [path, new Skeleton(path, 0.25, opts)]; 112 | }, 113 | assert: function(path, skeleton) { 114 | assert(skeleton.waves.length == 7); 115 | } 116 | }, 117 | { 118 | name: "Ω path test cap=0", 119 | draw: drawWithoutSpokes, 120 | setup: function() { 121 | let path = new Pather([0, 1]) 122 | .lineTo([0.25, 1]) 123 | .lineTo([0, 0.5]) 124 | .lineTo([0.5, 0]) 125 | .lineTo([1, 0.5]) 126 | .lineTo([0.75, 1]) 127 | .lineTo([1, 1]) 128 | .path; 129 | let opts = newoptions(); 130 | opts.capWeight = 0; 131 | return [path, new Skeleton(path, 0.25, opts)]; 132 | }, 133 | assert: function(path, skeleton) { 134 | assert(skeleton.waves.length == 3); 135 | } 136 | }, 137 | { 138 | name: "Ω path test cap=1", 139 | draw: drawWithoutSpokes, 140 | setup: function() { 141 | let path = new Pather([0, 1]) 142 | .lineTo([0.25, 1]) 143 | .lineTo([0, 0.5]) 144 | .lineTo([0.5, 0]) 145 | .lineTo([1, 0.5]) 146 | .lineTo([0.75, 1]) 147 | .lineTo([1, 1]) 148 | .path; 149 | let opts = newoptions(); 150 | opts.capWeight = 1; 151 | return [path, new Skeleton(path, 0.25, opts)]; 152 | }, 153 | assert: function(path, skeleton) { 154 | assert(skeleton.waves.length == 7); 155 | } 156 | } 157 | ]; 158 | 159 | var DebugOptions = false; 160 | for (let test of tests) { 161 | if (test.only === true) { 162 | DebugOptions = true; 163 | break; 164 | } 165 | } 166 | 167 | function newoptions() { 168 | if (DebugOptions) { 169 | return { 170 | DEBUG: true, 171 | DEBUG_DRAW_INITIAL: true, 172 | DEBUG_DRAW_INITIAL_EVENTS: false, 173 | DEBUG_DRAW_SKIPPED_EVENTS: true, 174 | DEBUG_DRAW_STEPS: true, 175 | DEBUG_DRAW_MOVE: true, 176 | DEBUG_DRAW_OBTUSE_EVENTS_EACH_STEP: false 177 | }; 178 | } 179 | return {}; 180 | } 181 | 182 | var options = newoptions(); 183 | 184 | Object.defineProperties(exports, { 185 | name: {value: "skeleton open path tests"}, 186 | tests: {value: tests} 187 | }); 188 | --------------------------------------------------------------------------------