└── addons └── gdDelaunay ├── Delaunay.gd ├── LICENSE ├── plugin.cfg └── plugin.gd /addons/gdDelaunay/Delaunay.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | class_name Delaunay 3 | 4 | # ==== CLASSES ==== 5 | 6 | class Edge: 7 | var a: Vector2 8 | var b: Vector2 9 | 10 | func _init(a: Vector2, b: Vector2): 11 | self.a = a 12 | self.b = b 13 | 14 | func equals(edge: Edge) -> bool: 15 | return (a == edge.a && b == edge.b) || (a == edge.b && b == edge.a) 16 | 17 | func length() -> float: 18 | return a.distance_to(b) 19 | 20 | func center() -> Vector2: 21 | return (a + b) * 0.5 22 | 23 | 24 | class Triangle: 25 | var a: Vector2 26 | var b: Vector2 27 | var c: Vector2 28 | 29 | var edge_ab: Edge 30 | var edge_bc: Edge 31 | var edge_ca: Edge 32 | 33 | var center: Vector2 34 | var radius_sqr: float 35 | 36 | func _init(a: Vector2, b: Vector2, c: Vector2): 37 | self.a = a 38 | self.b = b 39 | self.c = c 40 | edge_ab = Edge.new(a,b) 41 | edge_bc = Edge.new(b,c) 42 | edge_ca = Edge.new(c,a) 43 | recalculate_circumcircle() 44 | 45 | 46 | func recalculate_circumcircle() -> void: 47 | var ab := a.length_squared() 48 | var cd := b.length_squared() 49 | var ef := c.length_squared() 50 | 51 | var cmb := c - b 52 | var amc := a - c 53 | var bma := b - a 54 | 55 | var circum := Vector2( 56 | (ab * cmb.y + cd * amc.y + ef * bma.y) / (a.x * cmb.y + b.x * amc.y + c.x * bma.y), 57 | (ab * cmb.x + cd * amc.x + ef * bma.x) / (a.y * cmb.x + b.y * amc.x + c.y * bma.x) 58 | ) 59 | 60 | center = circum * 0.5 61 | radius_sqr = a.distance_squared_to(center) 62 | 63 | func is_point_inside_circumcircle(point: Vector2) -> bool: 64 | if point == a or point == b or point == c: 65 | return false 66 | return center.distance_squared_to(point) < radius_sqr 67 | 68 | func is_corner(point: Vector2) -> bool: 69 | return point == a || point == b || point == c 70 | 71 | func get_corner_opposite_edge(corner: Vector2) -> Edge: 72 | if corner == a: 73 | return edge_bc 74 | elif corner == b: 75 | return edge_ca 76 | elif corner == c: 77 | return edge_ab 78 | else: 79 | return null 80 | 81 | 82 | class VoronoiSite: 83 | var center: Vector2 84 | var polygon: PackedVector2Array # points in absolute position, clockwise 85 | var source_triangles: Array # of Triangle's that create this site internally 86 | var neighbours: Array # of VoronoiEdge 87 | 88 | func _init(center: Vector2): 89 | self.center = center 90 | 91 | func _sort_source_triangles(a: Triangle, b: Triangle) -> bool: 92 | var da := center.direction_to(a.center).angle() 93 | var db := center.direction_to(b.center).angle() 94 | return da < db # clockwise sort 95 | 96 | func get_relative_polygon() -> PackedVector2Array: # return points in relative position to center 97 | var polygon_local: PackedVector2Array 98 | for point in polygon: 99 | polygon_local.append(point - center) 100 | return polygon_local 101 | 102 | func get_boundary() -> Rect2: 103 | var rect := Rect2(polygon[0], Vector2.ZERO) 104 | for point in polygon: 105 | rect = rect.expand(point) 106 | return rect 107 | 108 | 109 | class VoronoiEdge: 110 | var a: Vector2 111 | var b: Vector2 112 | var this: VoronoiSite 113 | var other: VoronoiSite 114 | 115 | func equals(edge: VoronoiEdge) -> bool: 116 | return (a == edge.a && b == edge.b) || (a == edge.b && b == edge.a) 117 | 118 | func length() -> float: 119 | return a.distance_to(b) 120 | 121 | func center() -> Vector2: 122 | return (a + b) * 0.5 123 | 124 | func normal() -> Vector2: 125 | return a.direction_to(b).orthogonal() 126 | 127 | 128 | # ==== PUBLIC STATIC FUNCTIONS ==== 129 | 130 | # calculates rect that contains all given points 131 | static func calculate_rect(points: PackedVector2Array, padding: float = 0.0) -> Rect2: 132 | var rect := Rect2(points[0], Vector2.ZERO) 133 | for point in points: 134 | rect = rect.expand(point) 135 | return rect.grow(padding) 136 | 137 | 138 | # ==== PUBLIC VARIABLES ==== 139 | var points: PackedVector2Array 140 | 141 | 142 | # ==== PRIVATE VARIABLES ==== 143 | var _rect: Rect2 144 | var _rect_super: Rect2 145 | var _rect_super_corners: Array 146 | var _rect_super_triangle1: Triangle 147 | var _rect_super_triangle2: Triangle 148 | 149 | 150 | # ==== CONSTRUCTOR ==== 151 | func _init(rect := Rect2()): 152 | if (rect.has_area()): 153 | set_rectangle(rect) 154 | 155 | 156 | # ==== PUBLIC FUNCTIONS ==== 157 | func add_point(point: Vector2) -> void: 158 | points.append(point) 159 | 160 | 161 | func set_rectangle(rect: Rect2) -> void: 162 | _rect = rect # save original rect 163 | 164 | # we expand rect to super rect to make sure 165 | # all future points won't be too close to broder 166 | var rect_max_size = max(_rect.size.x, _rect.size.y) 167 | _rect_super = _rect.grow(rect_max_size * 1) 168 | 169 | # calcualte and cache triangles for super rectangle 170 | var c0 := Vector2(_rect_super.position) 171 | var c1 := Vector2(_rect_super.position + Vector2(_rect_super.size.x,0)) 172 | var c2 := Vector2(_rect_super.position + Vector2(0,_rect_super.size.y)) 173 | var c3 := Vector2(_rect_super.end) 174 | _rect_super_corners.append_array([c0,c1,c2,c3]) 175 | _rect_super_triangle1 = Triangle.new(c0,c1,c2) 176 | _rect_super_triangle2 = Triangle.new(c1,c2,c3) 177 | 178 | 179 | func is_border_triangle(triangle: Triangle) -> bool: 180 | return _rect_super_corners.has(triangle.a) || _rect_super_corners.has(triangle.b) || _rect_super_corners.has(triangle.c) 181 | 182 | 183 | func remove_border_triangles(triangulation: Array) -> void: 184 | var border_triangles: Array 185 | for triangle in triangulation: 186 | if is_border_triangle(triangle): 187 | border_triangles.append(triangle) 188 | for border_triangle in border_triangles: 189 | triangulation.erase(border_triangle) 190 | 191 | 192 | func is_border_site(site: VoronoiSite) -> bool: 193 | return !_rect.encloses(site.get_boundary()) 194 | 195 | ### XN: Helper function to get the site polygons or if the site is a border site, get the clipped polygon. 196 | func get_polygon_site(site: VoronoiSite) -> PackedVector2Array: 197 | if !is_border_site(site): 198 | return site.polygon 199 | 200 | # reconstruct the bounding rectangle into polygon 201 | var bound_rect = PackedVector2Array([ 202 | _rect.position, 203 | _rect.position + Vector2(_rect.size.x, 0), 204 | _rect.end, 205 | _rect.position + Vector2(0, _rect.size.y), 206 | ]) 207 | 208 | var intersects = Geometry2D.intersect_polygons(site.polygon, bound_rect) 209 | if intersects.size() > 1 : 210 | print_debug("Warning: more than 1 intersect areas, return the first intersect area") 211 | 212 | return intersects[0] 213 | 214 | func remove_border_sites(sites: Array) -> void: 215 | var border_sites: Array 216 | for site in sites: 217 | if is_border_site(site): 218 | border_sites.append(site) 219 | for border_site in border_sites: 220 | sites.erase(border_site) 221 | 222 | 223 | func triangulate() -> Array: # of Triangle 224 | var triangulation: Array # of Triangle 225 | 226 | # calculate rectangle if none 227 | if !(_rect.has_area()): 228 | set_rectangle(calculate_rect(points)) 229 | 230 | triangulation.append(_rect_super_triangle1) 231 | triangulation.append(_rect_super_triangle2) 232 | 233 | var bad_triangles: Array # of Triangle 234 | var polygon: Array # of Edge 235 | 236 | for point in points: 237 | bad_triangles.clear() 238 | polygon.clear() 239 | 240 | _find_bad_triangles(point, triangulation, bad_triangles) 241 | for bad_tirangle in bad_triangles: 242 | triangulation.erase(bad_tirangle) 243 | 244 | _make_outer_polygon(bad_triangles, polygon) 245 | for edge in polygon: 246 | triangulation.append(Triangle.new(point, edge.a, edge.b)) 247 | 248 | return triangulation 249 | 250 | 251 | func make_voronoi(triangulation: Array) -> Array: # of VoronoiSite 252 | var sites: Array 253 | 254 | var completion_counter: Array # of Vector2, no PackedVector2Array to allow more oeprations 255 | var triangle_usage: Dictionary # of Triangle and Array[VoronoiSite], used for neighbour scan 256 | for triangle in triangulation: 257 | triangle_usage[triangle] = [] 258 | 259 | for point in points: 260 | var site := VoronoiSite.new(point) 261 | 262 | completion_counter.clear() 263 | 264 | for triangle in triangulation: 265 | if !triangle.is_corner(point): 266 | continue 267 | 268 | site.source_triangles.append(triangle) 269 | 270 | var edge: Edge = triangle.get_corner_opposite_edge(point) 271 | completion_counter.erase(edge.a) 272 | completion_counter.erase(edge.b) 273 | completion_counter.append(edge.a) 274 | completion_counter.append(edge.b) 275 | 276 | var is_complete := completion_counter.size() == site.source_triangles.size() 277 | if !is_complete: 278 | continue # do not add sites without complete polygon, usually only corner sites than come from Rect boundary 279 | 280 | var sort_func = Callable(site, "_sort_source_triangles") 281 | sort_func.bind(site.source_triangles) 282 | site.source_triangles.sort_custom(sort_func) 283 | #site.source_triangles.sort_custom(site, "_sort_source_triangles") 284 | 285 | var polygon: PackedVector2Array 286 | for triangle in site.source_triangles: 287 | polygon.append(triangle.center) 288 | triangle_usage[triangle].append(site) 289 | 290 | site.polygon = polygon 291 | sites.append(site) 292 | 293 | # scan for neighbours 294 | for site in sites: 295 | for triangle in site.source_triangles: 296 | var posibilities = triangle_usage[triangle] 297 | var neighbour := _find_voronoi_neighbour(site, triangle, posibilities) 298 | if neighbour != null: 299 | site.neighbours.append(neighbour) 300 | 301 | return sites 302 | 303 | 304 | # ==== PRIVATE FUNCTIONS ==== 305 | func _make_outer_polygon(triangles: Array, out_polygon: Array) -> void: 306 | var duplicates: Array # of Edge 307 | 308 | for triangle in triangles: 309 | out_polygon.append(triangle.edge_ab) 310 | out_polygon.append(triangle.edge_bc) 311 | out_polygon.append(triangle.edge_ca) 312 | 313 | for edge1 in out_polygon: 314 | for edge2 in out_polygon: 315 | if edge1 != edge2 && edge1.equals(edge2): 316 | duplicates.append(edge1) 317 | duplicates.append(edge2) 318 | 319 | for edge in duplicates: 320 | out_polygon.erase(edge) 321 | 322 | 323 | func _find_bad_triangles(point: Vector2, triangles: Array, out_bad_triangles: Array) -> void: 324 | for triangle in triangles: 325 | if triangle.is_point_inside_circumcircle(point): 326 | out_bad_triangles.append(triangle) 327 | 328 | 329 | func _find_voronoi_neighbour(site: VoronoiSite, triangle: Triangle, possibilities: Array) -> VoronoiEdge: 330 | var triangle_index := site.source_triangles.find(triangle) 331 | var next_triangle_index := triangle_index + 1 332 | if next_triangle_index == site.source_triangles.size(): 333 | next_triangle_index = 0 334 | var next_triangle: Triangle = site.source_triangles[next_triangle_index] 335 | 336 | var opposite_edge := triangle.get_corner_opposite_edge(site.center) 337 | var opposite_edge_next := next_triangle.get_corner_opposite_edge(site.center) 338 | var common_point := opposite_edge.a 339 | if common_point != opposite_edge_next.a && common_point != opposite_edge_next.b: 340 | common_point = opposite_edge.b 341 | 342 | for pos_site in possibilities: 343 | if pos_site.center != common_point: 344 | continue 345 | 346 | var edge := VoronoiEdge.new() 347 | edge.a = triangle.center 348 | edge.b = next_triangle.center 349 | edge.this = site 350 | edge.other = pos_site 351 | return edge 352 | 353 | return null 354 | 355 | 356 | # super triangle is not used since this method was giving worse results than super rectangle 357 | # but I'm leaving this function because it works if someone needs it 358 | func _calculate_super_triangle() -> Triangle: 359 | # calculate super rectangle 360 | var minp: Vector2 = points[0] 361 | var maxp: Vector2 = points[0] 362 | for point in points: 363 | minp.x = min(minp.x, point.x) 364 | minp.y = min(minp.y, point.y) 365 | maxp.x = max(maxp.x, point.x) 366 | maxp.y = max(maxp.y, point.y) 367 | 368 | # add extra safe space padding 369 | minp = minp - (maxp - minp) * 0.25 370 | maxp = maxp + (maxp - minp) * 0.25 371 | 372 | # extend rectangle to square 373 | var a := maxp.x - minp.x 374 | var b := maxp.y - minp.y 375 | var hd = abs(a - b) * 0.5 376 | if a > b: 377 | minp.y = minp.y - hd 378 | maxp.y = maxp.y + hd 379 | b = a 380 | elif a < b: 381 | minp.x = minp.x - hd 382 | maxp.x = maxp.x + hd 383 | a = b 384 | 385 | # make equilateral triangle that contains such square 386 | var b2 := b * 0.5 387 | var a4 := a * 0.25 388 | 389 | var p1 := Vector2((minp.x + maxp.x) * 0.5, minp.y - b2) 390 | var p2 := Vector2(minp.x - a4, maxp.y) 391 | var p3 := Vector2(maxp.x + a4, maxp.y) 392 | 393 | return Triangle.new(p1, p2, p3) 394 | -------------------------------------------------------------------------------- /addons/gdDelaunay/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bartekd97 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/gdDelaunay/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GDScript Delaunay + Voronoi" 4 | description="A Bowyer-Watson algorithm implementation for Delaunay triangulation. 5 | Also generates Voronoi diagram. 6 | Written as a single GDScript file." 7 | author="bartekd97" 8 | version="1.3.0" 9 | script="plugin.gd" 10 | -------------------------------------------------------------------------------- /addons/gdDelaunay/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | pass 7 | 8 | 9 | func _exit_tree(): 10 | pass 11 | --------------------------------------------------------------------------------