Skip to content

Commit

Permalink
fix(path2D): added missing innerToCAG method to path2D proto & added …
Browse files Browse the repository at this point in the history
…tests & docs (#52)

* Changed checkIfConvex() to return boolean
* Changed Polygon constructor to throw error
* Added JSDOC comments
* Added tests for Polygon.Shared
* Addition JSDOC comments for Polygon.Shared
* Added simple tests for transformations
* Added simple tests for CAG and CSG conversions
* Test suites for Path2D and supporting functions
* Added JSDOC compatible comments
* Added missing innerToCAG() method to Path2D prototype
* added basic test to creates CAG from paths
* Rewrote some comments to JSDOC standards
* Added JSDOC comments for appendBezier()
  • Loading branch information
z3dev authored and kaosat-dev committed Sep 10, 2017
1 parent 94a8155 commit 4a5e37e
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 35 deletions.
127 changes: 92 additions & 35 deletions src/math/Path2.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ const {parseOptionAs2DVector, parseOptionAsFloat, parseOptionAsInt, parseOptionA
const {defaultResolution2D} = require('../constants')
const Vertex = require('./Vertex2')
const Side = require('./Side')
// const {fromSides, fromPoints} = require('../CAGMakers')

// # Class Path2D
/** Class Path2D
* Represents a series of points, connected by infinitely thin lines.
* A path can be open or closed, i.e. additional line between first and last points.
* The difference between Path2D and CAG is that a path is a 'thin' line, whereas a CAG is an enclosed area.
* @constructor
* @param {Vector2D[]} [points=[]] - list of points
* @param {boolean} [closed=false] - closer of path
*
* @example
* new CSG.Path2D()
* new CSG.Path2D([[10,10], [-10,10], [-10,-10], [10,-10]], true) // closed
*/
const Path2D = function (points, closed) {
closed = !!closed
points = points || []
Expand All @@ -31,21 +41,26 @@ const Path2D = function (points, closed) {
this.closed = closed
}

/*
Construct a (part of a) circle. Parameters:
options.center: the center point of the arc (Vector2D or array [x,y])
options.radius: the circle radius (float)
options.startangle: the starting angle of the arc, in degrees
0 degrees corresponds to [1,0]
90 degrees to [0,1]
and so on
options.endangle: the ending angle of the arc, in degrees
options.resolution: number of points per 360 degree of rotation
options.maketangent: adds two extra tiny line segments at both ends of the circle
this ensures that the gradients at the edges are tangent to the circle
Returns a Path2D. The path is not closed (even if it is a 360 degree arc).
close() the resulting path if you want to create a true circle.
*/
/** Construct an arc.
* @param {Object} [options] - options for construction
* @param {Vector2D} [options.center=[0,0]] - center of circle
* @param {Number} [options.radius=1] - radius of circle
* @param {Number} [options.startangle=0] - starting angle of the arc, in degrees
* @param {Number} [options.endangle=360] - ending angle of the arc, in degrees
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @param {Boolean} [options.maketangent=false] - adds line segments at both ends of the arc to ensure that the gradients at the edges are tangent
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let path = CSG.Path2D.arc({
* center: [5, 5],
* radius: 10,
* startangle: 90,
* endangle: 180,
* resolution: 36,
* maketangent: true
* });
*/
Path2D.arc = function (options) {
let center = parseOptionAs2DVector(options, 'center', 0)
let radius = parseOptionAsFloat(options, 'radius', 1)
Expand Down Expand Up @@ -96,14 +111,19 @@ Path2D.prototype = {
},

/**
* get the array of Vector2 points that make up the path
* Get the points that make up the path.
* note that this is current internal list of points, not an immutable copy.
* @returns {Vector2[]} array of points the make up the path
*/
getPoints: function() {
return this.points;
},

/**
* Append an point to the end of the path.
* @param {Vector2D} point - point to append
* @returns {Path2D} new Path2D object (not closed)
*/
appendPoint: function (point) {
if (this.closed) {
throw new Error('Path must not be closed')
Expand All @@ -113,6 +133,11 @@ Path2D.prototype = {
return new Path2D(newpoints)
},

/**
* Append a list of points to the end of the path.
* @param {Vector2D[]} points - points to append
* @returns {Path2D} new Path2D object (not closed)
*/
appendPoints: function (points) {
if (this.closed) {
throw new Error('Path must not be closed')
Expand All @@ -129,8 +154,8 @@ Path2D.prototype = {
},

/**
* Tell whether the path is a closed path or not
* @returns {boolean} true when the path is closed. false otherwise.
* Determine if the path is a closed or not.
* @returns {Boolean} true when the path is closed, otherwise false
*/
isClosed: function() {
return this.closed
Expand Down Expand Up @@ -174,13 +199,38 @@ Path2D.prototype = {
return expanded
},

innerToCAG: function() {
const CAG = require('../CAG') // FIXME: cyclic dependencies CAG => PATH2 => CAG
if (!this.closed) throw new Error("The path should be closed!");
return CAG.fromPoints(this.points);
},

transform: function (matrix4x4) {
let newpoints = this.points.map(function (point) {
return point.multiply4x4(matrix4x4)
})
return new Path2D(newpoints, this.closed)
},

/**
* Append a Bezier curve to the end of the path, using the control points to transition the curve through start and end points.
* <br>
* The Bézier curve starts at the last point in the path,
* and ends at the last given control point. Other control points are intermediate control points.
* <br>
* The first control point may be null to ensure a smooth transition occurs. In this case,
* the second to last control point of the path is mirrored into the control points of the Bezier curve.
* In other words, the trailing gradient of the path matches the new gradient of the curve.
* @param {Vector2D[]} controlpoints - list of control points
* @param {Object} [options] - options for construction
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let p5 = new CSG.Path2D([[10,-20]],false);
* p5 = p5.appendBezier([[10,-10],[25,-10],[25,-20]]);
* p5 = p5.appendBezier([[25,-30],[40,-30],[40,-20]]);
*/
appendBezier: function (controlpoints, options) {
if (arguments.length < 2) {
options = {}
Expand Down Expand Up @@ -295,21 +345,28 @@ Path2D.prototype = {
return result
},

/*
options:
.resolution // smoothness of the arc (number of segments per 360 degree of rotation)
// to create a circular arc:
.radius
// to create an elliptical arc:
.xradius
.yradius
.xaxisrotation // the rotation (in degrees) of the x axis of the ellipse with respect to the x axis of our coordinate system
// this still leaves 4 possible arcs between the two given points. The following two flags select which one we draw:
.clockwise // = true | false (default is false). Two of the 4 solutions draw clockwise with respect to the center point, the other 2 counterclockwise
.large // = true | false (default is false). Two of the 4 solutions are an arc longer than 180 degrees, the other two are <= 180 degrees
This implementation follows the SVG arc specs. For the details see
http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
*/

/**
* Append an arc to the end of the path.
* This implementation follows the SVG arc specs. For the details see
* http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
* @param {Vector2D} endpoint - end point of arc
* @param {Object} [options] - options for construction
* @param {Number} [options.radius=0] - radius of arc (X and Y), see also xradius and yradius
* @param {Number} [options.xradius=0] - X radius of arc, see also radius
* @param {Number} [options.yradius=0] - Y radius of arc, see also radius
* @param {Number} [options.xaxisrotation=0] - rotation (in degrees) of the X axis of the arc with respect to the X axis of the coordinate system
* @param {Number} [options.resolution=defaultResolution2D] - number of sides per 360 rotation
* @param {Boolean} [options.clockwise=false] - draw an arc clockwise with respect to the center point
* @param {Boolean} [options.large=false] - draw an arc longer than 180 degrees
* @returns {Path2D} new Path2D object (not closed)
*
* @example
* let p1 = new CSG.Path2D([[27.5,-22.96875]],false);
* p1 = p1.appendPoint([27.5,-3.28125]);
* p1 = p1.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: false,large: false});
* p1 = p1.close();
*/
appendArc: function (endpoint, options) {
let decimals = 100000
if (arguments.length < 2) {
Expand Down
154 changes: 154 additions & 0 deletions test/csg-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import test from 'ava'
import {CSG} from '../csg'
import {OBJ} from './helpers/obj-store'
import {assertSameGeometry} from './helpers/asserts'

// Testing common shape generation can only be done by comparing
// with previously human validated shapes. It would be trivially
// rewriting the generation code to test it with code instead.

function isValid (t, name, observed) {
const expected = OBJ.loadPrevious('csg-shapes.' + name, observed)
assertSameGeometry(t, observed, expected)
}

test('CSG.Path2D constructor creates an empty path', t => {
let p1 = new CSG.Path2D()

t.is(typeof p1, 'object')
t.false(p1.isClosed())
t.is(p1.getPoints().length,0)

// make sure methods work on empty paths
let p2 = new CSG.Path2D()
let p3 = p1.concat(p2)

t.false(p3.isClosed())
t.is(p3.getPoints().length,0)

let matrix = CSG.Matrix4x4.rotationX(90)
p3 = p2.transform(matrix)

t.false(p3.isClosed())
t.is(p3.getPoints().length,0)

p3 = p2.appendPoint([1,1])

t.false(p3.isClosed())
t.is(p3.getPoints().length,1)
t.false(p2.isClosed())
t.is(p2.getPoints().length,0)

p3 = p2.appendPoints(p1.getPoints())

t.false(p3.isClosed())
t.is(p3.getPoints().length,0)

// test that close is possible
let p4 = p3.close()

t.true(p4.isClosed())
t.is(p4.getPoints().length,0)

})

test('CSG.Path2D.arc() creates correct paths', t => {
// test default options
let a1 = CSG.Path2D.arc()
let p1 = a1.getPoints()

t.false(a1.isClosed())
t.is(p1.length,34)
t.deepEqual(p1[0],new CSG.Vector2D([1,0]))

// test center
let a2 = CSG.Path2D.arc({center: [5,5]})
let p2 = a2.getPoints()

t.false(a2.isClosed())
t.is(p2.length,34)
t.deepEqual(p2[0],new CSG.Vector2D([6,5]))

// test radius (with center)
let a3 = CSG.Path2D.arc({center: [5,5],radius: 10})
let p3 = a3.getPoints()

t.false(a3.isClosed())
t.is(p3.length,34)
t.deepEqual(p3[0],new CSG.Vector2D([15,5]))

// test start angle (with radius)
let a4 = CSG.Path2D.arc({radius: 10,startangle: 180})
let p4 = a4.getPoints()

t.false(a4.isClosed())
t.is(p4.length,18)
//t.deepEqual(p4[0],new CSG.Vector2D([10,0]))

// test end angle (with center)
let a5 = CSG.Path2D.arc({center: [5,5],endangle: 90})
let p5 = a5.getPoints()

t.false(a5.isClosed())
t.is(p5.length,10)
t.deepEqual(p5[0],new CSG.Vector2D([6,5]))

// test resolution (with radius)
let a6 = CSG.Path2D.arc({radius: 10,resolution: 144})
let p6 = a6.getPoints()

t.false(a6.isClosed())
t.is(p6.length,146)
t.deepEqual(p6[0],new CSG.Vector2D([10,0]))

// test make tangent (with radius)
let a7 = CSG.Path2D.arc({radius: 10,maketangent: true})
let p7 = a7.getPoints()

t.false(a7.isClosed())
t.is(p7.length,36)
t.deepEqual(p7[0],new CSG.Vector2D([10,0]))
})

test('CSG.Path2D creates CAG from paths', t => {
let p1 = new CSG.Path2D([[27.5,-22.96875]],false);
p1 = p1.appendPoint([27.5,-3.28125]);
p1 = p1.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: false,large: false});
p1 = p1.close();

let cag01 = p1.innerToCAG();
t.is(typeof cag01, 'object')

let p2 = new CSG.Path2D([[27.5,-22.96875]],false);
p2 = p2.appendPoint([27.5,-3.28125]);
p2 = p2.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: false,large: true});
p2 = p2.close();

let cag02 = p2.innerToCAG();
t.is(typeof cag02, 'object')

let p3 = new CSG.Path2D([[27.5,-22.96875]],false);
p3 = p3.appendPoint([27.5,-3.28125]);
p3 = p3.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: true,large: true});
p3 = p3.close();

let cag03 = p3.innerToCAG();
t.is(typeof cag03, 'object')

let p4 = new CSG.Path2D([[27.5,-22.96875]],false);
p4 = p4.appendPoint([27.5,-3.28125]);
p4 = p4.appendArc([12.5,-22.96875],{xradius: 15,yradius: -19.6875,xaxisrotation: 0,clockwise: true,large: false});
p4 = p4.close();

let cag04 = p4.innerToCAG();
t.is(typeof cag04, 'object')

let p5 = new CSG.Path2D([[10,-20]],false);
p5 = p5.appendBezier([[10,-10],[25,-10],[25,-20]]);
p5 = p5.appendBezier([[25,-30],[40,-30],[40,-20]]);

let cag05 = p5.expandToCAG(0.05,CSG.defaultResolution2D);
t.is(typeof cag05, 'object')
})


0 comments on commit 4a5e37e

Please sign in to comment.