Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test cases and JSDOC comments for Path2D #52

Merged
merged 11 commits into from
Sep 10, 2017
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')
})