diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2baf2e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Udit Parmar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Poisson.gd b/Poisson.gd new file mode 100644 index 0000000..1164727 --- /dev/null +++ b/Poisson.gd @@ -0,0 +1,145 @@ +extends Control + +export var default_width = 500 +export var default_height = 500 +export var default_spacing = 5 + +var data = { + width = default_width, + height = default_height, + spacing = default_spacing +} + +var polygon_data = { + width = 500, + height = 500, + spacing = 1.5 +} + +var poisson_disc_sampling: PoissonDiscSampling = PoissonDiscSampling.new() +var points = PoolByteArray() +var poissons = [] +var truncs = [] + +signal new_poisson(points) +signal new_truncs(truncs) + +func _ready(): + reset_options(default_width, default_height, default_spacing) + +func reset_options(width, height, spacing): + $HBoxContainer/Properties/width/width_edit.text = str(width) + $HBoxContainer/Properties/height/height_edit.text = str(height) + $HBoxContainer/Properties/spacing/spacing_edit.text = str(spacing) + +func set_data(width, height, spacing): + data.width = width + data.height = height + data.spacing = spacing + +func init_points(): + points.resize(data.width * data.height) + points.fill(0) + +func _on_Button_pressed(): + set_data( + int($HBoxContainer/Properties/width/width_edit.text), + int($HBoxContainer/Properties/height/height_edit.text), + int($HBoxContainer/Properties/spacing/spacing_edit.text) + ) + + init_points() + + var rect = Rect2(Vector2(0, 0), Vector2(data.width, data.height)) + poissons = poisson_disc_sampling.generate_points(data.spacing, rect, 20) + + for poisson in poissons: + points.set(int(poisson.x) + int(poisson.y) * data.width, 1) + + emit_signal("new_poisson", points) + + +func _on_save_pressed(): + $SaveFileDialog.popup() + +func _on_SaveFileDialog_file_selected(path): + var file = File.new() + file.open(path, 2) + file.store_var(data) + file.store_var(points) + + +func _on_open_pressed(): + $OpenFileDialog.popup() + +func _on_OpenFileDialog_file_selected(path): + var file = File.new() + file.open(path, 1) + var new_data = file.get_var() + set_data(new_data.width, new_data.height, new_data.spacing) + reset_options(new_data.width, new_data.height, new_data.spacing) + points = file.get_var() + emit_signal("new_poisson", points) + + +func _on_trunc_pressed(): + truncs = [] + # Création d'un polygon + var polygon = PoolVector2Array([ + Vector2(0, 0), + Vector2(polygon_data.width, 0), + Vector2(polygon_data.width, + polygon_data.height), + Vector2(0, polygon_data.height) + ]) + + # Exemple d'hexagone + polygon = PoolVector2Array([ + Vector2(200, 0), + Vector2(400, 133), + Vector2(400, 266), + Vector2(200, 400), + Vector2(0, 266), + Vector2(0, 133), + ]) + + + + # Spacing + # Si sup à celui de base, le polygon doit se rétrécir proportionnellement + # Les positions des points augmentent à la fin + # Et inversement + + var factor = float(polygon_data.spacing) / float(data.spacing) + + for i in polygon.size(): + var vector = Vector2(polygon[i].x / factor, polygon[i].y / factor) + polygon.set(i, vector) + + var max_point = Vector2(0, 0) + for point in polygon: + if point.x > max_point.x: + max_point.x = point.x + if point.y > max_point.y: + max_point.y = point.y + + # Déplacement du polygon de telle sorte qu'il reste dans le carré original. + var rng = RandomNumberGenerator.new() + rng.randomize() + var offset = Vector2(rng.randi_range(0, data.width - max_point.x), rng.randi_range(0, data.height - max_point.y)) + polygon = Transform2D(0, offset).xform(polygon) + max_point += offset + + + + # On récupère les points à l'intérieur du polygon + for i in range(offset.x, max_point.x): + for j in range(offset.y, max_point.y): + var trunc = Vector2(i, j) + if Geometry.is_point_in_polygon(trunc, polygon): + if points[i + j * data.width]: + trunc -= offset + trunc *= factor + truncs.append(trunc) + + emit_signal("new_truncs", truncs) diff --git a/Poisson.tscn b/Poisson.tscn new file mode 100644 index 0000000..760fcfe --- /dev/null +++ b/Poisson.tscn @@ -0,0 +1,132 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://Poisson.gd" type="Script" id=1] +[ext_resource path="res://Previsualisation.gd" type="Script" id=2] +[ext_resource path="res://Previsualisation2.gd" type="Script" id=3] + +[node name="Poisson" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +margin_right = 40.0 +margin_bottom = 40.0 + +[node name="Properties" type="VBoxContainer" parent="HBoxContainer"] +margin_right = 121.0 +margin_bottom = 176.0 + +[node name="width" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_right = 121.0 +margin_bottom = 24.0 + +[node name="width_label" type="Label" parent="HBoxContainer/Properties/width"] +margin_top = 5.0 +margin_right = 55.0 +margin_bottom = 19.0 +text = "Largeur :" + +[node name="width_edit" type="LineEdit" parent="HBoxContainer/Properties/width"] +margin_left = 59.0 +margin_right = 117.0 +margin_bottom = 24.0 + +[node name="height" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_top = 28.0 +margin_right = 121.0 +margin_bottom = 52.0 + +[node name="height_label" type="Label" parent="HBoxContainer/Properties/height"] +margin_top = 5.0 +margin_right = 59.0 +margin_bottom = 19.0 +text = "Hauteur :" + +[node name="height_edit" type="LineEdit" parent="HBoxContainer/Properties/height"] +margin_left = 63.0 +margin_right = 121.0 +margin_bottom = 24.0 + +[node name="spacing" type="HBoxContainer" parent="HBoxContainer/Properties"] +margin_top = 56.0 +margin_right = 121.0 +margin_bottom = 80.0 + +[node name="spacing_label" type="Label" parent="HBoxContainer/Properties/spacing"] +margin_top = 5.0 +margin_right = 58.0 +margin_bottom = 19.0 +text = "Densité :" + +[node name="spacing_edit" type="LineEdit" parent="HBoxContainer/Properties/spacing"] +margin_left = 62.0 +margin_right = 120.0 +margin_bottom = 24.0 + +[node name="generate" type="Button" parent="HBoxContainer/Properties"] +margin_top = 84.0 +margin_right = 121.0 +margin_bottom = 104.0 +text = "Générer" + +[node name="trunc" type="Button" parent="HBoxContainer/Properties"] +margin_top = 108.0 +margin_right = 121.0 +margin_bottom = 128.0 +text = "Tronquer" + +[node name="open" type="Button" parent="HBoxContainer/Properties"] +margin_top = 132.0 +margin_right = 121.0 +margin_bottom = 152.0 +text = "Ouvrir" + +[node name="save" type="Button" parent="HBoxContainer/Properties"] +margin_top = 156.0 +margin_right = 121.0 +margin_bottom = 176.0 +text = "Enregistrer" + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +margin_left = 125.0 +margin_right = 640.0 +margin_bottom = 503.0 + +[node name="Previsualisation" type="TextureRect" parent="ScrollContainer"] +script = ExtResource( 2 ) + +[node name="ScrollContainer2" type="ScrollContainer" parent="."] +margin_left = 678.0 +margin_top = 2.0 +margin_right = 1193.0 +margin_bottom = 505.0 + +[node name="Previsualisation" type="TextureRect" parent="ScrollContainer2"] +script = ExtResource( 3 ) + +[node name="SaveFileDialog" type="FileDialog" parent="."] +margin_left = 83.0 +margin_top = 133.0 +margin_right = 649.0 +margin_bottom = 505.0 +window_title = "Enregistrer un fichier" +access = 2 + +[node name="OpenFileDialog" type="FileDialog" parent="."] +margin_left = 83.0 +margin_top = 133.0 +margin_right = 649.0 +margin_bottom = 505.0 +window_title = "Ouvrir un fichier" +mode = 0 +access = 2 + +[connection signal="new_poisson" from="." to="ScrollContainer/Previsualisation" method="_on_Control_new_poisson"] +[connection signal="new_truncs" from="." to="ScrollContainer2/Previsualisation" method="_on_Poisson_new_truncs"] +[connection signal="pressed" from="HBoxContainer/Properties/generate" to="." method="_on_Button_pressed"] +[connection signal="pressed" from="HBoxContainer/Properties/trunc" to="." method="_on_trunc_pressed"] +[connection signal="pressed" from="HBoxContainer/Properties/open" to="." method="_on_open_pressed"] +[connection signal="pressed" from="HBoxContainer/Properties/save" to="." method="_on_save_pressed"] +[connection signal="file_selected" from="SaveFileDialog" to="." method="_on_SaveFileDialog_file_selected"] +[connection signal="file_selected" from="OpenFileDialog" to="." method="_on_OpenFileDialog_file_selected"] diff --git a/Previsualisation.gd b/Previsualisation.gd new file mode 100644 index 0000000..443084a --- /dev/null +++ b/Previsualisation.gd @@ -0,0 +1,25 @@ +extends TextureRect + +var image + +func create_image(points): + var width = get_parent().get_parent().data.width + var height = get_parent().get_parent().data.height + image = Image.new() + image.create(width, height, false, Image.FORMAT_RGBA8) + image.fill(Color.white) + image.lock() + for i in width * height: + if(points[i]): + image.set_pixel(i % width, i / height, Color.black) + image.unlock() + +func update_texture(): + var texture = ImageTexture.new() + texture.create_from_image(image) + set_texture(texture) + +func _on_Control_new_poisson(points): + create_image(points) + update_texture() + diff --git a/Previsualisation2.gd b/Previsualisation2.gd new file mode 100644 index 0000000..0c1405e --- /dev/null +++ b/Previsualisation2.gd @@ -0,0 +1,23 @@ +extends TextureRect + +var image + +func create_image(points): + var width = get_parent().get_parent().polygon_data.width + var height = get_parent().get_parent().polygon_data.height + image = Image.new() + image.create(width, height, false, Image.FORMAT_RGBA8) + image.fill(Color.white) + image.lock() + for point in points: + image.set_pixel(point.x, point.y, Color.black) + image.unlock() + +func update_texture(): + var texture = ImageTexture.new() + texture.create_from_image(image) + set_texture(texture) + +func _on_Poisson_new_truncs(truncs): + create_image(truncs) + update_texture() diff --git a/addons/PoissonDiscSampling/Demo/PolygonTester.gd b/addons/PoissonDiscSampling/Demo/PolygonTester.gd new file mode 100644 index 0000000..c9d95b6 --- /dev/null +++ b/addons/PoissonDiscSampling/Demo/PolygonTester.gd @@ -0,0 +1,32 @@ +extends Node2D + + +onready var polygon: Array = $Polygon2D.polygon +onready var n = polygon.size() + +var radius: int = 20 +var k: int = 0 +var points := [] + + +func _draw() -> void: + for i in n: + draw_line(polygon[i], polygon[(i+1)%n], Color(1,1,0), 2, 1) + + draw_circle(points[k], radius / 2, Color( 1, 0, 0, 1 )) + draw_circle(points[k], 2, Color( 1, 1, 0, 1 )) + +func _ready() -> void: + var pds = PoissonDiscSampling.new() + + var start_time = OS.get_ticks_msec() + points = pds.generate_points(radius, $Polygon2D.polygon, 30) + print(points.size(), " points generated in ", OS.get_ticks_msec() - start_time, " miliseconds" ) + + get_viewport().render_target_clear_mode = Viewport.UPDATE_ONCE + + +func _process(delta: float) -> void: + if k < points.size() - 1: + update() + k += 1 diff --git a/addons/PoissonDiscSampling/Demo/PolygonTester.tscn b/addons/PoissonDiscSampling/Demo/PolygonTester.tscn new file mode 100644 index 0000000..0f4b6d1 --- /dev/null +++ b/addons/PoissonDiscSampling/Demo/PolygonTester.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/PoissonDiscSampling/Demo/PolygonTester.gd" type="Script" id=1] + +[node name="PolygonTester" type="Node2D"] +script = ExtResource( 1 ) + +[node name="Polygon2D" type="Polygon2D" parent="."] +position = Vector2( -0.568787, 0 ) +color = Color( 0.921569, 1, 0, 0 ) +antialiased = true +polygon = PoolVector2Array( 1652.09, 404.164, 1020.9, 114.599, 791.346, 514.627, 474.889, 192.896, 357.097, 495.288, 277.983, 75.1039, 70.5279, 184.106, 35.3661, 625.387, 239.305, 918.988, 156.674, 609.564, 126.787, 249.155, 300.838, 609.564, 581.675, 703.491, 506.535, 345.85, 833.54, 739.663, 1089.23, 313.065, 1499.18, 501.771, 1495.92, 836.886, 1356.02, 862.915, 1050.19, 882.436, 1144.54, 583.11, 1277.93, 599.377, 1398.32, 758.801, 1378.79, 573.349, 1125.02, 443.207, 907.03, 934.493, 1609.8, 983.296 ) diff --git a/addons/PoissonDiscSampling/PoissonDiscSampling.gd b/addons/PoissonDiscSampling/PoissonDiscSampling.gd new file mode 100644 index 0000000..a47ea72 --- /dev/null +++ b/addons/PoissonDiscSampling/PoissonDiscSampling.gd @@ -0,0 +1,137 @@ +class_name PoissonDiscSampling + +var _radius: float +var _sample_region_shape +var _retries: int +var _start_pos: Vector2 +var _sample_region_rect: Rect2 +var _cell_size: float +var _rows: int +var _cols: int +var _cell_size_scaled: Vector2 +var _grid: Array = [] +var _points: Array = [] +var _spawn_points: Array = [] +var _transpose: Vector2 + +# radius - minimum distance between points +# sample_region_shape - takes any of the following: +# -a Rect2 for rectangular region +# -an array of Vector2 for polygon region +# -a Vector3 with x,y as the position and z as the radius of the circle +# retries - maximum number of attempts to look around a sample point, reduce this value to speed up generation +# start_pos - optional parameter specifying the starting point +# +# returns an Array of Vector2D with points in the order of their discovery +func generate_points(radius: float, sample_region_shape, retries: int, start_pos := Vector2(INF, INF)) -> Array: + _radius = radius + _sample_region_shape = sample_region_shape + _retries = retries + _start_pos = start_pos + _init_vars() + + while _spawn_points.size() > 0: + var spawn_index: int = randi() % _spawn_points.size() + var spawn_centre: Vector2 = _spawn_points[spawn_index] + var sample_accepted: bool = false + for i in retries: + var angle: float = 2 * PI * randf() + var sample: Vector2 = spawn_centre + Vector2(cos(angle), sin(angle)) * (radius + radius * randf()) + if _is_valid_sample(sample): + _grid[int((_transpose.x + sample.x) / _cell_size_scaled.x)][int((_transpose.y + sample.y) / _cell_size_scaled.y)] = _points.size() + _points.append(sample) + _spawn_points.append(sample) + sample_accepted = true + break + if not sample_accepted: + _spawn_points.remove(spawn_index) + return _points + + +func _is_valid_sample(sample: Vector2) -> bool: + if _is_point_in_sample_region(sample): + var cell := Vector2(int((_transpose.x + sample.x) / _cell_size_scaled.x), int((_transpose.y + sample.y) / _cell_size_scaled.y)) + var cell_start := Vector2(max(0, cell.x - 2), max(0, cell.y - 2)) + var cell_end := Vector2(min(cell.x + 2, _cols - 1), min(cell.y + 2, _rows - 1)) + + for i in range(cell_start.x, cell_end.x + 1): + for j in range(cell_start.y, cell_end.y + 1): + var search_index: int = _grid[i][j] + if search_index != -1: + var dist: float = _points[search_index].distance_to(sample) + if dist < _radius: + return false + return true + return false + + +func _is_point_in_sample_region(sample: Vector2) -> bool: + if _sample_region_rect.has_point(sample): + match typeof(_sample_region_shape): + TYPE_RECT2: + return true + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + if Geometry.is_point_in_polygon(sample, _sample_region_shape): + return true + TYPE_VECTOR3: + if Geometry.is_point_in_circle(sample, Vector2(_sample_region_shape.x, _sample_region_shape.y), _sample_region_shape.z): + return true + _: + return false + return false + +func _init_vars() -> void: + randomize() + + # identify the type of shape and it's bounding rectangle and starting point + match typeof(_sample_region_shape): + TYPE_RECT2: + _sample_region_rect = _sample_region_shape + if _start_pos.x == INF: + _start_pos.x = _sample_region_rect.position.x + _sample_region_rect.size.x * randf() + _start_pos.y = _sample_region_rect.position.y + _sample_region_rect.size.y * randf() + + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + var start: Vector2 = _sample_region_shape[0] + var end: Vector2 = _sample_region_shape[0] + for i in range(1, _sample_region_shape.size()): + start.x = min(start.x, _sample_region_shape[i].x) + start.y = min(start.y, _sample_region_shape[i].y) + end.x = max(end.x, _sample_region_shape[i].x) + end.y = max(end.y, _sample_region_shape[i].y) + _sample_region_rect = Rect2(start, end - start) + if _start_pos.x == INF: + var n: int = _sample_region_shape.size() + var i: int = randi() % n + _start_pos = _sample_region_shape[i] + (_sample_region_shape[(i + 1) % n] - _sample_region_shape[i]) * randf() + + TYPE_VECTOR3: + var x = _sample_region_shape.x + var y = _sample_region_shape.y + var r = _sample_region_shape.z + _sample_region_rect = Rect2(x - r, y - r, r * 2, r * 2) + if _start_pos.x == INF: + var angle: float = 2 * PI * randf() + _start_pos = Vector2(x, y) + Vector2(cos(angle), sin(angle)) * r * randf() + _: + _sample_region_shape = Rect2(0, 0, 0, 0) + push_error("Unrecognized shape!!! Please input a valid shape") + + _cell_size = _radius / sqrt(2) + _cols = max(floor(_sample_region_rect.size.x / _cell_size), 1) + _rows = max(floor(_sample_region_rect.size.y / _cell_size), 1) + # scale the cell size in each axis + _cell_size_scaled.x = _sample_region_rect.size.x / _cols + _cell_size_scaled.y = _sample_region_rect.size.y / _rows + # use tranpose to map points starting from origin to calculate grid position + _transpose = -_sample_region_rect.position + + _grid = [] + for i in _cols: + _grid.append([]) + for j in _rows: + _grid[i].append(-1) + + _points = [] + _spawn_points = [] + _spawn_points.append(_start_pos) diff --git a/project.godot b/project.godot index 3fc760b..af74a52 100644 --- a/project.godot +++ b/project.godot @@ -8,6 +8,16 @@ config_version=4 +_global_script_classes=[ { +"base": "Reference", +"class": "PoissonDiscSampling", +"language": "GDScript", +"path": "res://addons/PoissonDiscSampling/PoissonDiscSampling.gd" +} ] +_global_script_class_icons={ +"PoissonDiscSampling": "" +} + [application] config/name="Societer Utils"