Skip to content

Commit

Permalink
Merge branch 'feature/non-zero-rule'
Browse files Browse the repository at this point in the history
Fixes #57
  • Loading branch information
mfogel committed Mar 29, 2019
2 parents 67dbfec + a7c781d commit 68adcca
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 190 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

## vNext (in development)

* Change winding rule from [even-odd](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) to [non-zero](https://en.wikipedia.org/wiki/Nonzero-rule) ([#57](https://github.com/mfogel/polygon-clipping/issues/57))
* Performance improvements ([#55](https://github.com/mfogel/polygon-clipping/issues/55))
* Bug fixes (more instances of [#60](https://github.com/mfogel/polygon-clipping/issues/60))

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Each positional argument (`<geom>`) may be either a Polygon or a MultiPolygon. T
* MultiPolygons may contain touching or overlapping Polygons.
* rings are not required to be self-closing.
* rings may contain repeated points, which are ignored.
* rings may be self-touching and/or self-crossing. Self-crossing rings will be interpreted using the [even-odd rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule).
* rings may be self-touching and/or self-crossing. Self-crossing rings will be interpreted using the [non-zero rule](https://en.wikipedia.org/wiki/Nonzero-rule).
* winding order of rings does not matter.
* inner rings may extend outside their outer ring. The portion of inner rings outside their outer ring is dropped.
* inner rings may touch or overlap each other.
Expand Down
165 changes: 86 additions & 79 deletions src/segment.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,42 +134,44 @@ export default class Segment {
return 0
}

/* Warning: a reference to ringsIn input will be stored,
/* Warning: a reference to ringWindings input will be stored,
* and possibly will be later modified */
constructor (leftSE, rightSE, ringsIn) {
constructor (leftSE, rightSE, rings, windings) {
this.id = ++segmentId
this.leftSE = leftSE
leftSE.segment = this
leftSE.otherSE = rightSE
this.rightSE = rightSE
rightSE.segment = this
rightSE.otherSE = leftSE
this.ringsIn = ringsIn
this._cache = {}
this.rings = rings
this.windings = windings
// left unset for performance, set later in algorithm
// this.ringOut, this.consumedBy, this.prev
}

static fromRing(pt1, pt2, ring) {
let leftPt, rightPt
let leftPt, rightPt, winding

// ordering the two points according to sweep line ordering
const cmpPts = SweepEvent.comparePoints(pt1, pt2)
if (cmpPts < 0) {
leftPt = pt1
rightPt = pt2
winding = 1
}
else if (cmpPts > 0) {
leftPt = pt2
rightPt = pt1
winding = -1
}
else throw new Error(
`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`
)

const leftSE = new SweepEvent(leftPt, true)
const rightSE = new SweepEvent(rightPt, false)
return new Segment(leftSE, rightSE, [ring])
return new Segment(leftSE, rightSE, [ring], [winding])
}

/* When a segment is split, the rightSE is replaced with a new sweep event */
Expand Down Expand Up @@ -372,7 +374,9 @@ export default class Segment {
this.replaceRightSE(newRightSE)
newEvents.push(newRightSE)
newEvents.push(newLeftSE)
const newSeg = new Segment(newLeftSE, oldRightSE, this.ringsIn.slice())
const newSeg = new Segment(
newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice()
)

// when splitting a nearly vertical downward-facing segment,
// sometimes one of the resulting new segments is vertical, in which
Expand Down Expand Up @@ -402,9 +406,12 @@ export default class Segment {
this.leftSE = tmpEvt
this.leftSE.isLeft = true
this.rightSE.isLeft = false
for (let i = 0, iMax = this.windings.length; i < iMax; i++) {
this.windings[i] *= -1
}
}

/* Consume another segment. We take their ringsIn under our wing
/* Consume another segment. We take their rings under our wing
* and mark them as consumed. Use for perfectly overlapping segments */
consume (other) {
let consumer = this
Expand All @@ -429,10 +436,18 @@ export default class Segment {
consumee = tmp
}

for (let i = 0, iMax = consumee.ringsIn.length; i < iMax; i++) {
consumer.ringsIn.push(consumee.ringsIn[i])
for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {
const ring = consumee.rings[i]
const winding = consumee.windings[i]
const index = consumer.rings.indexOf(ring)
if (index === -1) {
consumer.rings.push(ring)
consumer.windings.push(winding)
}
else consumer.windings[index] += winding
}
consumee.ringsIn = null
consumee.rings = null
consumee.windings = null
consumee.consumedBy = consumer

// mark sweep events consumed as to maintain ordering in sweep event queue
Expand All @@ -442,68 +457,57 @@ export default class Segment {

/* The first segment previous segment chain that is in the result */
prevInResult () {
const key = 'prevInResult'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
}

_prevInResult () {
if (! this.prev) return null
if (this.prev.isInResult()) return this.prev
return this.prev.prevInResult()
}

ringsBefore () {
const key = 'ringsBefore'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
if (this._prevInResult !== undefined) return this._prevInResult
if (! this.prev) this._prevInResult = null
else if (this.prev.isInResult()) this._prevInResult = this.prev
else this._prevInResult = this.prev.prevInResult()
return this._prevInResult
}

_ringsBefore () {
if (! this.prev) return []
return (this.prev.consumedBy || this.prev).ringsAfter()
}

ringsAfter () {
const key = 'ringsAfter'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
}

_ringsAfter () {
const rings = this.ringsBefore().slice(0)
for (let i = 0, iMax = this.ringsIn.length; i < iMax; i++) {
const ring = this.ringsIn[i]
const index = rings.indexOf(ring)
if (index === -1) rings.push(ring)
else rings.splice(index, 1)
beforeState() {
if (this._beforeState !== undefined) return this._beforeState
if (! this.prev) this._beforeState = {
rings: [],
windings: [],
multiPolys: [],
}
return rings
}

multiPolysBefore () {
const key = 'multiPolysBefore'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
else {
const seg = this.prev.consumedBy || this.prev
this._beforeState = seg.afterState()
}
return this._beforeState
}

_multiPolysBefore () {
if (! this.prev) return []
return (this.prev.consumedBy || this.prev).multiPolysAfter()
}
afterState () {
if (this._afterState !== undefined) return this._afterState

multiPolysAfter () {
const key = 'multiPolysAfter'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
}
const beforeState = this.beforeState()
this._afterState = {
rings: beforeState.rings.slice(0),
windings: beforeState.windings.slice(0),
multiPolys: []
}
const ringsAfter = this._afterState.rings
const windingsAfter = this._afterState.windings
const mpsAfter = this._afterState.multiPolys

// calculate ringsAfter, windingsAfter
for (let i = 0, iMax = this.rings.length; i < iMax; i++) {
const ring = this.rings[i]
const winding = this.windings[i]
const index = ringsAfter.indexOf(ring)
if (index === -1) {
ringsAfter.push(ring)
windingsAfter.push(winding)
}
else windingsAfter[index] += winding
}

_multiPolysAfter () {
// first calcualte our polysAfter
// calcualte polysAfter
const polysAfter = []
const polysExclude = []
const ringsAfter = this.ringsAfter()
for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {
if (windingsAfter[i] === 0) continue // non-zero rule
const ring = ringsAfter[i]
const poly = ring.poly
if (polysExclude.indexOf(poly) !== -1) continue
Expand All @@ -514,28 +518,25 @@ export default class Segment {
if (index !== -1) polysAfter.splice(index, 1)
}
}
// now calculate our multiPolysAfter
const mps = []

// calculate multiPolysAfter
for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {
const mp = polysAfter[i].multiPoly
if (mps.indexOf(mp) === -1) mps.push(mp)
if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp)
}
return mps

return this._afterState
}

/* Is this segment part of the final result? */
isInResult () {
const key = 'isInResult'
if (this._cache[key] === undefined) this._cache[key] = this[`_${key}`]()
return this._cache[key]
}

_isInResult () {
// if we've been consumed, we're not in the result
if (this.consumedBy) return false

const mpsBefore = this.multiPolysBefore()
const mpsAfter = this.multiPolysAfter()
if (this._isInResult !== undefined) return this._isInResult

const mpsBefore = this.beforeState().multiPolys
const mpsAfter = this.afterState().multiPolys

switch (operation.type) {
case 'union': {
Expand All @@ -544,7 +545,8 @@ export default class Segment {
// * On the other side there is 1 or more.
const noBefores = mpsBefore.length === 0
const noAfters = mpsAfter.length === 0
return noBefores !== noAfters
this._isInResult = noBefores !== noAfters
break
}

case 'intersection': {
Expand All @@ -561,27 +563,32 @@ export default class Segment {
least = mpsAfter.length
most = mpsBefore.length
}
return most === operation.numMultiPolys && least < most
this._isInResult = most === operation.numMultiPolys && least < most
break
}

case 'xor': {
// XOR - included iff:
// * the difference between the number of multipolys represented
// with poly interiors on our two sides is an odd number
const diff = Math.abs(mpsBefore.length - mpsAfter.length)
return diff % 2 === 1
this._isInResult = diff % 2 === 1
break
}

case 'difference': {
// DIFFERENCE included iff:
// * on exactly one side, we have just the subject
const isJustSubject = mps => mps.length === 1 && mps[0].isSubject
return isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)
this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)
break
}

default:
throw new Error(`Unrecognized operation type found ${operation.type}`)
}

return this._isInResult
}

}
15 changes: 0 additions & 15 deletions test/end-to-end/even-odd-rule-not-non-zero-winding/all.geojson

This file was deleted.

23 changes: 5 additions & 18 deletions test/end-to-end/multipoly-with-self-crossing-rings/all.geojson
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,14 @@
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[[0, 0], [4, 0], [2, 2], [4, 4], [0, 4], [0, 0]],
[[2, 2], [1, 1], [1, 3], [2, 2]]
],
[
[[0, 10], [4, 10], [2, 12], [4, 14], [0, 14], [0, 10]],
[[2, 12], [1, 11], [1, 13], [2, 12]]
],
[
[[10, 0], [14, 0], [12, 2], [14, 4], [10, 4], [10, 0]],
[[12, 2], [11, 1], [11, 3], [12, 2]]
],
[
[[10, 10], [14, 10], [14, 14], [10, 10]],
[[10, 10], [12, 11], [13, 11], [10, 10]]
],
[[[0, 0], [4, 0], [2, 2], [4, 4], [0, 4], [0, 0]]],
[[[0, 10], [4, 10], [2, 12], [4, 14], [0, 14], [0, 10]]],
[[[10, 0], [14, 0], [12, 2], [14, 4], [10, 4], [10, 0]]],
[[[10, 10], [14, 10], [14, 14], [10, 10]]],
[
[[20, 0], [26, 0], [26, 6], [20, 6], [20, 0]],
[[21, 1], [21, 5], [25, 5], [23, 3], [25, 1], [21, 1]]
],
[[[23, 3], [22, 4], [22, 2], [23, 3]]]
]
]
}
}
25 changes: 25 additions & 0 deletions test/end-to-end/non-zero-rule-not-even-odd/all.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[0, 0],
[1, 0],
[1, -1],
[2, -1],
[2, 0],
[4, 0],
[4, 3],
[1, 3],
[1, 1],
[0, 1],
[0, 0]
],
[[2, 1], [2, 2], [3, 2], [3, 1], [2, 1]]
]
]
}
}
Loading

0 comments on commit 68adcca

Please sign in to comment.