Skip to content

Commit 5cb2f65

Browse files
authored
perf(modeling): faster geom2.toOutlines (jscad#1064)
1 parent da7bee1 commit 5cb2f65

File tree

3 files changed

+70
-58
lines changed

3 files changed

+70
-58
lines changed

packages/modeling/src/geometries/geom2/toOutlines.js

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,42 @@ const toSides = require('./toSides')
66
* Create a list of edges which SHARE vertices.
77
* This allows the edges to be traversed in order.
88
*/
9-
const toEdges = (sides) => {
10-
const vertices = {}
9+
const toSharedVertices = (sides) => {
10+
const unique = new Map() // {key: vertex}
1111
const getUniqueVertex = (vertex) => {
1212
const key = vertex.toString()
13-
if (!vertices[key]) {
14-
vertices[key] = vertex
13+
if (unique.has(key)) {
14+
return unique.get(key)
15+
} else {
16+
unique.set(key, vertex)
17+
return vertex
1518
}
16-
return vertices[key]
1719
}
1820

1921
return sides.map((side) => side.map(getUniqueVertex))
2022
}
2123

24+
/*
25+
* Convert a list of sides into a map from vertex to edges.
26+
*/
27+
const toVertexMap = (sides) => {
28+
const vertexMap = new Map()
29+
// first map to edges with shared vertices
30+
const edges = toSharedVertices(sides)
31+
// construct adjacent edges map
32+
edges.forEach((edge) => {
33+
if (vertexMap.has(edge[0])) {
34+
vertexMap.get(edge[0]).push(edge)
35+
} else {
36+
vertexMap.set(edge[0], [edge])
37+
}
38+
})
39+
return vertexMap
40+
}
41+
2242
/**
2343
* Create the outline(s) of the given geometry.
24-
* @param {geom2} geometry
44+
* @param {geom2} geometry - geometry to create outlines from
2545
* @returns {Array} an array of outlines, where each outline is an array of ordered points
2646
* @alias module:modeling/geometries/geom2.toOutlines
2747
*
@@ -30,65 +50,35 @@ const toEdges = (sides) => {
3050
* let outlines = toOutlines(geometry) // returns two outlines
3151
*/
3252
const toOutlines = (geometry) => {
33-
const vertexMap = new Map()
34-
const edges = toEdges(toSides(geometry))
35-
edges.forEach((edge) => {
36-
if (!(vertexMap.has(edge[0]))) {
37-
vertexMap.set(edge[0], [])
38-
}
39-
const sideslist = vertexMap.get(edge[0])
40-
sideslist.push(edge)
41-
})
42-
53+
const vertexMap = toVertexMap(toSides(geometry)) // {vertex: [edges]}
4354
const outlines = []
4455
while (true) {
45-
let startside
56+
let startSide
4657
for (const [vertex, edges] of vertexMap) {
47-
startside = edges.shift()
48-
if (!startside) {
58+
startSide = edges.shift()
59+
if (!startSide) {
4960
vertexMap.delete(vertex)
5061
continue
5162
}
5263
break
5364
}
54-
if (startside === undefined) break // all starting sides have been visited
65+
if (startSide === undefined) break // all starting sides have been visited
5566

5667
const connectedVertexPoints = []
57-
const startvertex = startside[0]
58-
const v0 = vec2.create()
68+
const startVertex = startSide[0]
5969
while (true) {
60-
connectedVertexPoints.push(startside[0])
61-
const nextvertex = startside[1]
62-
if (nextvertex === startvertex) break // the outline has been closed
63-
const nextpossiblesides = vertexMap.get(nextvertex)
64-
if (!nextpossiblesides) {
65-
throw new Error('the given geometry is not closed. verify proper construction')
70+
connectedVertexPoints.push(startSide[0])
71+
const nextVertex = startSide[1]
72+
if (nextVertex === startVertex) break // the outline has been closed
73+
const nextPossibleSides = vertexMap.get(nextVertex)
74+
if (!nextPossibleSides) {
75+
throw new Error(`geometry is not closed at vertex ${nextVertex}`)
6676
}
67-
let nextsideindex = -1
68-
if (nextpossiblesides.length === 1) {
69-
nextsideindex = 0
70-
} else {
71-
// more than one side starting at the same vertex
72-
let bestangle
73-
const startangle = vec2.angleDegrees(vec2.subtract(v0, startside[1], startside[0]))
74-
for (let sideindex = 0; sideindex < nextpossiblesides.length; sideindex++) {
75-
const nextpossibleside = nextpossiblesides[sideindex]
76-
const nextangle = vec2.angleDegrees(vec2.subtract(v0, nextpossibleside[1], nextpossibleside[0]))
77-
let angledif = nextangle - startangle
78-
if (angledif < -180) angledif += 360
79-
if (angledif >= 180) angledif -= 360
80-
if ((nextsideindex < 0) || (angledif > bestangle)) {
81-
nextsideindex = sideindex
82-
bestangle = angledif
83-
}
84-
}
77+
const nextSide = popNextSide(startSide, nextPossibleSides)
78+
if (nextPossibleSides.length === 0) {
79+
vertexMap.delete(nextVertex)
8580
}
86-
const nextside = nextpossiblesides[nextsideindex]
87-
nextpossiblesides.splice(nextsideindex, 1) // remove side from list
88-
if (nextpossiblesides.length === 0) {
89-
vertexMap.delete(nextvertex)
90-
}
91-
startside = nextside
81+
startSide = nextSide
9282
} // inner loop
9383

9484
// due to the logic of fromPoints()
@@ -102,4 +92,28 @@ const toOutlines = (geometry) => {
10292
return outlines
10393
}
10494

95+
// find the first counter-clockwise edge from startSide and pop from nextSides
96+
const popNextSide = (startSide, nextSides) => {
97+
if (nextSides.length === 1) {
98+
return nextSides.pop()
99+
}
100+
const v0 = vec2.create()
101+
const startAngle = vec2.angleDegrees(vec2.subtract(v0, startSide[1], startSide[0]))
102+
let bestAngle
103+
let bestIndex
104+
nextSides.forEach((nextSide, index) => {
105+
const nextAngle = vec2.angleDegrees(vec2.subtract(v0, nextSide[1], nextSide[0]))
106+
let angle = nextAngle - startAngle
107+
if (angle < -180) angle += 360
108+
if (angle >= 180) angle -= 360
109+
if (bestIndex === undefined || angle > bestAngle) {
110+
bestIndex = index
111+
bestAngle = angle
112+
}
113+
})
114+
const nextSide = nextSides[bestIndex]
115+
nextSides.splice(bestIndex, 1) // remove side from list
116+
return nextSide
117+
}
118+
105119
module.exports = toOutlines

packages/modeling/src/operations/modifiers/reTesselateCoplanarPolygons.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const reTesselateCoplanarPolygons = (sourcepolygons) => {
3131
// convert all polygon vertices to 2D
3232
// Make a list of all encountered y coordinates
3333
// And build a map of all polygons that have a vertex at a certain y coordinate:
34-
const ycoordinateBinningFactor = 1.0 / EPS * 10
34+
const ycoordinateBinningFactor = 10 / EPS
3535
for (let polygonindex = 0; polygonindex < numpolygons; polygonindex++) {
3636
const poly3d = sourcepolygons[polygonindex]
3737
let vertices2d = []

packages/utils/regl-renderer/src/geometry-utils-V2/geom3ToGeometries.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ const geom3ToGeometries = (options, solid) => {
4949

5050
const polygonIndices = []
5151
for (let j = 0; j < vertices.length; j++) {
52-
const vertex = vertices[j]
52+
const position = vertices[j]
5353

54-
const position = [vertex[0], vertex[1], vertex[2]]
5554
positions.push(position)
5655
normals.push(normal)
5756
colors.push(faceColor)
@@ -110,12 +109,11 @@ const smoothing = () => {
110109
const normal = calculateNormal(polygon)
111110
if (faceColor && faceColor[3] !== 1) isTransparent = true
112111
const polygonIndices = []
113-
// we need unique tupples of normal + position , that gives us a specific index (indices)
112+
// we need unique tupples of normal + position, that gives us a specific index (indices)
114113
// if the angle between a given normal and another normal is less than X they are considered the same
115114
for (let j = 0; j < vertices.length; j++) {
116115
let index
117-
const vertex = vertices[j]
118-
const position = [vertex[0], vertex[1], vertex[2]]
116+
const position = vertices[j]
119117
if (smoothLighting) {
120118
const candidateTupple = { normal, position }
121119
const existingTupple = fuzyNormalAndPositionLookup(normalPositionLookup, candidateTupple, normalThreshold)

0 commit comments

Comments
 (0)