Skip to content

Commit f895e61

Browse files
committed
perf(modeling): rework martinez for performance as well as modernizing code
1 parent 36c6ff0 commit f895e61

File tree

11 files changed

+185
-606
lines changed

11 files changed

+185
-606
lines changed

packages/modeling/src/operations/booleans/martinez/compareEvents.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,18 @@ export const compareEvents = (e1, e2) => {
2323
// Event with lower y-coordinate is processed first
2424
if (p1[1] !== p2[1]) return p1[1] > p2[1] ? 1 : -1
2525

26-
return specialCases(e1, e2, p1, p2)
27-
}
28-
29-
const specialCases = (e1, e2, p1, p2) => {
3026
// Same coordinates, but one is a left endpoint and the other is
3127
// a right endpoint. The right endpoint is processed first
3228
if (e1.left !== e2.left) { return e1.left ? 1 : -1 }
3329

34-
// const p2 = e1.otherEvent.point, p3 = e2.otherEvent.point
35-
// const sa = (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1])
3630
// Same coordinates, both events
3731
// are left endpoints or right endpoints.
3832
// not collinear
3933
if (signedArea(p1, e1.otherEvent.point, e2.otherEvent.point) !== 0) {
40-
// the event associate to the bottom segment is processed first
34+
// the event associate to the lowest segment is processed first
4135
return (!e1.isBelow(e2.otherEvent.point)) ? 1 : -1
4236
}
4337

38+
// Same coordinates, subject events are processed first
4439
return (!e1.isSubject && e2.isSubject) ? 1 : -1
4540
}

packages/modeling/src/operations/booleans/martinez/connectEdges.js

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,39 @@ import { Contour } from './contour.js'
1212
* @return {SweepEvent[]}
1313
*/
1414
const orderEvents = (sortedEvents) => {
15-
let event, i, len, tmp
1615
const resultEvents = []
17-
for (i = 0, len = sortedEvents.length; i < len; i++) {
18-
event = sortedEvents[i]
19-
if ((event.left && event.inResult) ||
20-
(!event.left && event.otherEvent.inResult)) {
21-
resultEvents.push(event)
16+
sortedEvents.forEach((e) => {
17+
if ((e.left && e.inResult) || (!e.left && e.otherEvent.inResult)) {
18+
resultEvents.push(e)
2219
}
23-
}
20+
})
2421
// Due to overlapping edges the resultEvents array can be not wholly sorted
2522
let sorted = false
2623
while (!sorted) {
2724
sorted = true
28-
for (i = 0, len = resultEvents.length; i < len; i++) {
25+
const len = resultEvents.length
26+
for (let i = 0; i < len; i++) {
2927
if ((i + 1) < len &&
3028
compareEvents(resultEvents[i], resultEvents[i + 1]) === 1) {
31-
tmp = resultEvents[i]
29+
const tmp = resultEvents[i]
3230
resultEvents[i] = resultEvents[i + 1]
3331
resultEvents[i + 1] = tmp
3432
sorted = false
3533
}
3634
}
3735
}
38-
39-
for (i = 0, len = resultEvents.length; i < len; i++) {
40-
event = resultEvents[i]
41-
event.otherPos = i
42-
}
36+
// and index
37+
resultEvents.forEach((e, i) => { e.otherPos = i })
4338

4439
// imagine, the right event is found in the beginning of the queue,
4540
// when his left counterpart is not marked yet
46-
for (i = 0, len = resultEvents.length; i < len; i++) {
47-
event = resultEvents[i]
48-
if (!event.left) {
49-
tmp = event.otherPos
50-
event.otherPos = event.otherEvent.otherPos
51-
event.otherEvent.otherPos = tmp
41+
resultEvents.forEach((e) => {
42+
if (!e.left) {
43+
const otherPos = e.otherPos
44+
e.otherPos = e.otherEvent.otherPos
45+
e.otherEvent.otherPos = otherPos
5246
}
53-
}
47+
})
5448

5549
return resultEvents
5650
}
@@ -62,14 +56,15 @@ const orderEvents = (sortedEvents) => {
6256
* @return {number}
6357
*/
6458
const nextPos = (pos, resultEvents, processed, origPos) => {
65-
let newPos = pos + 1
66-
const p = resultEvents[pos].point
67-
let p1
6859
const length = resultEvents.length
6960

61+
const p0 = resultEvents[pos].point
62+
63+
let newPos = pos + 1
64+
let p1
7065
if (newPos < length) { p1 = resultEvents[newPos].point }
7166

72-
while (newPos < length && p1[0] === p[0] && p1[1] === p[1]) {
67+
while (newPos < length && p1[0] === p0[0] && p1[1] === p0[1]) {
7368
if (!processed[newPos]) {
7469
return newPos
7570
} else {
@@ -81,7 +76,6 @@ const nextPos = (pos, resultEvents, processed, origPos) => {
8176
}
8277

8378
newPos = pos - 1
84-
8579
while (processed[newPos] && newPos > origPos) {
8680
newPos--
8781
}
@@ -90,7 +84,8 @@ const nextPos = (pos, resultEvents, processed, origPos) => {
9084
}
9185

9286
const initializeContourFromContext = (event, contours, contourId) => {
93-
const contour = new Contour()
87+
const contour = new Contour() // default is exterior contour of depth 0
88+
9489
if (event.prevInResult != null) {
9590
const prevInResult = event.prevInResult
9691
// Note that it is valid to query the "previous in result" for its output contour id,
@@ -99,6 +94,7 @@ const initializeContourFromContext = (event, contours, contourId) => {
9994
// result".
10095
const lowerContourId = prevInResult.outputContourId
10196
const lowerResultTransition = prevInResult.resultTransition
97+
10298
if (lowerContourId < 0) {
10399
contour.holeOf = null
104100
contour.depth = 0
@@ -122,30 +118,24 @@ const initializeContourFromContext = (event, contours, contourId) => {
122118
}
123119
} else {
124120
// We are outside => this contour is an exterior contour of same depth.
125-
contour.holeOf = null
126121
contour.depth = contours[lowerContourId].depth
127122
}
128-
} else {
129-
// There is no lower/previous contour => this contour is an exterior contour of depth 0.
130-
contour.holeOf = null
131-
contour.depth = 0
132123
}
133124
return contour
134125
}
135126

136127
/**
137128
* @param {Array.<SweepEvent>} sortedEvents
138-
* @return {Array.<*>} polygons
129+
* @return array of Contour
139130
*/
140131
export const connectEdges = (sortedEvents) => {
141132
const resultEvents = orderEvents(sortedEvents)
142-
const len = resultEvents.length
133+
const evlen = resultEvents.length
143134

144-
// "false"-filled array
145-
const processed = {}
135+
const processed = []
146136
const contours = []
147137

148-
for (let i = 0; i < len; i++) {
138+
for (let i = 0; i < evlen; i++) {
149139
if (processed[i]) {
150140
continue
151141
}
@@ -156,16 +146,15 @@ export const connectEdges = (sortedEvents) => {
156146
// Helper function that combines marking an event as processed with assigning its output contour ID
157147
const markAsProcessed = (pos) => {
158148
processed[pos] = true
159-
if (pos < resultEvents.length && resultEvents[pos]) {
149+
if (pos < evlen) {
160150
resultEvents[pos].outputContourId = contourId
161151
}
162152
}
163153

164154
let pos = i
165155
const origPos = i
166156

167-
const initial = resultEvents[i].point
168-
contour.points.push(initial)
157+
contour.points.push(resultEvents[pos].point)
169158

170159
while (true) {
171160
markAsProcessed(pos)
@@ -177,7 +166,7 @@ export const connectEdges = (sortedEvents) => {
177166

178167
pos = nextPos(pos, resultEvents, processed, origPos)
179168

180-
if (pos === origPos || pos >= resultEvents.length || !resultEvents[pos]) {
169+
if (pos === origPos || pos >= evlen) {
181170
break
182171
}
183172
}

packages/modeling/src/operations/booleans/martinez/contour.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export class Contour {
33
this.points = []
44
this.holeIds = []
55
this.holeOf = null
6-
this.depth = null
6+
this.depth = 0
77
}
88

99
isExterior () {

packages/modeling/src/operations/booleans/martinez/divideSegment.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,29 @@ import { SweepEvent } from './sweepEvent.js'
22
import { compareEvents } from './compareEvents.js'
33

44
/**
5-
* @param {SweepEvent} se
6-
* @param {Array.<Number>} p
5+
* Divide the given segment at the given point, push the parts on the given queue.
6+
* @param {SweepEvent} segment
7+
* @param {Array.<Number>} point
78
* @param {Queue} queue
8-
* @return {Queue}
9+
* @return {Queue} given queue
910
*/
10-
export const divideSegment = (se, p, queue) => {
11-
const r = new SweepEvent(p, false, se, se.isSubject)
12-
const l = new SweepEvent(p, true, se.otherEvent, se.isSubject)
11+
export const divideSegment = (segment, point, queue) => {
12+
const r = new SweepEvent(point, false, segment, segment.isSubject)
13+
const l = new SweepEvent(point, true, segment.otherEvent, segment.isSubject)
1314

14-
r.contourId = l.contourId = se.contourId
15+
r.contourId = l.contourId = segment.contourId
1516

1617
// avoid a rounding error. The left event would be processed after the right event
17-
if (compareEvents(l, se.otherEvent) > 0) {
18-
se.otherEvent.left = true
18+
if (compareEvents(l, segment.otherEvent) > 0) {
19+
segment.otherEvent.left = true
1920
l.left = false
2021
}
2122

2223
// avoid a rounding error. The left event would be processed after the right event
2324
// if (compareEvents(se, r) > 0) {}
2425

25-
se.otherEvent.otherEvent = l
26-
se.otherEvent = r
26+
segment.otherEvent.otherEvent = l
27+
segment.otherEvent = r
2728

2829
queue.push(l)
2930
queue.push(r)

packages/modeling/src/operations/booleans/martinez/fillQueue.js

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,23 @@ import { DIFFERENCE } from './operation.js'
99
import { SweepEvent } from './sweepEvent.js'
1010
import { Queue } from './tinyqueue.js'
1111

12-
const max = Math.max
13-
const min = Math.min
12+
let externalRingId = 0
1413

15-
let contourId = 0
16-
17-
const processPolygon = (contourOrHole, isSubject, depth, queue, bbox, isExteriorRing) => {
14+
const processPolygon = (contourOrHole, isSubject, ringId, queue, bbox, isExteriorRing) => {
1815
const len = contourOrHole.length - 1
19-
let s1, s2, e1, e2
2016
for (let i = 0; i < len; i++) {
21-
s1 = contourOrHole[i]
22-
s2 = contourOrHole[i + 1]
23-
e1 = new SweepEvent(s1, false, undefined, isSubject)
24-
e2 = new SweepEvent(s2, false, e1, isSubject)
17+
const s1 = contourOrHole[i]
18+
const s2 = contourOrHole[i + 1]
19+
const e1 = new SweepEvent(s1, false, undefined, isSubject)
20+
const e2 = new SweepEvent(s2, false, e1, isSubject)
21+
2522
e1.otherEvent = e2
2623

2724
if (s1[0] === s2[0] && s1[1] === s2[1]) {
2825
continue // skip collapsed edges, or it breaks
2926
}
3027

31-
e1.contourId = e2.contourId = depth
28+
e1.contourId = e2.contourId = ringId
3229
if (!isExteriorRing) {
3330
e1.isExteriorRing = false
3431
e2.isExteriorRing = false
@@ -41,10 +38,10 @@ const processPolygon = (contourOrHole, isSubject, depth, queue, bbox, isExterior
4138

4239
const x = s1[0]
4340
const y = s1[1]
44-
bbox[0] = min(bbox[0], x)
45-
bbox[1] = min(bbox[1], y)
46-
bbox[2] = max(bbox[2], x)
47-
bbox[3] = max(bbox[3], y)
41+
bbox[0] = Math.min(bbox[0], x)
42+
bbox[1] = Math.min(bbox[1], y)
43+
bbox[2] = Math.max(bbox[2], x)
44+
bbox[3] = Math.max(bbox[3], y)
4845

4946
// Pushing it so the queue is sorted from left to right,
5047
// with object on the left having the highest priority.
@@ -55,24 +52,23 @@ const processPolygon = (contourOrHole, isSubject, depth, queue, bbox, isExterior
5552

5653
export const fillQueue = (subject, clipping, sbbox, cbbox, operation) => {
5754
const eventQueue = new Queue([], compareEvents)
58-
let polygonSet, isExteriorRing, i, ii, j, jj //, k, kk
5955

60-
for (i = 0, ii = subject.length; i < ii; i++) {
61-
polygonSet = subject[i]
62-
for (j = 0, jj = polygonSet.length; j < jj; j++) {
63-
isExteriorRing = j === 0
64-
if (isExteriorRing) contourId++
65-
processPolygon(polygonSet[j], true, contourId, eventQueue, sbbox, isExteriorRing)
56+
for (let i = 0; i < subject.length; i++) {
57+
const polygonSet = subject[i]
58+
for (let j = 0; j < polygonSet.length; j++) {
59+
const isExteriorRing = j === 0
60+
if (isExteriorRing) externalRingId++
61+
processPolygon(polygonSet[j], true, externalRingId, eventQueue, sbbox, isExteriorRing)
6662
}
6763
}
6864

69-
for (i = 0, ii = clipping.length; i < ii; i++) {
70-
polygonSet = clipping[i]
71-
for (j = 0, jj = polygonSet.length; j < jj; j++) {
72-
isExteriorRing = j === 0
65+
for (let i = 0; i < clipping.length; i++) {
66+
const polygonSet = clipping[i]
67+
for (let j = 0; j < polygonSet.length; j++) {
68+
let isExteriorRing = j === 0
7369
if (operation === DIFFERENCE) isExteriorRing = false
74-
if (isExteriorRing) contourId++
75-
processPolygon(polygonSet[j], false, contourId, eventQueue, cbbox, isExteriorRing)
70+
if (isExteriorRing) externalRingId++
71+
processPolygon(polygonSet[j], false, externalRingId, eventQueue, cbbox, isExteriorRing)
7672
}
7773
}
7874

packages/modeling/src/operations/booleans/martinez/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const boolean = (subjectGeom, clippingGeom, operation) => {
135135
// Followed by holes if any
136136
for (let j = 0; j < contour.holeIds.length; j++) {
137137
const holeId = contour.holeIds[j]
138+
// Reverse the order of points for holes
138139
const holePoints = contours[holeId].points
139140
const hole = []
140141
for (let k = holePoints.length - 2; k >= 0; k--) {
@@ -146,7 +147,7 @@ export const boolean = (subjectGeom, clippingGeom, operation) => {
146147
}
147148
}
148149

149-
if (polygons) {
150+
if (polygons.length) {
150151
return fromOutlines(polygons.flat())
151152
} else {
152153
return geom2.create()

0 commit comments

Comments
 (0)