562 lines
15 KiB
GDScript
562 lines
15 KiB
GDScript
@tool
|
|
|
|
const HT_Errors = preload("./errors.gd")
|
|
|
|
|
|
# Godot has this internally but doesn't expose it
|
|
static func next_power_of_two(x: int) -> int:
|
|
x -= 1
|
|
x |= x >> 1
|
|
x |= x >> 2
|
|
x |= x >> 4
|
|
x |= x >> 8
|
|
x |= x >> 16
|
|
x += 1
|
|
return x
|
|
|
|
|
|
# CubeMesh doesn't have a wireframe option
|
|
static func create_wirecube_mesh(color = Color(1,1,1)) -> Mesh:
|
|
var positions := PackedVector3Array([
|
|
Vector3(0, 0, 0),
|
|
Vector3(1, 0, 0),
|
|
Vector3(1, 0, 1),
|
|
Vector3(0, 0, 1),
|
|
Vector3(0, 1, 0),
|
|
Vector3(1, 1, 0),
|
|
Vector3(1, 1, 1),
|
|
Vector3(0, 1, 1),
|
|
])
|
|
var colors := PackedColorArray([
|
|
color, color, color, color,
|
|
color, color, color, color,
|
|
])
|
|
var indices := PackedInt32Array([
|
|
0, 1,
|
|
1, 2,
|
|
2, 3,
|
|
3, 0,
|
|
|
|
4, 5,
|
|
5, 6,
|
|
6, 7,
|
|
7, 4,
|
|
|
|
0, 4,
|
|
1, 5,
|
|
2, 6,
|
|
3, 7
|
|
])
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = positions
|
|
arrays[Mesh.ARRAY_COLOR] = colors
|
|
arrays[Mesh.ARRAY_INDEX] = indices
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
|
|
return mesh
|
|
|
|
|
|
static func integer_square_root(x: int) -> int:
|
|
assert(typeof(x) == TYPE_INT)
|
|
var r := int(roundf(sqrt(x)))
|
|
if r * r == x:
|
|
return r
|
|
# Does not exist
|
|
return -1
|
|
|
|
|
|
# Formats integer using a separator between each 3-digit group
|
|
static func format_integer(n: int, sep := ",") -> String:
|
|
assert(typeof(n) == TYPE_INT)
|
|
|
|
var negative := false
|
|
if n < 0:
|
|
negative = true
|
|
n = -n
|
|
|
|
var s = ""
|
|
while n >= 1000:
|
|
s = str(sep, str(n % 1000).pad_zeros(3), s)
|
|
n /= 1000
|
|
|
|
if negative:
|
|
return str("-", str(n), s)
|
|
else:
|
|
return str(str(n), s)
|
|
|
|
|
|
# Goes up all parents until a node of the given class is found
|
|
static func get_node_in_parents(node: Node, klass) -> Node:
|
|
while node != null:
|
|
node = node.get_parent()
|
|
if node != null and is_instance_of(node, klass):
|
|
return node
|
|
return null
|
|
|
|
|
|
# Goes down all children until a node of the given class is found
|
|
static func find_first_node(node: Node, klass) -> Node:
|
|
if is_instance_of(node, klass):
|
|
return node
|
|
for i in node.get_child_count():
|
|
var child := node.get_child(i)
|
|
var found_node := find_first_node(child, klass)
|
|
if found_node != null:
|
|
return found_node
|
|
return null
|
|
|
|
|
|
static func is_in_edited_scene(node: Node) -> bool:
|
|
if not node.is_inside_tree():
|
|
return false
|
|
var edited_scene := node.get_tree().edited_scene_root
|
|
if node == edited_scene:
|
|
return true
|
|
return edited_scene != null and edited_scene.is_ancestor_of(node)
|
|
|
|
|
|
# Get an extended or cropped version of an image,
|
|
# with optional anchoring to decide in which direction to extend or crop.
|
|
# New pixels are filled with the provided fill color.
|
|
static func get_cropped_image(src: Image, width: int, height: int,
|
|
fill_color=null, anchor=Vector2(-1, -1)) -> Image:
|
|
|
|
width = int(width)
|
|
height = int(height)
|
|
if width == src.get_width() and height == src.get_height():
|
|
return src
|
|
var im := Image.create(width, height, false, src.get_format())
|
|
if fill_color != null:
|
|
im.fill(fill_color)
|
|
var p = get_cropped_image_params(
|
|
src.get_width(), src.get_height(), width, height, anchor)
|
|
im.blit_rect(src, p.src_rect, p.dst_pos)
|
|
return im
|
|
|
|
|
|
static func get_cropped_image_params(src_w: int, src_h: int, dst_w: int, dst_h: int,
|
|
anchor: Vector2) -> Dictionary:
|
|
|
|
var rel_anchor := (anchor + Vector2(1, 1)) / 2.0
|
|
|
|
var dst_x := (dst_w - src_w) * rel_anchor.x
|
|
var dst_y := (dst_h - src_h) * rel_anchor.y
|
|
|
|
var src_x := 0
|
|
var src_y := 0
|
|
|
|
if dst_x < 0:
|
|
src_x -= dst_x
|
|
src_w -= dst_x
|
|
dst_x = 0
|
|
|
|
if dst_y < 0:
|
|
src_y -= dst_y
|
|
src_h -= dst_y
|
|
dst_y = 0
|
|
|
|
if dst_x + src_w >= dst_w:
|
|
src_w = dst_w - dst_x
|
|
|
|
if dst_y + src_h >= dst_h:
|
|
src_h = dst_h - dst_y
|
|
|
|
return {
|
|
"src_rect": Rect2i(src_x, src_y, src_w, src_h),
|
|
"dst_pos": Vector2i(dst_x, dst_y)
|
|
}
|
|
|
|
# TODO Workaround for https://github.com/godotengine/godot/issues/24488
|
|
# TODO Simplify in Godot 3.1 if that's still not fixed,
|
|
# using https://github.com/godotengine/godot/pull/21806
|
|
# And actually that function does not even work.
|
|
#static func get_shader_param_or_default(mat: Material, name: String):
|
|
# assert(mat.shader != null)
|
|
# var v = mat.get_shader_param(name)
|
|
# if v != null:
|
|
# return v
|
|
# var params = VisualServer.shader_get_param_list(mat.shader)
|
|
# for p in params:
|
|
# if p.name == name:
|
|
# match p.type:
|
|
# TYPE_OBJECT:
|
|
# return null
|
|
# # I should normally check default values,
|
|
# # however they are not accessible
|
|
# TYPE_BOOL:
|
|
# return false
|
|
# TYPE_REAL:
|
|
# return 0.0
|
|
# TYPE_VECTOR2:
|
|
# return Vector2()
|
|
# TYPE_VECTOR3:
|
|
# return Vector3()
|
|
# TYPE_COLOR:
|
|
# return Color()
|
|
# return null
|
|
|
|
|
|
# Generic way to apply editor scale to a plugin UI scene.
|
|
# It is slower than doing it manually on specific controls.
|
|
# Takes a node as root because since Godot 4 Window dialogs are no longer Controls.
|
|
static func apply_dpi_scale(root: Node, dpi_scale: float):
|
|
if dpi_scale == 1.0:
|
|
return
|
|
var to_process := [root]
|
|
while len(to_process) > 0:
|
|
var node : Node = to_process[-1]
|
|
to_process.pop_back()
|
|
if node is Window:
|
|
node.size = Vector2(node.size) * dpi_scale
|
|
elif node is Viewport or node is SubViewport:
|
|
continue
|
|
elif node is Control:
|
|
if node.custom_minimum_size != Vector2(0, 0):
|
|
node.custom_minimum_size = node.custom_minimum_size * dpi_scale
|
|
var parent = node.get_parent()
|
|
if parent != null:
|
|
if not (parent is Container):
|
|
node.offset_bottom *= dpi_scale
|
|
node.offset_left *= dpi_scale
|
|
node.offset_top *= dpi_scale
|
|
node.offset_right *= dpi_scale
|
|
for i in node.get_child_count():
|
|
to_process.append(node.get_child(i))
|
|
|
|
|
|
# TODO AABB has `intersects_segment` but doesn't provide the hit point
|
|
# So we have to rely on a less efficient method.
|
|
# Returns a list of intersections between an AABB and a segment, sorted
|
|
# by distance to the beginning of the segment.
|
|
static func get_aabb_intersection_with_segment(aabb: AABB,
|
|
segment_begin: Vector3, segment_end: Vector3) -> Array:
|
|
|
|
var hits := []
|
|
|
|
if not aabb.intersects_segment(segment_begin, segment_end):
|
|
return hits
|
|
|
|
var hit
|
|
|
|
var x_rect := Rect2(aabb.position.y, aabb.position.z, aabb.size.y, aabb.size.z)
|
|
|
|
hit = Plane(Vector3(1, 0, 0), aabb.position.x) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and x_rect.has_point(Vector2(hit.y, hit.z)):
|
|
hits.append(hit)
|
|
|
|
hit = Plane(Vector3(1, 0, 0), aabb.end.x) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and x_rect.has_point(Vector2(hit.y, hit.z)):
|
|
hits.append(hit)
|
|
|
|
var y_rect := Rect2(aabb.position.x, aabb.position.z, aabb.size.x, aabb.size.z)
|
|
|
|
hit = Plane(Vector3(0, 1, 0), aabb.position.y) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and y_rect.has_point(Vector2(hit.x, hit.z)):
|
|
hits.append(hit)
|
|
|
|
hit = Plane(Vector3(0, 1, 0), aabb.end.y) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and y_rect.has_point(Vector2(hit.x, hit.z)):
|
|
hits.append(hit)
|
|
|
|
var z_rect := Rect2(aabb.position.x, aabb.position.y, aabb.size.x, aabb.size.y)
|
|
|
|
hit = Plane(Vector3(0, 0, 1), aabb.position.z) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and z_rect.has_point(Vector2(hit.x, hit.y)):
|
|
hits.append(hit)
|
|
|
|
hit = Plane(Vector3(0, 0, 1), aabb.end.z) \
|
|
.intersects_segment(segment_begin, segment_end)
|
|
if hit != null and z_rect.has_point(Vector2(hit.x, hit.y)):
|
|
hits.append(hit)
|
|
|
|
if len(hits) == 2:
|
|
# The segment has two hit points. Sort them by distance
|
|
var d0 = hits[0].distance_squared_to(segment_begin)
|
|
var d1 = hits[1].distance_squared_to(segment_begin)
|
|
if d0 > d1:
|
|
var temp = hits[0]
|
|
hits[0] = hits[1]
|
|
hits[1] = temp
|
|
else:
|
|
assert(len(hits) < 2)
|
|
|
|
return hits
|
|
|
|
|
|
class HT_GridRaytraceResult2D:
|
|
var hit_cell_pos: Vector2
|
|
var prev_cell_pos: Vector2
|
|
|
|
|
|
# Iterates through a virtual 2D grid of unit-sized square cells,
|
|
# and executes an action on each cell intersecting the given segment,
|
|
# ordered from begin to end.
|
|
# One of my most re-used pieces of code :)
|
|
#
|
|
# Initially inspired by http://www.cse.yorku.ca/~amana/research/grid.pdf
|
|
#
|
|
# Ported from https://github.com/bulletphysics/bullet3/blob/
|
|
# 687780af6b491056700cfb22cab57e61aeec6ab8/src/BulletCollision/CollisionShapes/
|
|
# btHeightfieldTerrainShape.cpp#L418
|
|
#
|
|
static func grid_raytrace_2d(ray_origin: Vector2, ray_direction: Vector2,
|
|
quad_predicate: Callable, max_distance: float) -> HT_GridRaytraceResult2D:
|
|
|
|
if max_distance < 0.0001:
|
|
# Consider the ray is too small to hit anything
|
|
return null
|
|
|
|
var xi_step := 0
|
|
if ray_direction.x > 0:
|
|
xi_step = 1
|
|
elif ray_direction.x < 0:
|
|
xi_step = -1
|
|
|
|
var yi_step := 0
|
|
if ray_direction.y > 0:
|
|
yi_step = 1
|
|
elif ray_direction.y < 0:
|
|
yi_step = -1
|
|
|
|
var infinite := 9999999.0
|
|
|
|
var param_delta_x := infinite
|
|
if xi_step != 0:
|
|
param_delta_x = 1.0 / absf(ray_direction.x)
|
|
|
|
var param_delta_y := infinite
|
|
if yi_step != 0:
|
|
param_delta_y = 1.0 / absf(ray_direction.y)
|
|
|
|
# pos = param * dir
|
|
# At which value of `param` we will cross a x-axis lane?
|
|
var param_cross_x := infinite
|
|
# At which value of `param` we will cross a y-axis lane?
|
|
var param_cross_y := infinite
|
|
|
|
# param_cross_x and param_cross_z are initialized as being the first cross
|
|
# X initialization
|
|
if xi_step != 0:
|
|
if xi_step == 1:
|
|
param_cross_x = (ceilf(ray_origin.x) - ray_origin.x) * param_delta_x
|
|
else:
|
|
param_cross_x = (ray_origin.x - floorf(ray_origin.x)) * param_delta_x
|
|
else:
|
|
# Will never cross on X
|
|
param_cross_x = infinite
|
|
|
|
# Y initialization
|
|
if yi_step != 0:
|
|
if yi_step == 1:
|
|
param_cross_y = (ceilf(ray_origin.y) - ray_origin.y) * param_delta_y
|
|
else:
|
|
param_cross_y = (ray_origin.y - floorf(ray_origin.y)) * param_delta_y
|
|
else:
|
|
# Will never cross on Y
|
|
param_cross_y = infinite
|
|
|
|
var x := int(floorf(ray_origin.x))
|
|
var y := int(floorf(ray_origin.y))
|
|
|
|
# Workaround cases where the ray starts at an integer position
|
|
if param_cross_x == 0.0:
|
|
param_cross_x += param_delta_x
|
|
# If going backwards, we should ignore the position we would get by the above flooring,
|
|
# because the ray is not heading in that direction
|
|
if xi_step == -1:
|
|
x -= 1
|
|
|
|
if param_cross_y == 0.0:
|
|
param_cross_y += param_delta_y
|
|
if yi_step == -1:
|
|
y -= 1
|
|
|
|
var prev_x := x
|
|
var prev_y := y
|
|
var param := 0.0
|
|
var prev_param := 0.0
|
|
|
|
while true:
|
|
prev_x = x
|
|
prev_y = y
|
|
prev_param = param
|
|
|
|
if param_cross_x < param_cross_y:
|
|
# X lane
|
|
x += xi_step
|
|
# Assign before advancing the param,
|
|
# to be in sync with the initialization step
|
|
param = param_cross_x
|
|
param_cross_x += param_delta_x
|
|
|
|
else:
|
|
# Y lane
|
|
y += yi_step
|
|
param = param_cross_y
|
|
param_cross_y += param_delta_y
|
|
|
|
if param > max_distance:
|
|
param = max_distance
|
|
# quad coordinates, enter param, exit/end param
|
|
if quad_predicate.call(prev_x, prev_y, prev_param, param):
|
|
var res := HT_GridRaytraceResult2D.new()
|
|
res.hit_cell_pos = Vector2(x, y)
|
|
res.prev_cell_pos = Vector2(prev_x, prev_y)
|
|
return res
|
|
else:
|
|
break
|
|
|
|
elif quad_predicate.call(prev_x, prev_y, prev_param, param):
|
|
var res := HT_GridRaytraceResult2D.new()
|
|
res.hit_cell_pos = Vector2(x, y)
|
|
res.prev_cell_pos = Vector2(prev_x, prev_y)
|
|
return res
|
|
|
|
return null
|
|
|
|
|
|
static func get_segment_clipped_by_rect(rect: Rect2,
|
|
segment_begin: Vector2, segment_end: Vector2) -> Array:
|
|
|
|
# /
|
|
# A-----/---B A-----+---B
|
|
# | / | => | / |
|
|
# | / | | / |
|
|
# C--/------D C--+------D
|
|
# /
|
|
|
|
if rect.has_point(segment_begin) and rect.has_point(segment_end):
|
|
return [segment_begin, segment_end]
|
|
|
|
var a := rect.position
|
|
var b := Vector2(rect.end.x, rect.position.y)
|
|
var c := Vector2(rect.position.x, rect.end.y)
|
|
var d := rect.end
|
|
|
|
var ab = Geometry2D.segment_intersects_segment(segment_begin, segment_end, a, b)
|
|
var cd = Geometry2D.segment_intersects_segment(segment_begin, segment_end, c, d)
|
|
var ac = Geometry2D.segment_intersects_segment(segment_begin, segment_end, a, c)
|
|
var bd = Geometry2D.segment_intersects_segment(segment_begin, segment_end, b, d)
|
|
|
|
var hits = []
|
|
if ab != null:
|
|
hits.append(ab)
|
|
if cd != null:
|
|
hits.append(cd)
|
|
if ac != null:
|
|
hits.append(ac)
|
|
if bd != null:
|
|
hits.append(bd)
|
|
|
|
# Now we need to order the hits from begin to end
|
|
if len(hits) == 1:
|
|
if rect.has_point(segment_begin):
|
|
hits = [segment_begin, hits[0]]
|
|
elif rect.has_point(segment_end):
|
|
hits = [hits[0], segment_end]
|
|
else:
|
|
# TODO This has a tendency to happen with integer coordinates...
|
|
# How can you get only 1 hit and have no end of the segment
|
|
# inside of the rectangle? Float precision shit? Assume no hit...
|
|
return []
|
|
|
|
elif len(hits) == 2:
|
|
var d0 = hits[0].distance_squared_to(segment_begin)
|
|
var d1 = hits[1].distance_squared_to(segment_begin)
|
|
if d0 > d1:
|
|
hits = [hits[1], hits[0]]
|
|
|
|
return hits
|
|
|
|
|
|
static func get_pixel_clamped(im: Image, x: int, y: int) -> Color:
|
|
x = clampi(x, 0, im.get_width() - 1)
|
|
y = clampi(y, 0, im.get_height() - 1)
|
|
return im.get_pixel(x, y)
|
|
|
|
|
|
static func update_configuration_warning(node: Node, recursive: bool):
|
|
if not Engine.is_editor_hint():
|
|
return
|
|
node.update_configuration_warnings()
|
|
if recursive:
|
|
for i in node.get_child_count():
|
|
var child = node.get_child(i)
|
|
update_configuration_warning(child, true)
|
|
|
|
|
|
static func write_import_file(settings: Dictionary, imp_fpath: String, logger) -> bool:
|
|
# TODO Should use ConfigFile instead
|
|
var f := FileAccess.open(imp_fpath, FileAccess.WRITE)
|
|
if f == null:
|
|
var err = FileAccess.get_open_error()
|
|
logger.error("Could not open '{0}' for write, error {1}" \
|
|
.format([imp_fpath, HT_Errors.get_message(err)]))
|
|
return false
|
|
|
|
for section in settings:
|
|
f.store_line(str("[", section, "]"))
|
|
f.store_line("")
|
|
var params = settings[section]
|
|
for key in params:
|
|
var v = params[key]
|
|
var sv
|
|
match typeof(v):
|
|
TYPE_STRING:
|
|
sv = str('"', v.replace('"', '\"'), '"')
|
|
TYPE_BOOL:
|
|
sv = "true" if v else "false"
|
|
_:
|
|
sv = str(v)
|
|
f.store_line(str(key, "=", sv))
|
|
f.store_line("")
|
|
|
|
return true
|
|
|
|
|
|
static func update_texture_partial(
|
|
tex: ImageTexture, im: Image, src_rect: Rect2i, dst_pos: Vector2i):
|
|
|
|
# ..ooo@@@XXX%%%xx..
|
|
# .oo@@XXX%x%xxx.. ` .
|
|
# .o@XX%%xx.. ` .
|
|
# o@X%.. ..ooooooo
|
|
# .@X%x. ..o@@^^ ^^@@o
|
|
# .ooo@@@@@@ooo.. ..o@@^ @X%
|
|
# o@@^^^ ^^^@@@ooo.oo@@^ %
|
|
# xzI -*-- ^^^o^^ --*- %
|
|
# @@@o ooooooo^@@^o^@X^@oooooo .X%x
|
|
# I@@@@@@@@@XX%%xx ( o@o )X%x@ROMBASED@@@X%x
|
|
# I@@@@XX%%xx oo@@@@X% @@X%x ^^^@@@@@@@X%x
|
|
# @X%xx o@@@@@@@X% @@XX%%x ) ^^@X%x
|
|
# ^ xx o@@@@@@@@Xx ^ @XX%%x xxx
|
|
# o@@^^^ooo I^^ I^o ooo . x
|
|
# oo @^ IX I ^X @^ oo
|
|
# IX U . V IX
|
|
# V . . V
|
|
#
|
|
|
|
# TODO Optimize: Godot 4 has lost the ability to update textures partially!
|
|
var fuck = tex.get_image()
|
|
fuck.blit_rect(im, src_rect, dst_pos)
|
|
tex.update(fuck)
|
|
|
|
|
|
# Should be used because if `set_shader_parameter` has never been called, `get_shader_parameter`
|
|
# will return null even if the shader's corresponding uniform has a default value.
|
|
# See https://github.com/godotengine/godot/issues/44454
|
|
static func get_shader_material_parameter(material: ShaderMaterial, param_name: StringName):
|
|
var v = material.get_shader_parameter(param_name)
|
|
if v == null:
|
|
var shader : Shader = material.shader
|
|
if shader != null:
|
|
v = RenderingServer.shader_get_parameter_default(shader.get_rid(), param_name)
|
|
return v
|
|
|
|
|