Skip to content

Commit

Permalink
feat(2d polygons): add support for polygons (2D) with holes(#101)
Browse files Browse the repository at this point in the history
* allow multiple paths in points list + holes support
Hole detection follows the even/odd rule, which means that:
- The order of the paths is not important.
- The sense of rotation of the paths is not important.
* use functions rather than methods when possible
* add simple test
* add isPointInPolygon() helper
  • Loading branch information
Sébastien Mischler (aka skarab) authored and kaosat-dev committed Apr 22, 2018
1 parent e4606ee commit 22f8f80
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 16 deletions.
2 changes: 2 additions & 0 deletions src/api/primitives2d-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ function circle (params) {
* @example
* let poly = polygon([0,1,2,3,4])
* or
* let poly = polygon([[0,1,2,3],[4,5,6,7]])
* or
* let poly = polygon({path: [0,1,2,3,4]})
* or
* let poly = polygon({path: [0,1,2,3,4], points: [2,1,3]})
Expand Down
32 changes: 32 additions & 0 deletions src/api/primitives2d.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,35 @@ test.failing('polygon (object params, with custom paths)', t => {
t.deepEqual(obs.sides.length, 3)
t.truthy(comparePositonVertices(obs.sides, expSides))
})

test('polygon (nested points array whit holes)', t => {
const obs = polygon([
[ [0,0], [0,10], [10,10], [10,0] ],
[ [2,2], [2,8], [8,8], [8,2] ],
[ [3,3], [3,7], [7,7], [7,3] ],
[ [4,4], [4,6], [6,6], [6,4] ]
])

const expSides = [
[ [7,3], [7,7] ],
[ [7,7], [3,7] ],
[ [3,7], [3,3] ],
[ [3,3], [7,3] ],
[ [6,6], [6,4] ],
[ [4,6], [6,6] ],
[ [4,4], [4,6] ],
[ [6,4], [4,4] ],
[ [10,0], [10,10] ],
[ [10,10], [0,10] ],
[ [0,10], [0,0] ],
[ [0,0], [10,0] ],
[ [8,8], [8,2] ],
[ [2,8], [8,8] ],
[ [2,2], [2,8] ],
[ [8,2], [2,2] ],
]

// we just use a sample of points for simplicity
t.deepEqual(obs.sides.length, 16)
t.truthy(comparePositonVertices(obs.sides, expSides))
})
7 changes: 6 additions & 1 deletion src/core/CAG.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const {fromSides, fromFakeCSG} = require('./CAGFactories')

const canonicalize = require('./utils/canonicalize')
const retesselate = require('./utils/retesellate')
const {isCAGValid, isSelfIntersecting} = require('./utils/cagValidation')
const {isCAGValid, isSelfIntersecting, hasPointInside} = require('./utils/cagValidation')
const {area, getBounds} = require('./utils/cagMeasurements')

// all of these are good candidates for elimination in this scope, since they are part of a functional api
Expand Down Expand Up @@ -179,6 +179,11 @@ CAG.prototype = {
return overCutInsideCorners(this, cutterradius)
},

// ALIAS !
hasPointInside: function (point) {
return hasPointInside(this, point)
},

// All the toXXX functions
toString: function () {
let result = 'CAG (' + this.sides.length + ' sides):\n'
Expand Down
93 changes: 79 additions & 14 deletions src/core/CAGFactories.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const Side = require('./math/Side')
const Vector2D = require('./math/Vector2')
const Vertex2 = require('./math/Vertex2')
const {areaEPS} = require('./constants')
const {isSelfIntersecting} = require('./utils/cagValidation')
const {isSelfIntersecting, hasPointInside} = require('./utils/cagValidation')
const {union, difference} = require('../api/ops-booleans')

/** Construct a CAG from a list of `Side` instances.
* @param {Side[]} sides - list of sides
Expand All @@ -27,24 +28,41 @@ const fromFakeCSG = function (csg) {
return fromSides(sides)
}

/** Construct a CAG from a list of points (a polygon).
/** Construct a CAG from a list of points (a polygon) or an nested array of points.
* The rotation direction of the points is not relevant.
* The points can define a convex or a concave polygon.
* The polygon must not self intersect.
* @param {points[]} points - list of points in 2D space
* Hole detection follows the even/odd rule,
* which means that the order of the paths is not important.
* @param {points[]|Array.<points[]>} points - (nested) list of points in 2D space
* @returns {CAG} new CAG object
*/
const fromPoints = function (points) {
let numpoints = points.length
if (numpoints < 3) throw new Error('CAG shape needs at least 3 points')
if (!points) {
throw new Error('points parameter must be defined')
}
if (!Array.isArray(points)) {
throw new Error('points parameter must be an array')
}
if (points[0].x !== undefined || typeof points[0][0] === 'number') {
return fromPointsArray(points)
}
if (typeof points[0][0] === 'object') {
return fromNestedPointsArray(points)
}
throw new Error('Unsupported points list format')
}

// Do not export the two following function (code splitting for fromPoints())
const fromPointsArray = function (points) {
if (points.length < 3) {
throw new Error('CAG shape needs at least 3 points')
}
let sides = []
let prevpoint = new Vector2D(points[numpoints - 1])
let prevvertex = new Vertex2(prevpoint)
points.map(function (p) {
let point = new Vector2D(p)
let vertex = new Vertex2(point)
let side = new Side(prevvertex, vertex)
sides.push(side)
let prevvertex = new Vertex2(new Vector2D(points[points.length - 1]))
points.map(function (point) {
let vertex = new Vertex2(new Vector2D(point))
sides.push(new Side(prevvertex, vertex))
prevvertex = vertex
})
let result = fromSides(sides)
Expand All @@ -58,8 +76,55 @@ const fromPoints = function (points) {
if (area < 0) {
result = result.flipped()
}
result = result.canonicalized()
return result
return result.canonicalized()
}

const fromNestedPointsArray = function (points) {
// First pass: create a collection of CAG paths
let paths = []
points.forEach(path => {
paths.push(fromPointsArray(path))
})
// Second pass: make a tree of paths
let tree = {}
let point = null
// for each polygon extract parents and childs polygons
paths.forEach((p1, i) => {
// only check for first point in polygon
point = p1.sides[0].vertex0.pos
// check for intersection
paths.forEach((p2, y) => {
if (p1 !== p2) {
// create default node
tree[i] || (tree[i] = { parents: [], isHole: false })
tree[y] || (tree[y] = { parents: [], isHole: false })
// check if point stay in poylgon
if (hasPointInside(p2, point)) {
// push parent and child; odd parents number ==> hole
tree[i].parents.push(y)
tree[i].isHole = !! (tree[i].parents.length % 2)
tree[y].isHole = !! (tree[y].parents.length % 2)
}
}
})
})
// Third pass: subtract holes
let path = null
for (key in tree) {
path = tree[key]
if (path.isHole) {
delete tree[key] // remove holes for final pass
path.parents.forEach(parentKey => {
paths[parentKey] = difference(paths[parentKey], paths[key])
})
}
}
// Fourth and last pass: create final CAG object
let cag = fromSides([])
for (key in tree) {
cag = union(cag, paths[key])
}
return cag
}

/** Reconstruct a CAG from an object with identical property names.
Expand Down
28 changes: 27 additions & 1 deletion src/core/utils/cagValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,30 @@ const isSelfIntersecting = function (cag, debug) {
return false
}

module.exports = {isCAGValid, isSelfIntersecting}
/** Check if the point stay inside the CAG shape
* ray-casting algorithm based on :
* https://github.com/substack/point-in-polygon/blob/master/index.js
* http://www.ecse.rp1.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
* originaly writed for https://github.com/lautr3k/SLAcer.js/blob/dev/js/slacer/slicer.js#L82
* @param {CAG} cag - CAG object
* @param {Object} p0 - Vertex2 like object
* @returns {Boolean}
*/
const hasPointInside = function (cag, p0) {
let p1 = null
let p2 = null
let inside = false
cag.sides.forEach(side => {
p1 = side.vertex0.pos
p2 = side.vertex1.pos
if (hasPointInside.c1(p0, p1, p2) && hasPointInside.c2(p0, p1, p2)) {
inside = !inside
}
})
return inside
}

hasPointInside.c1 = (p0, p1, p2) => (p1.y > p0.y) !== (p2.y > p0.y)
hasPointInside.c2 = (p0, p1, p2) => (p0.x < (p2.x - p1.x) * (p0.y - p1.y) / (p2.y - p1.y) + p1.x)

module.exports = {isCAGValid, isSelfIntersecting, hasPointInside}

0 comments on commit 22f8f80

Please sign in to comment.