modeling/src/geometries/geom2/toOutlines.js

  1. const vec2 = require('../../maths/vec2')
  2. const toSides = require('./toSides')
  3. /*
  4. * Create a list of edges which SHARE vertices.
  5. * This allows the edges to be traversed in order.
  6. */
  7. const toSharedVertices = (sides) => {
  8. const unique = new Map() // {key: vertex}
  9. const getUniqueVertex = (vertex) => {
  10. const key = vertex.toString()
  11. if (unique.has(key)) {
  12. return unique.get(key)
  13. } else {
  14. unique.set(key, vertex)
  15. return vertex
  16. }
  17. }
  18. return sides.map((side) => side.map(getUniqueVertex))
  19. }
  20. /*
  21. * Convert a list of sides into a map from vertex to edges.
  22. */
  23. const toVertexMap = (sides) => {
  24. const vertexMap = new Map()
  25. // first map to edges with shared vertices
  26. const edges = toSharedVertices(sides)
  27. // construct adjacent edges map
  28. edges.forEach((edge) => {
  29. if (vertexMap.has(edge[0])) {
  30. vertexMap.get(edge[0]).push(edge)
  31. } else {
  32. vertexMap.set(edge[0], [edge])
  33. }
  34. })
  35. return vertexMap
  36. }
  37. /**
  38. * Create the outline(s) of the given geometry.
  39. * @param {geom2} geometry - geometry to create outlines from
  40. * @returns {Array} an array of outlines, where each outline is an array of ordered points
  41. * @alias module:modeling/geometries/geom2.toOutlines
  42. *
  43. * @example
  44. * let geometry = subtract(rectangle({size: [5, 5]}), rectangle({size: [3, 3]}))
  45. * let outlines = toOutlines(geometry) // returns two outlines
  46. */
  47. const toOutlines = (geometry) => {
  48. const vertexMap = toVertexMap(toSides(geometry)) // {vertex: [edges]}
  49. const outlines = []
  50. while (true) {
  51. let startSide
  52. for (const [vertex, edges] of vertexMap) {
  53. startSide = edges.shift()
  54. if (!startSide) {
  55. vertexMap.delete(vertex)
  56. continue
  57. }
  58. break
  59. }
  60. if (startSide === undefined) break // all starting sides have been visited
  61. const connectedVertexPoints = []
  62. const startVertex = startSide[0]
  63. while (true) {
  64. connectedVertexPoints.push(startSide[0])
  65. const nextVertex = startSide[1]
  66. if (nextVertex === startVertex) break // the outline has been closed
  67. const nextPossibleSides = vertexMap.get(nextVertex)
  68. if (!nextPossibleSides) {
  69. throw new Error(`geometry is not closed at vertex ${nextVertex}`)
  70. }
  71. const nextSide = popNextSide(startSide, nextPossibleSides)
  72. if (nextPossibleSides.length === 0) {
  73. vertexMap.delete(nextVertex)
  74. }
  75. startSide = nextSide
  76. } // inner loop
  77. // due to the logic of fromPoints()
  78. // move the first point to the last
  79. if (connectedVertexPoints.length > 0) {
  80. connectedVertexPoints.push(connectedVertexPoints.shift())
  81. }
  82. outlines.push(connectedVertexPoints)
  83. } // outer loop
  84. vertexMap.clear()
  85. return outlines
  86. }
  87. // find the first counter-clockwise edge from startSide and pop from nextSides
  88. const popNextSide = (startSide, nextSides) => {
  89. if (nextSides.length === 1) {
  90. return nextSides.pop()
  91. }
  92. const v0 = vec2.create()
  93. const startAngle = vec2.angleDegrees(vec2.subtract(v0, startSide[1], startSide[0]))
  94. let bestAngle
  95. let bestIndex
  96. nextSides.forEach((nextSide, index) => {
  97. const nextAngle = vec2.angleDegrees(vec2.subtract(v0, nextSide[1], nextSide[0]))
  98. let angle = nextAngle - startAngle
  99. if (angle < -180) angle += 360
  100. if (angle >= 180) angle -= 360
  101. if (bestIndex === undefined || angle > bestAngle) {
  102. bestIndex = index
  103. bestAngle = angle
  104. }
  105. })
  106. const nextSide = nextSides[bestIndex]
  107. nextSides.splice(bestIndex, 1) // remove side from list
  108. return nextSide
  109. }
  110. module.exports = toOutlines