commit 5be8beb69fdeb88a325fcb72ea1e7e3fd0086430 Author: Patrick Wuttke Date: Sat Aug 7 21:13:07 2021 +0200 Initial commit diff --git a/plugin.cfg b/plugin.cfg new file mode 100644 index 0000000..f9c42dd --- /dev/null +++ b/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GDScript Basics" +description="Basic (and advanced) utility for gdscript." +author="Patrick Wuttke" +version="0.2" +script="plugin.gd" diff --git a/plugin.gd b/plugin.gd new file mode 100644 index 0000000..824ed80 --- /dev/null +++ b/plugin.gd @@ -0,0 +1,8 @@ +tool +extends EditorPlugin + +func _enter_tree(): + pass + +func _exit_tree(): + pass diff --git a/scripts/libs/algorithm.gd b/scripts/libs/algorithm.gd new file mode 100644 index 0000000..96a1606 --- /dev/null +++ b/scripts/libs/algorithm.gd @@ -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 diff --git a/scripts/libs/constants.gd b/scripts/libs/constants.gd new file mode 100644 index 0000000..0412406 --- /dev/null +++ b/scripts/libs/constants.gd @@ -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.") diff --git a/scripts/libs/coroutine.gd b/scripts/libs/coroutine.gd new file mode 100644 index 0000000..5edfd16 --- /dev/null +++ b/scripts/libs/coroutine.gd @@ -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 diff --git a/scripts/libs/debug.gd b/scripts/libs/debug.gd new file mode 100644 index 0000000..c65eafb --- /dev/null +++ b/scripts/libs/debug.gd @@ -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() diff --git a/scripts/libs/format.gd b/scripts/libs/format.gd new file mode 100644 index 0000000..e7bd6bc --- /dev/null +++ b/scripts/libs/format.gd @@ -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"] diff --git a/scripts/libs/fsutil.gd b/scripts/libs/fsutil.gd new file mode 100644 index 0000000..b0afbbf --- /dev/null +++ b/scripts/libs/fsutil.gd @@ -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": "", "commit": "???", "commit_short": "???"} + var text = file.get_line() + if !text.begins_with("ref:"): + return {"branch": "", "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": "", "commit_short": ""} + 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() diff --git a/scripts/libs/geoutil.gd b/scripts/libs/geoutil.gd new file mode 100644 index 0000000..30cf6af --- /dev/null +++ b/scripts/libs/geoutil.gd @@ -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) diff --git a/scripts/libs/graphing.gd b/scripts/libs/graphing.gd new file mode 100644 index 0000000..c4edf7b --- /dev/null +++ b/scripts/libs/graphing.gd @@ -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)) diff --git a/scripts/libs/math.gd b/scripts/libs/math.gd new file mode 100644 index 0000000..d1d2504 --- /dev/null +++ b/scripts/libs/math.gd @@ -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 diff --git a/scripts/libs/settings.gd b/scripts/libs/settings.gd new file mode 100644 index 0000000..ae0d1d7 --- /dev/null +++ b/scripts/libs/settings.gd @@ -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) diff --git a/scripts/libs/util.gd b/scripts/libs/util.gd new file mode 100644 index 0000000..247415e --- /dev/null +++ b/scripts/libs/util.gd @@ -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)