Initial commit

This commit is contained in:
Patrick 2021-08-07 21:13:07 +02:00
commit 5be8beb69f
Signed by: mewin
GPG Key ID: CEDB412C39B5BC47
13 changed files with 1587 additions and 0 deletions

7
plugin.cfg Normal file
View File

@ -0,0 +1,7 @@
[plugin]
name="GDScript Basics"
description="Basic (and advanced) utility for gdscript."
author="Patrick Wuttke<mewin@mewin.de>"
version="0.2"
script="plugin.gd"

8
plugin.gd Normal file
View File

@ -0,0 +1,8 @@
tool
extends EditorPlugin
func _enter_tree():
pass
func _exit_tree():
pass

118
scripts/libs/algorithm.gd Normal file
View File

@ -0,0 +1,118 @@
###
### algorithmic utility
###
extends Object
class_name GDBAlgorithm
class __SortBy:
var prop_name := ""
var descending := false
func __sort(obj0, obj1):
var res = obj0.get(prop_name) < obj1.get(prop_name)
if descending:
return !res
return res
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
# Packs source code into a small script object, like a lambda in other languages.
# This function can then be called by calling the "eval" (or whatever you pass as
# fn_name) function of the object. This is especially useful for GDScript functions
# like "sort_custom" which take an object and a function as parameters.
#
# Example:
# var names := ["Bob", "Alice", "Elfriede"]
# names.sort_custom(GDBAlgorithm.lambda("return name0.substr(1) < name1.substr(1)", ["name0", "name1"]), "eval")
# print(names) # prints "[Elfriede, Alice, Bob]"
#
# @param source The source code of your lambda function.
# @param params A list of parameter names your function takes.
# @param fn_name The name of the resulting function, defaults to "eval".
# @returns An object containing the lambda function as fn_name.
static func lambda(source : String, params := [], fn_name := "eval") -> Reference:
var script := GDScript.new();
script.source_code = "tool\nextends Reference\nfunc {fn_name}({param_list}):\n\t{source}".format({
"source": source,
"param_list": PoolStringArray(params).join(","),
"fn_name": fn_name
})
var err : int = script.reload()
if err == OK:
return script.new()
else:
printerr("lambda: script compilation failed.")
return null
# Removes elements from a collection only if the specified condition is met.
# Takes either an object and a function name or a lambda and a parameter list
# as parameters.
#
# Example 1:
# func is_negative(num):
# return num < 0
#
# func remove_negatives(col):
# GDBAlgorithm.remove_if(col, self, "is_negative")
#
# Example 2:
# func remove_negatives2(col):
# GDBAlgorithm.remove_if(col, "num < 0", ["num"])
static func remove_if(collection, p0, p1 = null) -> int:
if p0 is String:
if p1 && !p1 is Array:
printerr("remove_if failed: invalid parameters")
return 0
var lmb := lambda(p0 as String, p1 as Array if p1 else [])
if !lmb:
return 0 # errors have already been printed
return __remove_if(collection, lmb, "eval")
elif !p0 is Object || !p1 is String:
printerr("remove_if failed: invalid parameters")
return 0
else:
return __remove_if(collection, p0, p1)
static func sort_by(arr : Array, prop_name : String, descending := false):
var comparator := __SortBy.new()
comparator.prop_name = prop_name
comparator.descending = descending
arr.sort_custom(comparator, "__sort")
#################
# private stuff #
#################
static func __remove_if(collection, object : Object, predicate : String) -> int:
if collection is Array:
return __remove_if_array(collection, object, predicate)
elif collection is Dictionary:
return __remove_if_dict(collection, object, predicate)
else:
var values_to_remove := []
for ele in collection:
if object.call(predicate, ele):
values_to_remove.append(ele)
for ele in values_to_remove:
collection.erase(ele)
return values_to_remove.size()
static func __remove_if_array(array : Array, object : Object, predicate : String) -> int:
var removed := 0
for i in range(array.size() - 1, -1, -1):
if object.call(predicate, array[i]):
array.remove(i)
removed += 1
return removed
static func __remove_if_dict(dict : Dictionary, object : Object, predicate : String) -> int:
var removed := 0
for key in dict.keys():
if object.call(predicate, dict[key]):
dict.erase(key)
removed += 1
return removed

15
scripts/libs/constants.gd Normal file
View File

@ -0,0 +1,15 @@
###
### global constants
###
extends Object
class_name GDBConstants
const SETTING_RECENT_PLACES = "recent_places"
const SETTING_FAVOURITE_PLACES = "favourite_places"
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")

148
scripts/libs/coroutine.gd Normal file
View File

@ -0,0 +1,148 @@
###
### utility for writing coroutines
###
extends Object
class_name GDBCoroutine
class __WaitAll:
var __SIG_SEPERATOR = Reference.new()
var remaining_signals : Array
var results := []
func __connect_signals():
for i in range(remaining_signals.size()):
var ele = remaining_signals[i]
assert(ele is Dictionary && ele.has("object") && ele.has("signal"))
ele["object"].connect(ele["signal"], self, "_on_signal", [__SIG_SEPERATOR, ele["object"], ele["signal"], i])
results.resize(remaining_signals.size())
func _on_signal(p0 = null, p1 = null, p2 = null, p3 = null, p4 = null, p5 = null, p6 = null, p7 = null):
var params := [p0, p1, p2, p3, p4, p5, p6, p7]
assert(__SIG_SEPERATOR in params)
# store the parameters
var param = params.pop_front()
var sig_params = []
while param != __SIG_SEPERATOR:
sig_params.append(param)
param = params.pop_front()
# object, signal and index should be remaining (and some nulls)
assert(params.size() >= 3)
for i in range(remaining_signals.size()):
if remaining_signals[i]["object"] == params[0] && remaining_signals[i]["signal"] == params[1]:
remaining_signals.remove(i)
results[params[2]] = sig_params
break
if remaining_signals.empty():
emit_signal("finished", results)
signal finished(results)
class __WaitAny:
var __SIG_SEPERATOR = Reference.new()
func __connect_signals(signals : Array):
for ele in signals:
assert(ele is Dictionary && ele.has("object") && ele.has("signal"))
ele["object"].connect(ele["signal"], self, "_on_signal", [__SIG_SEPERATOR, ele["object"], ele["signal"]])
func _on_signal(p0 = null, p1 = null, p2 = null, p3 = null, p4 = null, p5 = null, p6 = null):
var params := [p0, p1, p2, p3, p4, p5, p6]
assert(__SIG_SEPERATOR in params)
# store the parameters
var param = params.pop_front()
var sig_params := []
while param != __SIG_SEPERATOR:
sig_params.append(param)
param = params.pop_front()
# object and signal should be remaining (and some nulls)
assert(params.size() >= 2)
emit_signal("finished", params[0], params[1], sig_params)
for con in get_signal_connection_list("_on_signal"):
con["source"].disconnect(con["signal"], self, "_on_signal")
signal finished(obj, sig, result)
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
#! Wait for multiple signals.
#!
#! Utility function to wait for multiple signals to occur, in any order.
#! The returned object will emit a "finished" signal after all signals
#! fired.
#!
#! The parameters provided to the signal handlers are stored inside a
#! "results" field inside the returned object and also provided to the
#! finish signal. Their order corresponds to the order of the objects
#! and signals provided in the parameters.
#!
#! If no signals have been provided, it will fire on the next frame. (This could
#! easily be increased by editing the source of this function, if required.)
#!
#! For technical reasons the signals must provide at most four parameters.
#!
#! Example (waits until all three buttons have been pressed):
#! yield(coroutine.wait_for_all([
#! {"object": $button0, "signal": "pressed"},
#! {"object": $button1, "signal": "pressed"},
#! {"object": $button2, "signal": "pressed"}
#! ]), "finished")
#!
#! \param objects_and_signals An array of dictionaries, each containing
#! "object" and "signal" elements.
#! \returns An object with a "finished" signal.
static func wait_for_all(objects_and_signals : Array) -> Object:
var obj := __WaitAll.new()
if objects_and_signals:
obj.remaining_signals = objects_and_signals
else:
obj.remaining_signals = [{"object": GDBUtility.get_scene_tree(), "signal": "idle_frame"}]
obj.__connect_signals()
return obj
#! Wait for multiple signals.
#!
#! Utility function to wait for one of multiple signals to occur.
#! The returned object will emit a "finished" signal after any of the
#! provided signals occured. It will only be emitted a single time.
#!
#! If no signals have been provided, the signal will never fire.
#!
#! The provided signal takes three parameters: 1. the object that
#! emitted the initial signal, 2. the signal that had been emitted
#! and 3. the parameters provided to the signal handler, as an array.
#!
#! For technical reasons the signals must provide at most four parameters.
#!
#! Example (waits until any of the buttons has been pressed):
#! var res = yield(coroutine.wait_for_any([
#! {"object": $button0, "signal": "pressed"},
#! {"object": $button1, "signal": "pressed"},
#! {"object": $button2, "signal": "pressed"}
#! ]), "finished")
#! var button = res[0]
#! button.disabled = true
#!
#! \param objects_and_signals An array of dictionaries, each containing
#! "object" and "signal" elements.
#! \returns An object with a "finished" signal.
static func wait_for_any(objects_and_signals : Array):
var obj := __WaitAny.new()
obj.__connect_signals(objects_and_signals)
return obj
static func await(res):
if res is GDScriptFunctionState:
return yield(res, "completed")
yield(GDBUtility.get_scene_tree(), "idle_frame")
return res

29
scripts/libs/debug.gd Normal file
View File

@ -0,0 +1,29 @@
###
### debugging tools
### mostly validity checks
###
extends Object
class_name GDBDebug
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func assert_valid_num(v):
assert(!is_nan(v) && !is_inf(v))
static func assert_valid_vec2(v : Vector2):
assert(!is_nan(v.x) && !is_inf(v.x))
assert(!is_nan(v.y) && !is_inf(v.y))
static func assert_valid_vec3(v : Vector3):
assert(!is_nan(v.x) && !is_inf(v.x))
assert(!is_nan(v.y) && !is_inf(v.y))
assert(!is_nan(v.z) && !is_inf(v.z))
static func deprecated(note : String) -> void:
printerr("Usage of deprecated function: " + note)
print_stack()

80
scripts/libs/format.gd Normal file
View File

@ -0,0 +1,80 @@
###
### various formatting functions
###
extends Object
class_name GDBFormat
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func format_bytes(bytes : int) -> String:
var UNITS = ["B", "KiB", "MiB", "GiB", "TiB"]
var idx = 0
while bytes > 1024 && idx < UNITS.size() - 1:
bytes = bytes / 1024.0
idx += 1
return "%.4f %s" % [bytes, UNITS[idx]]
static func format_time(time : float) -> String:
# warning-ignore:integer_division
var minutes = int(time) / 60
var seconds = int(time) % 60
return "%02d:%02d" % [minutes, seconds]
static func format_unixtime(unix_time : int, format := tr("{0year}-{0month}-{0day} {0hour}:{0minute}")) -> String:
var datetime = OS.get_datetime_from_unix_time(unix_time)
datetime["year2"] = datetime["year"] % 100
datetime["0year"] = "%02d" % datetime["year2"]
datetime["0month"] = "%02d" % datetime["month"]
datetime["0day"] = "%02d" % datetime["day"]
datetime["0hour"] = "%02d" % datetime["hour"]
datetime["0minute"] = "%02d" % datetime["minute"]
datetime["0second"] = "%02d" % datetime["second"]
# return "%02d-%02d-%02d %02d:%02d" % [datetime["year"] % 100, datetime["month"], datetime["day"], datetime["hour"], datetime["minute"]]
return format.format(datetime)
static func smart_format_unixtime(unix_time : int, format_date := tr("{0year}-{0month}-{0day}"), format_time := tr("{0hour}:{0minute}")) -> String:
var now = OS.get_unix_time()
var datetime = OS.get_datetime_from_unix_time(unix_time)
__datetime_add_fields(datetime)
if now == unix_time:
return GDBUtility.translate("just now")
elif now > unix_time:
var tdiff = now - unix_time
if tdiff < 60: # < 60 seconds
return GDBUtility.translate("%d seconds ago") % tdiff
elif tdiff < 3600: # < 60 minutes
return GDBUtility.translate("%d minutes ago") % (tdiff / 60)
var now_datetime = OS.get_datetime_from_unix_time(now)
if now_datetime["year"] == datetime["year"] && now_datetime["month"] == datetime["month"] && now_datetime["day"] == datetime["day"]:
return GDBUtility.translate("today at %s") % format_time.format(datetime)
else:
var tdiff = unix_time - now
if tdiff < 60:
return GDBUtility.translate("in %d seconds") % tdiff
elif tdiff < 3600:
return GDBUtility.translate("in %d minutes") % (tdiff / 60)
var now_datetime = OS.get_datetime_from_unix_time(now)
if now_datetime["year"] == datetime["year"] && now_datetime["month"] == datetime["month"] && now_datetime["day"] == datetime["day"]:
return GDBUtility.translate("today at %s") % format_time.format(datetime)
return "%s %s" % [format_date.format(datetime), format_time.format(datetime)]
#################
# private stuff #
#################
static func __datetime_add_fields(datetime : Dictionary) -> void:
datetime["year2"] = datetime["year"] % 100
datetime["0year"] = "%02d" % datetime["year2"]
datetime["0month"] = "%02d" % datetime["month"]
datetime["0day"] = "%02d" % datetime["day"]
datetime["0hour"] = "%02d" % datetime["hour"]
datetime["0minute"] = "%02d" % datetime["minute"]
datetime["0second"] = "%02d" % datetime["second"]

100
scripts/libs/fsutil.gd Normal file
View File

@ -0,0 +1,100 @@
###
### file system utility
###
extends Object
class_name GDBFsUtil
const __META_FILENAME_REGEX = "__gdb_fsutil_filename_regex__"
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func find_all_by_name(path : String, name : String, files_only = true) -> Dictionary:
var result = []
__find_all_by_name(path, name, result, files_only)
return result
static func escape_filename(filename : String) -> String:
return __get_filename_regex().sub(filename, "_", true)
static func is_filename_valid(filename : String) -> bool:
return __get_filename_regex().search(filename) == null
static func load_image_resource(filename : String):
var res = load(filename)
if res:
return res
var img = Image.new()
if img.load(filename) == OK:
var tex = ImageTexture.new()
tex.create_from_image(img)
return tex
return null
#! Retrieve information about the current git branch.
#!
#! Used to display this information in development builds.
#! Returns a dictionary with the following keys:
#! - "branch" - the name of the current branch
#! - "commit" - the full hash of the latest commit
#! - "commit_short" - the shortened hash of the last commit
static func get_git_info() -> Dictionary:
var file = File.new()
if file.open("res://.git/HEAD", File.READ) != OK:
return {"branch": "<unknown>", "commit": "???", "commit_short": "???"}
var text = file.get_line()
if !text.begins_with("ref:"):
return {"branch": "<detached>", "commit": text, "commit_short": text.left(7)}
var ref = text.right(5).get_file()
file.close()
if file.open("res://.git/refs/heads/%s" % ref, File.READ) != OK:
return {"branch": ref, "commit": "<unknown>", "commit_short": "<unknown>"}
var commitid = file.get_line()
return {"branch": ref, "commit": commitid, "commit_short": commitid.left(7)}
static func get_home_folder() -> String:
if OS.has_feature("X11"):
if OS.has_environment("HOME"):
return OS.get_environment("HOME")
elif OS.has_feature("Windows"):
if OS.has_environment("USERPROFILE"):
return OS.get_environment("USERPROFILE")
return "/"
#################
# private stuff #
#################
static func __get_filename_regex() -> RegEx:
var tree := GDBUtility.get_scene_tree()
if tree.has_meta(__META_FILENAME_REGEX):
return tree.get_meta(__META_FILENAME_REGEX) as RegEx
var filename_regex := RegEx.new()
filename_regex.compile("[^a-zA-Z0-9_\\-\\. ]")
tree.set_meta(__META_FILENAME_REGEX, filename_regex)
return filename_regex
static func __find_all_by_name(path, name, result, files_only):
var dir = Directory.new()
if dir.open(path) != OK:
# print("cannot open dir %s" % path)
return
dir.list_dir_begin(true)
while true:
var fname = dir.get_next()
if fname == "":
break
var full_name = path + "/" + fname
if fname == name && (!files_only || dir.file_exists(full_name)):
result.append(full_name)
# dir_exists doesnt work for res:// paths, just attempt to add, will silently fail for files
if true: # dir.dir_exists(full_name):
__find_all_by_name(full_name, name, result, files_only)
dir.list_dir_end()

173
scripts/libs/geoutil.gd Normal file
View File

@ -0,0 +1,173 @@
###
### geometry utility code
### some of this might not work yet, please dont use it
###
extends Object
class_name GDBGeoUtility
class Grid3D:
var size : Vector3
var data : Array
func _init(size_ : Vector3, init_val = null):
size = size_
data = []
data.resize(size.x * size.y * size.z)
for i in range(data.size()):
data[i] = init_val
func _idx(x, y, z):
return x + y * size.x + z * size.x * size.y
func get_at(x, y, z):
return data[_idx(x, y, z)]
func set_at(x, y, z, val):
data[_idx(x, y, z)] = val
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func full_aabb(root : Spatial):
var aabb = AABB()
for vi in GDBUtility.find_nodes_by_type(root, VisualInstance):
var local_aabb = vi.get_aabb()
local_aabb = vi.global_transform.xform(local_aabb)
aabb = aabb.merge(local_aabb)
return aabb
static func gen_aabb(points : Array, point_transform = Transform()) -> AABB:
var aabb = AABB()
for point in points:
aabb = aabb.expand(point_transform.xform(point))
return aabb
static func collsion_aabb(collision_object : CollisionObject) -> AABB:
var aabb := AABB()
for owner_id in collision_object.get_shape_owners():
var trans = collision_object.shape_owner_get_transform(owner_id)
for shape_id in range(collision_object.shape_owner_get_shape_count(owner_id)):
var shape = collision_object.shape_owner_get_shape(owner_id, shape_id)
var new_aabb = shape_aabb(shape)
if new_aabb.size:
aabb = aabb.merge(trans.xform(new_aabb))
return aabb
static func shape_aabb(shape : Shape) -> AABB:
if shape is BoxShape:
return AABB(-shape.extents, 2.0 * shape.extents)
elif shape is CapsuleShape:
return AABB(Vector3(-shape.radius, -shape.radius - 0.5 * shape.height, -shape.radius), \
Vector3(2.0 * shape.radius, 2.0 * shape.radius + shape.height, 2.0 * shape.radius))
elif shape is CylinderShape:
return AABB(Vector3(-shape.radius, -0.5 * shape.height, -shape.radius), \
Vector3(2.0 * shape.radius, shape.height, 2.0 * shape.radius))
elif shape is PlaneShape:
return AABB()
elif shape is SphereShape:
return AABB(-Vector3(shape.radius, shape.radius, shape.radius), 2.0 * Vector3(shape.radius, shape.radius, shape.radius))
else:
# TODO: polygon shapes
return AABB()
static func orphan_global_transform(node : Node):
var transform : Transform
if node is Spatial:
transform = node.transform
var parent = node.get_parent()
if parent != null:
transform = orphan_global_transform(parent) * transform
return transform
static func voxelize_surf(mesh : ArrayMesh, surf : int, particle_size = -1.0) -> PoolVector3Array:
var arrays = mesh.surface_get_arrays(surf)
var points = arrays[Mesh.ARRAY_VERTEX]
return PoolVector3Array(points)
static func voxelize(mesh : ArrayMesh, particle_size = -1.0) -> PoolVector3Array:
var points = PoolVector3Array()
for surf in range(mesh.get_surface_count()):
points.append_array(voxelize_surf(mesh, surf, particle_size))
return points
static func transform_to(node : Node, root : Node):
var transform = Transform()
var node_ = node
while node_ != root && node_ != null:
if node_ is Spatial:
transform = transform * node_.transform
node_ = node_.get_parent()
return transform
static func mesh_to_grid(mesh : ArrayMesh, grid_size : float, point_transform = Transform()):
var aabb = AABB()
for i in range(mesh.get_surface_count()):
var points = mesh.surface_get_arrays(i)[Mesh.ARRAY_VERTEX]
aabb = aabb.merge(gen_aabb(points, point_transform))
var grid_x = ceil(aabb.size.x / grid_size)
var grid_y = ceil(aabb.size.y / grid_size)
var grid_z = ceil(aabb.size.z / grid_size)
var grid = Grid3D.new(Vector3(grid_x, grid_y, grid_z), 0)
var grid_origin = aabb.position
for i in range(mesh.get_surface_count()):
__insert_surf(mesh, i, grid_origin, grid_size, grid, point_transform)
var result = []
for x in range(grid.size.x):
for y in range(grid.size.y):
var inside = false
for z in range(grid.size.z):
var cell_value = grid.get_at(x, y, z) % 2
if cell_value == 1:
inside = !inside
if inside || cell_value > 0:
result.append(Vector3(x, y, z))
return result
# http://www.boris-belousov.net/2016/12/01/quat-dist/
static func basis_angle(basis0 : Basis, basis1 : Basis):
var basis_diff = basis0 * basis1.transposed()
var tr : float = basis_diff.x.x + basis_diff.y.y + basis_diff.z.z
return acos((tr - 1) / 2)
static func angle_normalize(angle : float) -> float:
while angle < -PI:
angle += 2.0 * PI
while angle > PI:
angle -= 2.0 * PI
return angle
static func angle_diff(angle0 : float, angle1 : float) -> float:
return angle_normalize(angle0 - angle1)
#################
# private stuff #
#################
static func __insert_tri(a : Vector3, b : Vector3, c : Vector3, grid_origin : Vector3, grid_size : float, grid : Grid3D, point_transform : Transform):
var ray_dir = Vector3(0, 0, 1)
for x in range(grid.size.x):
for y in range(grid.size.y):
var ray_origin = grid_origin + grid_size * Vector3(x, y, 0)
var inters = Geometry.ray_intersects_triangle(ray_origin, ray_dir, point_transform.xform(a), point_transform.xform(b), point_transform.xform(c))
if inters != null:
var z = floor(0.99 * (inters.z - grid_origin.z) / grid_size)
grid.set_at(x, y, z, grid.get_at(x, y, z) + 1)
static func __insert_surf(mesh : ArrayMesh, surf : int, grid_origin : Vector3, grid_size : float, grid : Grid3D, point_transform : Transform):
var arrays = mesh.surface_get_arrays(surf)
if arrays.size() >= Mesh.ARRAY_INDEX - 1 && arrays[Mesh.ARRAY_INDEX].size() > 0:
# index mode
var indices = arrays[Mesh.ARRAY_INDEX]
var vertices = arrays[Mesh.ARRAY_VERTEX]
for i in range(0, indices.size() - 2, 3):
__insert_tri(vertices[indices[i]], vertices[indices[i + 1]], vertices[indices[i + 2]], grid_origin, grid_size, grid, point_transform)
else:
# normal mode
var vertices = arrays[Mesh.ARRAY_VERTEX]
for i in range(0, vertices.size() - 2, 3):
__insert_tri(vertices[i], vertices[i + 1], vertices[i + 2], grid_origin, grid_size, grid, point_transform)

306
scripts/libs/graphing.gd Normal file
View File

@ -0,0 +1,306 @@
###
### graph class + algorithms
###
extends Object
class Edge:
var idx0 : int
var idx1 : int
var weight : float
func _init(idx0_ : int, idx1_ : int, weight_ : float):
idx0 = idx0_
idx1 = idx1_
weight = weight_
class Graph:
var vertices = []
var edges = []
func append_vertex(vertex):
var idx = vertices.size()
vertices.append(vertex)
return idx
func append_vertices(vertices_ : Array):
for vertex in vertices_:
vertices.append(vertex)
func append_edge(idx0, idx1, weight := 1.0):
for edge in edges:
if (edge.idx0 == idx0 && edge.idx1 == idx1) || \
(edge.idx0 == idx1 && edge.idx1 == idx0):
return
edges.append(Edge.new(idx0, idx1, weight))
func remove_vertex(vertex):
var idx = vertices.find(vertex)
if idx > -1:
remove_vertex_at(idx)
func remove_vertex_at(idx):
vertices.remove(idx)
# fix edges
var to_remove = []
for edge in edges:
if edge.idx0 == idx || edge.idx1 == idx:
to_remove.append(edge)
else:
if edge.idx0 > idx:
edge.idx0 -= 1
if edge.idx1 > idx:
edge.idx1 -= 1
for edge in to_remove:
edges.erase(edge)
func remove_edge(idx0, idx1):
var to_remove = []
for edge in edges:
if (edge.idx0 == idx0 && edge.idx1 == idx1) || \
(edge.idx0 == idx1 && edge.idx1 == idx0):
to_remove.append(edge)
for edge in to_remove:
edges.erase(edge)
func duplicate(deep = false):
var dupl = Graph.new()
dupl.vertices = vertices.duplicate(deep)
for edge in edges:
dupl.append_edge(edge.idx0, edge.idx1, edge.weight)
return dupl
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
# generates a delaunay triangulation for the given point list
func gen_delaunay(point_list : Array) -> Graph:
assert(point_list.size() > 2)
var graph = Graph.new()
var tris = []
graph.append_vertices(point_list)
__gen_super_triangle(graph, point_list)
tris.append(Vector3(graph.vertices.size() - 3, graph.vertices.size() - 2, graph.vertices.size() - 1))
for idx in range(point_list.size()):
var point = point_list[idx]
var bad_triangles = []
for tri in tris:
var p0 = graph.vertices[tri.x]
var p1 = graph.vertices[tri.y]
var p2 = graph.vertices[tri.z]
if __point_in_circumcircle(point, p0, p1, p2):
bad_triangles.append(tri)
var polygon := []
for bad_tri in bad_triangles:
if !__edge_shared(bad_tri.x, bad_tri.y, bad_triangles):
polygon.append(Vector2(bad_tri.x, bad_tri.y))
if !__edge_shared(bad_tri.y, bad_tri.z, bad_triangles):
polygon.append(Vector2(bad_tri.y, bad_tri.z))
if !__edge_shared(bad_tri.z, bad_tri.x, bad_triangles):
polygon.append(Vector2(bad_tri.z, bad_tri.x))
for bad_tri in bad_triangles:
tris.erase(bad_tri)
graph.remove_edge(bad_tri.x, bad_tri.y)
graph.remove_edge(bad_tri.y, bad_tri.z)
graph.remove_edge(bad_tri.z, bad_tri.x)
for edge in polygon:
var tri = Vector3(edge.x, edge.y, idx)
var p0 = graph.vertices[tri.x]
var p1 = graph.vertices[tri.y]
var p2 = graph.vertices[tri.z]
tris.append(tri)
graph.append_edge(tri.x, tri.y, p0.distance_to(p1))
graph.append_edge(tri.y, tri.z, p1.distance_to(p2))
graph.append_edge(tri.z, tri.x, p2.distance_to(p0))
# remove super triangle
for idx in range(graph.vertices.size() - 1, graph.vertices.size() - 4, -1):
graph.remove_vertex_at(idx)
return graph
# generate the minimum spanning tree
func gen_mst(graph : Graph) -> Graph:
var mst = graph.duplicate()
var possible_edges = mst.edges
var forests = []
mst.edges = []
possible_edges.sort_custom(self, "__edge_weight_comp")
for idx in range(mst.vertices.size()):
forests.append([idx])
while !possible_edges.empty():
var edge = possible_edges.pop_front()
# already connected?
if forests[edge.idx0] == forests[edge.idx1]:
continue
# append edge
mst.append_edge(edge.idx0, edge.idx1, edge.weight)
# merge forests
for idx in forests[edge.idx1]:
forests[edge.idx0].append(idx)
forests[idx] = forests[edge.idx0]
return mst
# finds the shortest path through the given graph
# returns a dictionary with a "distance" and a "path"
func find_path(graph : Graph, start_idx : int, end_idx : int) -> Dictionary:
var dist := []
var prev := []
var queue := []
for idx in range(graph.vertices.size()):
dist.append(INF)
prev.append(-1)
queue.append(idx)
dist[start_idx] = 0.0
while !queue.empty():
var shortest_dist = INF
var shortest_idx : int
for idx in queue:
if dist[idx] < shortest_dist:
shortest_dist = dist[idx]
shortest_idx = idx
if shortest_idx == end_idx: # found it
if shortest_dist == INF:
return {
"distance": INF,
"path": []
}
var path = [end_idx]
var idx = end_idx
while prev[idx] != -1:
path.append(prev[idx])
idx = prev[idx]
assert(idx == start_idx)
return {
"distance": dist[end_idx],
"path": path
}
queue.erase(shortest_idx)
for edge in graph.edges:
var target_idx
if edge.idx0 == shortest_idx:
target_idx = edge.idx1
elif edge.idx1 == shortest_idx:
target_idx = edge.idx0
else:
continue
var new_dist = shortest_dist + edge.weight
if new_dist < dist[target_idx]:
dist[target_idx] = new_dist
prev[target_idx] = shortest_idx
return {
"distance": INF,
"path": []
}
# draws a graph onto a control
func dump_graph_2d(graph : Graph, control : Control) -> void:
var normalized_graph = graph.duplicate()
var min_xy = Vector2.INF
var max_xy = -Vector2.INF
# normalize graph
for vertex in normalized_graph.vertices:
if vertex.x < min_xy.x:
min_xy.x = vertex.x
if vertex.x > max_xy.x:
max_xy.x = vertex.x
if vertex.y < min_xy.y:
min_xy.y = vertex.y
if vertex.y > max_xy.y:
max_xy.y = vertex.y
for idx in range(normalized_graph.vertices.size()):
var normalized_vertex = normalized_graph.vertices[idx]
normalized_vertex.x = inverse_lerp(min_xy.x, max_xy.x, normalized_vertex.x)
normalized_vertex.y = inverse_lerp(min_xy.y, max_xy.y, normalized_vertex.y)
normalized_graph.vertices[idx] = normalized_vertex
var img_size = control.rect_size
for edge in normalized_graph.edges:
var v0 = normalized_graph.vertices[edge.idx0]
var v1 = normalized_graph.vertices[edge.idx1]
var coord0 = __img_vertex_coord(v0, img_size)
var coord1 = __img_vertex_coord(v1, img_size)
control.draw_line(coord0, coord1, Color.yellow, 2.0, true)
for vertex in normalized_graph.vertices:
var coord = __img_vertex_coord(vertex, img_size)
control.draw_circle(coord, 5.0, Color.red)
#################
# private stuff #
#################
func __gen_super_triangle(graph : Graph, point_list : Array) -> void:
var min_xy = Vector2.INF
var max_xy = -Vector2.INF
for point in point_list:
if point.x < min_xy.x:
min_xy.x = point.x
if point.y < min_xy.y:
min_xy.y = point.y
if point.x > max_xy.x:
max_xy.x = point.x
if point.y > max_xy.y:
max_xy.y = point.y
min_xy -= Vector2(1.0, 1.0)
var p1 = Vector2(2.0 * max_xy.x, min_xy.y)
var p2 = Vector2(min_xy.x, 2.0 * max_xy.y)
var idx0 = graph.append_vertex(min_xy)
var idx1 = graph.append_vertex(p1)
var idx2 = graph.append_vertex(p2)
graph.append_edge(idx0, idx1, min_xy.distance_to(p1))
graph.append_edge(idx1, idx2, p1.distance_to(p2))
graph.append_edge(idx2, idx0, p2.distance_to(min_xy))
func __edge_shared(p0 : int, p1 : int, tris : Array) -> bool:
var edge_count = 0
for tri in tris:
if (p0 == tri.x || p0 == tri.y || p0 == tri.z) && \
(p1 == tri.x || p1 == tri.y || p1 == tri.z):
edge_count += 1
return edge_count > 1
func __point_in_circumcircle(point : Vector2, p0 : Vector2, p1 : Vector2, p2 : Vector2) -> bool:
var p0x_ = p0.x - point.x
var p0y_ = p0.y - point.y
var p1x_ = p1.x - point.x
var p1y_ = p1.y - point.y
var p2x_ = p2.x - point.x
var p2y_ = p2.y - point.y
return (
(p0x_*p0x_ + p0y_*p0y_) * (p1x_*p2y_-p2x_*p1y_) - \
(p1x_*p1x_ + p1y_*p1y_) * (p0x_*p2y_-p2x_*p0y_) + \
(p2x_*p2x_ + p2y_*p2y_) * (p0x_*p1y_-p1x_*p0y_) \
) > 0;
func __check_poly(polygon) -> void:
for edge0 in polygon:
for edge1 in polygon:
if edge0 == edge1:
continue
assert(edge0.x != edge1.x || edge0.y != edge1.y)
assert(edge0.x != edge1.y || edge0.y != edge1.x)
func __edge_weight_comp(e0 : Edge, e1 : Edge):
return e0.weight < e1.weight
func __img_vertex_coord(vertex : Vector2, img_size : Vector2):
return Vector2(5.0, 5.0) + vertex * (img_size - Vector2(10.0, 10.0))

74
scripts/libs/math.gd Normal file
View File

@ -0,0 +1,74 @@
###
### math utility
###
extends Object
class_name GDBMath
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func angle(vec0 : Vector3, vec1 : Vector3, axis : Vector3) -> float:
var res = vec0.angle_to(vec1)
var crs = axis.cross(vec0)
var dir = vec1 - vec0
if dir.dot(crs) < 0:
return -res
else:
return res
static func find_perp(a : Vector3, b : Vector3, d : Vector3) -> Vector3:
var t0 = d.dot(a)
var t1 = d.dot(b)
if t0 - t1 < 0.01:
return a
var t = -t1 / (t0 - t1)
# assert(t >= 0 && t <= 1)
var n = b.linear_interpolate(a, t).normalized()
var an = n.angle_to(d)
assert(abs(n.dot(d)) < 0.01)
assert(abs(an - 0.5 * PI) < 0.01)
return n
static func rects_intersect(pos0 : Vector2, size0 : Vector2, pos1 : Vector2, size1 : Vector2) -> bool:
if pos0.x + size0.x < pos1.x || pos1.x + size1.x < pos0.x:
return false
if pos0.y + size0.y < pos1.y || pos1.y + size1.y < pos1.y:
return false
return true
static func min_element(array : Array) -> int:
var min_idx := -1
for idx in range(array.size()):
if min_idx == -1 || array[min_idx] > array[idx]:
min_idx = idx
return min_idx
static func max_element(array : Array) -> int:
var min_idx := -1
for idx in range(array.size()):
if min_idx == -1 || array[min_idx] < array[idx]:
min_idx = idx
return min_idx
static func to_int(any, default := 0) -> int:
if any is int:
return any
elif any is bool || any is float || any is String:
return int(any)
else:
return default
static func to_float(any, default := 0.0) -> float:
if any is float:
return any
elif any is bool || any is int || any is String:
return float(any)
else:
return default

144
scripts/libs/settings.gd Normal file
View File

@ -0,0 +1,144 @@
###
### settings management API
###
extends Object
class_name GDBSettings
const __META_STATE = "__gdb_settings_state__"
class __State:
# modification of the libraries behaviour
var settings_file_name := "user://settings.dat"
var disable_default_settings := false
var settings_changed := false
var values := {}
signal setting_changed(name, value)
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func has_value(name : String) -> bool:
return __get_state().values.has(name)
static func get_value(name : String, def = null, type = TYPE_NIL):
if type == TYPE_NIL && def != null:
type = typeof(def)
var val = __get_state().values.get(name, def)
if type is Object:
if !val is type:
return def
else:
match type:
TYPE_NIL:
return val
TYPE_STRING:
val = str(val)
TYPE_INT:
val = int(val)
TYPE_REAL:
val = float(val)
TYPE_BOOL:
val = bool(val)
_:
if typeof(val) != type:
return def
if val is Array || val is Dictionary:
val = val.duplicate()
return val
static func set_value(name : String, value) -> void:
if __get_state().values.get(name) != value:
__set_value(name, value)
# save next frame (in case multiple values are changed)
__get_state().settings_changed = true
yield(GDBUtility.get_scene_tree(), "idle_frame")
if __get_state().settings_changed:
store_to_file()
__get_state().settings_changed = false
static func toggle_value(name : String) -> void:
set_value(name, !__get_state().values.get(name, false))
static func array_add_value(name : String, value) -> void:
var arr = get_value(name, [])
arr.append(value)
set_value(name, arr)
static func array_remove_value(name : String, value) -> void:
var arr = get_value(name, [])
arr.erase(value)
set_value(name, arr)
static func array_has_value(name : String, value) -> bool:
var arr = get_value(name, [])
return arr.has(value)
static func array_toggle_value(name : String, value) -> void:
var arr = get_value(name, [])
if arr.has(value):
arr.erase(value)
else:
arr.append(value)
set_value(name, arr)
static func load_from_file() -> void:
var in_file = File.new()
if not in_file.file_exists(__get_state().settings_file_name):
return # nothing to load
in_file.open(__get_state().settings_file_name, File.READ)
__load_data(parse_json(in_file.get_as_text()))
static func store_to_file() -> void:
var out_file = File.new()
out_file.open(__get_state().settings_file_name, File.WRITE)
out_file.store_line(__save_data())
out_file.close()
static func set_disable_default_settings(disable : bool) -> void:
__get_state().disable_default_settings = disable
static func is_disable_default_settings() -> bool:
return __get_state().disable_default_settings
static func connect_static(sig_name : String, receiver : Object, method : String, binds := []) -> int:
return __get_state().connect(sig_name, receiver, method, binds)
static func disconnect_static(sig_name : String, receiver : Object, method : String) -> void:
__get_state().disconnect(sig_name, receiver, method)
#################
# private stuff #
#################
static func __save_data():
return to_json(__get_state().values)
static func __load_data(data : Dictionary):
for name in data:
__set_value(name, data[name])
static func __set_value(name : String, value):
__get_state().values[name] = value
__on_setting_changed(name, value)
__get_state().emit_signal("setting_changed", name, value)
static func __on_setting_changed(name : String, value):
if __get_state().disable_default_settings:
return
match name:
"fullscreen":
OS.window_fullscreen = value
"vsync":
OS.vsync_enabled = value
static func __get_state() -> __State:
return GDBUtility.get_state_object(__META_STATE, __State)

385
scripts/libs/util.gd Normal file
View File

@ -0,0 +1,385 @@
###
### a set of random utility functions
###
extends Object
class_name GDBUtility
################
# public stuff #
################
func _init() -> void:
assert(0, "This class should not be instantiated.")
static func find_node_filter(root : Node, object : Object, method : String, bfs := false) -> Node:
if object.call(method, root):
return root
if bfs:
for child in root.get_children():
if object.call(method, child):
return child
for child in root.get_children():
var res = find_node_filter(child, object, method)
if res != null:
return res
return null
static func find_node_by_type(root : Node, tp, bfs := false) -> Node:
if root is tp:
return root
if bfs:
for child in root.get_children():
if child is tp:
return child
for child in root.get_children():
var res = find_node_by_type(child, tp, bfs)
if res != null:
return res
return null
static func find_node_by_name(root : Node, name : String, bfs := false, use_cache := true) -> Node:
if root.name == name:
return root
# create the cache regardless of use_cache
if !root.has_meta("__find_by_name_node_cache"):
root.set_meta("__find_by_name_node_cache", {})
var cache = root.get_meta("__find_by_name_node_cache")
if use_cache && name in cache:
var node = root.get_node(cache[name])
if node:
return node
else:
cache.erase(name)
if bfs:
for child in root.get_children():
if child.name == name:
cache[name] = child
return child
for child in root.get_children():
var res = find_node_by_name(child, name)
if res != null:
cache[name] = root.get_path_to(res)
return res
return null
static func find_nodes_by_type(root : Node, tp, as_path = false) -> Array:
var result = []
__find_nodes_by_type(root, tp, as_path, result)
return result
static func find_nodes_with_member(root : Node, member : String) -> Array:
var result = []
__find_nodes_with_member(root, member, result)
return result
static func find_parent_with_method(root : Node, method : String) -> Node:
if root.has_method(method):
return root
else:
var parent = root.get_parent()
if parent == null:
return null
return find_parent_with_method(parent, method)
static func find_parent_with_member(root : Node, member : String) -> Node:
if root.get(member) != null:
return root
else:
var parent = root.get_parent()
if parent == null:
return null
return find_parent_with_member(parent, member)
static func find_parent_with_name(root : Node, name : String) -> Node:
if root.name == name:
return root
else:
var parent = root.get_parent()
if parent == null:
return null
return find_parent_with_name(parent, name)
static func find_parent_with_type(root : Node, tp) -> Node:
if (tp is Script && tp.instance_has(root)) \
|| root is tp:
return root
else:
var parent = root.get_parent()
if parent == null:
return null
return find_parent_with_type(parent, tp)
static func find_treeitem_with_metadata(root : TreeItem, metadata, column := 0) -> TreeItem:
while root:
if root.get_metadata(column) == metadata:
return root
var in_children := find_treeitem_with_metadata(root.get_children(), metadata, column)
if in_children:
return in_children
root = root.get_next()
return null
static func hide_properties(properties : Array, names_to_hide : Array) -> void:
for i in range(properties.size() - 1, -1, -1):
if properties[i]["name"] in names_to_hide:
properties.remove(i)
static func type_name(type : int) -> String:
match type:
TYPE_NIL:
return "null"
TYPE_BOOL:
return "bool"
TYPE_INT:
return "int"
TYPE_REAL:
return "float"
TYPE_STRING:
return "String"
TYPE_VECTOR2:
return "Vector2"
TYPE_RECT2:
return "Rect2"
TYPE_VECTOR3:
return "Vector3"
TYPE_TRANSFORM2D:
return "Transform2D"
TYPE_PLANE:
return "Plane"
TYPE_QUAT:
return "Quat"
TYPE_AABB:
return "AABB"
TYPE_BASIS:
return "Basis"
TYPE_TRANSFORM:
return "Transform"
TYPE_COLOR:
return "Color"
TYPE_NODE_PATH:
return "NodePath"
TYPE_RID:
return "RID"
TYPE_OBJECT:
return "Object"
TYPE_DICTIONARY:
return "Dictionary"
TYPE_ARRAY:
return "Array"
TYPE_RAW_ARRAY:
return "PoolByteArray"
TYPE_INT_ARRAY:
return "PoolIntArray"
TYPE_REAL_ARRAY:
return "PoolRealArray"
TYPE_STRING_ARRAY:
return "PoolStringArray"
TYPE_VECTOR2_ARRAY:
return "Vector2Array"
TYPE_VECTOR3_ARRAY:
return "Vector3Array"
TYPE_COLOR_ARRAY:
return "ColorArray"
_:
return "Unknown"
static func format_type(type : Dictionary, none_name := "void") -> String:
match type["type"]:
TYPE_NIL:
return none_name
TYPE_OBJECT:
return type.get("class_name", "Object")
var tp:
return type_name(tp)
static func format_method_signature(method : Dictionary, format := "{return} {name}({args})") -> String:
var args := PoolStringArray()
var rettype := "void"
if method.has("return"):
rettype = format_type(method["return"])
for i in range(method["args"].size()):
var arg = method["args"][i]
var def = ""
if i < method["default_args"].size():
def = " = %s" % str(method["default_args"][i])
args.append("{name}: {type}{def}".format({
"name": arg["name"],
"type": format_type(arg, "Variant"),
"def": def
}))
return format.format({
"return": rettype,
"name": method["name"],
"args": args.join(", ")
})
static func format_signal_signature(sig : Dictionary) -> String:
return format_method_signature(sig, "{name}({args})")
static func get_type_property_list(type : Dictionary) -> Array:
match type["type"]:
TYPE_VECTOR2:
return [
{"name": "x", "type": TYPE_REAL},
{"name": "y", "type": TYPE_REAL}
]
TYPE_RECT2:
return [
{"name": "position", "type": TYPE_VECTOR2},
{"name": "size", "type": TYPE_VECTOR2},
{"name": "end", "type": TYPE_VECTOR2}
]
TYPE_VECTOR3:
return [
{"name": "x", "type": TYPE_REAL},
{"name": "y", "type": TYPE_REAL},
{"name": "z", "type": TYPE_REAL}
]
TYPE_TRANSFORM2D:
return [
{"name": "x", "type": TYPE_VECTOR2},
{"name": "y", "type": TYPE_VECTOR2},
{"name": "origin", "type": TYPE_VECTOR2}
]
TYPE_PLANE:
return [
{"name": "normal", "type": TYPE_VECTOR3},
{"name": "x", "type": TYPE_REAL},
{"name": "y", "type": TYPE_REAL},
{"name": "z", "type": TYPE_REAL},
{"name": "d", "type": TYPE_REAL}
]
TYPE_QUAT:
return [
{"name": "x", "type": TYPE_REAL},
{"name": "y", "type": TYPE_REAL},
{"name": "z", "type": TYPE_REAL},
{"name": "w", "type": TYPE_REAL}
]
TYPE_AABB:
return [
{"name": "position", "type": TYPE_VECTOR3},
{"name": "size", "type": TYPE_VECTOR3},
{"name": "end", "type": TYPE_VECTOR3}
]
TYPE_BASIS:
return [
{"name": "x", "type": TYPE_VECTOR3},
{"name": "y", "type": TYPE_VECTOR3},
{"name": "z", "type": TYPE_VECTOR3}
]
TYPE_TRANSFORM:
return [
{"name": "basis", "type": TYPE_BASIS},
{"name": "origin", "type": TYPE_VECTOR3}
]
TYPE_COLOR:
return [
{"name": "r", "type": TYPE_REAL},
{"name": "g", "type": TYPE_REAL},
{"name": "b", "type": TYPE_REAL},
{"name": "a", "type": TYPE_REAL},
{"name": "h", "type": TYPE_REAL},
{"name": "s", "type": TYPE_REAL},
{"name": "v", "type": TYPE_REAL},
{"name": "r8", "type": TYPE_INT},
{"name": "g8", "type": TYPE_INT},
{"name": "b8", "type": TYPE_INT},
{"name": "a8", "type": TYPE_INT}
]
TYPE_OBJECT:
if type.has("class_name"):
return ClassDB.class_get_property_list(type["class_name"])
return []
static func get_property_type(obj : Object, property : String) -> int:
return typeof(obj.get(property)) # TODO
static func get_method_arg_types(obj : Object, method : String) -> Array:
var methods := obj.get_method_list()
var types := []
for ele in methods:
if ele["name"] == method:
for arg in ele["args"]:
types.append(arg["type"])
break
return types
static func get_scene_tree() -> SceneTree:
return Engine.get_main_loop() as SceneTree
static func wait_frames(nframes : int) -> void:
var tree := get_scene_tree()
for i in range(max(1, nframes)): # yield at least once so this always returns a GDScriptFunctionState
yield(tree, "idle_frame")
static func disconnect_all(sender : Object, receiver : Object, signal_name := "") -> void:
if signal_name == "":
for sig in sender.get_signal_list():
disconnect_all(sender, receiver, sig["name"])
return
for connection in sender.get_signal_connection_list(signal_name):
if connection["target"] != receiver:
continue
sender.disconnect(signal_name, receiver, connection["method"])
static func copy_signal_handlers(target : Object, source : Object, sig_name : String) -> void:
for connection in source.get_signal_connection_list(sig_name):
target.connect(sig_name, connection["target"], connection["method"], connection["binds"])
static func copy_all_signal_handlers(target : Object, source : Object, sig_names : Array) -> void:
for sig_name in sig_names:
copy_signal_handlers(target, source, sig_name)
static func control_is_in_front_of(front : Control, back : Control) -> bool:
var front_popup : bool = find_parent_with_type(front, Popup) != null
var back_popup : bool = find_parent_with_type(back, Popup) != null
if front_popup && !back_popup:
return true
elif !front_popup && back_popup:
return false
elif front_popup && back_popup:
return popup_is_in_front_of(front, back)
else:
return back.is_greater_than(front)
static func popup_is_in_front_of(front : Popup, back : Popup) -> bool:
return back.is_greater_than(front) # no idea?!
static func get_state_object(meta_key : String, type):
var tree := get_scene_tree()
if !tree.has_meta(meta_key):
tree.set_meta(meta_key, type.new())
return tree.get_meta(meta_key)
static func translate(text : String) -> String:
return get_scene_tree().root.tr(text)
#################
# private stuff #
#################
static func __find_nodes_by_type(root : Node, tp, as_path, result):
if root is tp:
if as_path:
result.append(root.get_path())
else:
result.append(root)
for child in root.get_children():
__find_nodes_by_type(child, tp, as_path, result)
return result
static func __find_nodes_with_member(root : Node, member : String, result):
if root.get(member) != null:
result.append(root)
for child in root.get_children():
__find_nodes_with_member(child, member, result)