first commit

This commit is contained in:
Ugric
2026-03-02 02:17:04 +00:00
commit 5d56860f3a
813 changed files with 41799 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
@tool
# Implementation of MeshInstance which doesn't use the scene tree
var _mesh_instance := RID()
# Need to keep a reference so that the mesh RID doesn't get freed
var _mesh : Mesh
func _init():
var rs = RenderingServer
_mesh_instance = rs.instance_create()
rs.instance_set_visible(_mesh_instance, true)
func _notification(p_what: int):
if p_what == NOTIFICATION_PREDELETE:
if _mesh_instance != RID():
RenderingServer.free_rid(_mesh_instance)
_mesh_instance = RID()
func enter_world(world: World3D):
assert(_mesh_instance != RID())
RenderingServer.instance_set_scenario(_mesh_instance, world.get_scenario())
func exit_world():
assert(_mesh_instance != RID())
RenderingServer.instance_set_scenario(_mesh_instance, RID())
func set_world(world: World3D):
if world != null:
enter_world(world)
else:
exit_world()
func set_transform(world_transform: Transform3D):
assert(_mesh_instance != RID())
RenderingServer.instance_set_transform(_mesh_instance, world_transform)
func set_mesh(mesh: Mesh):
assert(_mesh_instance != RID())
RenderingServer.instance_set_base(_mesh_instance, mesh.get_rid() if mesh != null else RID())
_mesh = mesh
func set_material(material: Material):
assert(_mesh_instance != RID())
RenderingServer.instance_geometry_set_material_override( \
_mesh_instance, material.get_rid() if material != null else RID())
func set_visible(visible: bool):
assert(_mesh_instance != RID())
RenderingServer.instance_set_visible(_mesh_instance, visible)
func set_aabb(aabb: AABB):
assert(_mesh_instance != RID())
RenderingServer.instance_set_custom_aabb(_mesh_instance, aabb)

View File

@@ -0,0 +1 @@
uid://6gbprtd5bkc5

View File

@@ -0,0 +1,48 @@
@tool
# Implementation of MultiMeshInstance which doesn't use the scene tree
var _multimesh_instance := RID()
func _init():
_multimesh_instance = RenderingServer.instance_create()
func _notification(what: int):
if what == NOTIFICATION_PREDELETE:
RenderingServer.free_rid(_multimesh_instance)
func set_world(world: World3D):
RenderingServer.instance_set_scenario(
_multimesh_instance, world.get_scenario() if world != null else RID())
func set_visible(visible: bool):
RenderingServer.instance_set_visible(_multimesh_instance, visible)
func set_transform(trans: Transform3D):
RenderingServer.instance_set_transform(_multimesh_instance, trans)
func set_multimesh(mm: MultiMesh):
RenderingServer.instance_set_base(_multimesh_instance, mm.get_rid() if mm != null else RID())
func set_material_override(material: Material):
RenderingServer.instance_geometry_set_material_override( \
_multimesh_instance, material.get_rid() if material != null else RID())
func set_aabb(aabb: AABB):
RenderingServer.instance_set_custom_aabb(_multimesh_instance, aabb)
func set_layer_mask(mask: int):
RenderingServer.instance_set_layer_mask(_multimesh_instance, mask)
func set_cast_shadow(cast_shadow: int):
RenderingServer.instance_geometry_set_cast_shadows_setting(_multimesh_instance, cast_shadow)

View File

@@ -0,0 +1 @@
uid://d08q4dkqyljl8

View File

@@ -0,0 +1,58 @@
@tool
# Taken from https://docs.godotengine.org/en/3.0/classes/class_@globalscope.html#enum-globalscope-error
const _names = {
OK: "ok",
FAILED: "Generic error.",
ERR_UNAVAILABLE: "Unavailable error",
ERR_UNCONFIGURED: "Unconfigured error",
ERR_UNAUTHORIZED: "Unauthorized error",
ERR_PARAMETER_RANGE_ERROR: "Parameter range error",
ERR_OUT_OF_MEMORY: "Out of memory (OOM) error",
ERR_FILE_NOT_FOUND: "File Not found error",
ERR_FILE_BAD_DRIVE: "File Bad drive error",
ERR_FILE_BAD_PATH: "File Bad path error",
ERR_FILE_NO_PERMISSION: "File No permission error",
ERR_FILE_ALREADY_IN_USE: "File Already in use error",
ERR_FILE_CANT_OPEN: "File Can't open error",
ERR_FILE_CANT_WRITE: "File Can't write error",
ERR_FILE_CANT_READ: "File Can't read error",
ERR_FILE_UNRECOGNIZED: "File Unrecognized error",
ERR_FILE_CORRUPT: "File Corrupt error",
ERR_FILE_MISSING_DEPENDENCIES: "File Missing dependencies error",
ERR_FILE_EOF: "File End of file (EOF) error",
ERR_CANT_OPEN: "Can't open error",
ERR_CANT_CREATE: "Can't create error",
ERR_QUERY_FAILED: "Query failed error",
ERR_ALREADY_IN_USE: "Already in use error",
ERR_LOCKED: "Locked error",
ERR_TIMEOUT: "Timeout error",
ERR_CANT_CONNECT: "Can't connect",
ERR_CANT_RESOLVE: "Can't resolve",
ERR_CONNECTION_ERROR: "Connection error",
ERR_CANT_ACQUIRE_RESOURCE: "Can't acquire resource error",
ERR_CANT_FORK: "Can't fork",
ERR_INVALID_DATA: "Invalid data error",
ERR_INVALID_PARAMETER: "Invalid parameter error",
ERR_ALREADY_EXISTS: "Already exists error",
ERR_DOES_NOT_EXIST: "Does not exist error",
ERR_DATABASE_CANT_READ: "Database Read error",
ERR_DATABASE_CANT_WRITE: "Database Write error",
ERR_COMPILATION_FAILED: "Compilation failed error",
ERR_METHOD_NOT_FOUND: "Method not found error",
ERR_LINK_FAILED: "Linking failed error",
ERR_SCRIPT_FAILED: "Script failed error",
ERR_CYCLIC_LINK: "Cycling link (import cycle) error",
ERR_INVALID_DECLARATION: "Invalid declaration",
ERR_DUPLICATE_SYMBOL: "Duplicate symbol",
ERR_PARSE_ERROR: "Parse error",
ERR_BUSY: "Busy error",
ERR_SKIP: "Skip error",
ERR_HELP: "Help error",
ERR_BUG: "Bug error",
ERR_PRINTER_ON_FIRE: "The printer is on fire"
}
static func get_message(err_code: int):
return str("[", err_code, "]: ", _names[err_code])

View File

@@ -0,0 +1 @@
uid://b3g2wirxbp7s7

View File

@@ -0,0 +1,203 @@
# Note: `tool` is optional but without it there are no error reporting in the editor
@tool
# TODO Remove grid_ prefixes, context is already given by the script itself
# Performs a positive integer division rounded to upper (4/2 = 2, 5/3 = 2)
static func up_div(a: int, b: int):
if a % b != 0:
return a / b + 1
return a / b
# Creates a 2D array as an array of arrays.
# if v is provided, all cells will contain the same value.
# if v is a funcref, it will be executed to fill the grid cell per cell.
static func create_grid(w: int, h: int, v=null):
var is_create_func = typeof(v) == TYPE_CALLABLE
var grid := []
grid.resize(h)
for y in range(grid.size()):
var row := []
row.resize(w)
if is_create_func:
for x in range(row.size()):
row[x] = v.call(x, y)
else:
for x in range(row.size()):
row[x] = v
grid[y] = row
return grid
# Creates a 2D array that is a copy of another 2D array
static func clone_grid(other_grid):
var grid := []
grid.resize(other_grid.size())
for y in range(0, grid.size()):
var row := []
var other_row = other_grid[y]
row.resize(other_row.size())
grid[y] = row
for x in range(0, row.size()):
row[x] = other_row[x]
return grid
# Resizes a 2D array and allows to set or call functions for each deleted and created cells.
# This is especially useful if cells contain objects and you don't want to loose existing data.
static func resize_grid(grid, new_width, new_height, create_func=null, delete_func=null):
# Check parameters
assert(new_width >= 0 and new_height >= 0)
assert(grid != null)
if delete_func != null:
assert(typeof(delete_func) == TYPE_CALLABLE)
# `create_func` can also be a default value
var is_create_func = typeof(create_func) == TYPE_CALLABLE
# Get old size (supposed to be rectangular!)
var old_height = grid.size()
var old_width = 0
if grid.size() != 0:
old_width = grid[0].size()
# Delete old rows
if new_height < old_height:
if delete_func != null:
for y in range(new_height, grid.size()):
var row = grid[y]
for x in len(row):
var elem = row[x]
delete_func.call(elem)
grid.resize(new_height)
# Delete old columns
if new_width < old_width:
for y in len(grid):
var row = grid[y]
if delete_func != null:
for x in range(new_width, row.size()):
var elem = row[x]
delete_func.call(elem)
row.resize(new_width)
# Create new columns
if new_width > old_width:
for y in len(grid):
var row = grid[y]
row.resize(new_width)
if is_create_func:
for x in range(old_width, new_width):
row[x] = create_func.call(x,y)
else:
for x in range(old_width, new_width):
row[x] = create_func
# Create new rows
if new_height > old_height:
grid.resize(new_height)
for y in range(old_height, new_height):
var row = []
row.resize(new_width)
grid[y] = row
if is_create_func:
for x in new_width:
row[x] = create_func.call(x,y)
else:
for x in new_width:
row[x] = create_func
# Debug test check
assert(grid.size() == new_height)
for y in len(grid):
assert(len(grid[y]) == new_width)
# Retrieves the minimum and maximum values from a grid
static func grid_min_max(grid):
if grid.size() == 0 or grid[0].size() == 0:
return [0,0]
var vmin = grid[0][0]
var vmax = vmin
for y in len(grid):
var row = grid[y]
for x in len(row):
var v = row[x]
if v > vmax:
vmax = v
elif v < vmin:
vmin = v
return [vmin, vmax]
# Copies a sub-region of a grid as a new grid. No boundary check!
static func grid_extract_area(src_grid, x0, y0, w, h):
var dst = create_grid(w, h)
for y in h:
var dst_row = dst[y]
var src_row = src_grid[y0+y]
for x in w:
dst_row[x] = src_row[x0+x]
return dst
# Extracts data and crops the result if the requested rect crosses the bounds
static func grid_extract_area_safe_crop(src_grid, x0, y0, w, h):
# Return empty is completely out of bounds
var gw = src_grid.size()
if gw == 0:
return []
var gh = src_grid[0].size()
if x0 >= gw or y0 >= gh:
return []
# Crop min pos
if x0 < 0:
w += x0
x0 = 0
if y0 < 0:
h += y0
y0 = 0
# Crop max pos
if x0 + w >= gw:
w = gw-x0
if y0 + h >= gh:
h = gh-y0
return grid_extract_area(src_grid, x0, y0, w, h)
# Sets values from a grid inside another grid. No boundary check!
static func grid_paste(src_grid, dst_grid, x0, y0):
for y in range(0, src_grid.size()):
var src_row = src_grid[y]
var dst_row = dst_grid[y0+y]
for x in range(0, src_row.size()):
dst_row[x0+x] = src_row[x]
# Tests if two grids are the same size and contain the same values
static func grid_equals(a, b):
if a.size() != b.size():
return false
for y in a.size():
var a_row = a[y]
var b_row = b[y]
if a_row.size() != b_row.size():
return false
for x in b_row.size():
if a_row[x] != b_row[x]:
return false
return true
static func grid_get_or_default(grid, x, y, defval=null):
if y >= 0 and y < len(grid):
var row = grid[y]
if x >= 0 and x < len(row):
return row[x]
return defval

View File

@@ -0,0 +1 @@
uid://b753i2lh1lcjk

View File

@@ -0,0 +1,291 @@
@tool
# Used to store temporary images on disk.
# This is useful for undo/redo as image edition can quickly fill up memory.
# Image data is stored in archive files together,
# because when dealing with many images it speeds up filesystem I/O on Windows.
# If the file exceeds a predefined size, a new one is created.
# Writing to disk is performed from a thread, to leave the main thread responsive.
# However if you want to obtain an image back while it didn't save yet, the main thread will block.
# When the application or plugin is closed, the files get cleared.
const HT_Logger = preload("./logger.gd")
const HT_Errors = preload("./errors.gd")
const CACHE_FILE_SIZE_THRESHOLD = 1048576
# For debugging
const USE_THREAD = true
var _cache_dir := ""
var _next_id := 0
var _session_id := ""
var _cache_image_info := {}
var _logger = HT_Logger.get_for(self)
var _current_cache_file_index := 0
var _cache_file_offset := 0
var _saving_thread := Thread.new()
var _save_queue := []
var _save_queue_mutex := Mutex.new()
var _save_semaphore := Semaphore.new()
var _save_thread_running := false
func _init(cache_dir: String):
assert(cache_dir != "")
_cache_dir = cache_dir
var rng := RandomNumberGenerator.new()
rng.randomize()
for i in 16:
_session_id += str(rng.randi() % 10)
_logger.debug(str("Image cache session ID: ", _session_id))
if not DirAccess.dir_exists_absolute(_cache_dir):
var err := DirAccess.make_dir_absolute(_cache_dir)
if err != OK:
_logger.error("Could not create directory {0}: {1}" \
.format([_cache_dir, HT_Errors.get_message(err)]))
_save_thread_running = true
if USE_THREAD:
_saving_thread.start(_save_thread_func)
# TODO Cannot cleanup the cache in destructor!
# Godot doesn't allow me to call clear()...
# https://github.com/godotengine/godot/issues/31166
func _notification(what: int):
if what == NOTIFICATION_PREDELETE:
#clear()
_save_thread_running = false
_save_semaphore.post()
if USE_THREAD:
_saving_thread.wait_to_finish()
func _create_new_cache_file(fpath: String):
var f := FileAccess.open(fpath, FileAccess.WRITE)
if f == null:
var err = FileAccess.get_open_error()
_logger.error("Failed to create new cache file {0}: {1}" \
.format([fpath, HT_Errors.get_message(err)]))
return
func _get_current_cache_file_name() -> String:
return _cache_dir.path_join(str(_session_id, "_", _current_cache_file_index, ".cache"))
func save_image(im: Image) -> int:
assert(im != null)
if im.has_mipmaps():
# TODO Add support for this? Didn't need it so far
_logger.error("Caching an image with mipmaps, this isn't supported")
var fpath := _get_current_cache_file_name()
if _next_id == 0:
# First file
_create_new_cache_file(fpath)
var id := _next_id
_next_id += 1
var item := {
# Duplicate the image so we are sure nothing funny will happen to it
# while the thread saves it
"image": im.duplicate(),
"path": fpath,
"data_offset": _cache_file_offset,
"saved": false
}
_cache_file_offset += _get_image_data_size(im)
if _cache_file_offset >= CACHE_FILE_SIZE_THRESHOLD:
_cache_file_offset = 0
_current_cache_file_index += 1
_create_new_cache_file(_get_current_cache_file_name())
_cache_image_info[id] = item
_save_queue_mutex.lock()
_save_queue.append(item)
_save_queue_mutex.unlock()
_save_semaphore.post()
if not USE_THREAD:
var before = Time.get_ticks_msec()
while len(_save_queue) > 0:
_save_thread_func()
if Time.get_ticks_msec() - before > 10_000:
_logger.error("Taking to long to empty save queue in non-threaded mode!")
return id
static func _get_image_data_size(im: Image) -> int:
return 1 + 4 + 4 + 4 + len(im.get_data())
static func _write_image(f: FileAccess, im: Image):
f.store_8(im.get_format())
f.store_32(im.get_width())
f.store_32(im.get_height())
var data : PackedByteArray = im.get_data()
f.store_32(len(data))
f.store_buffer(data)
static func _read_image(f: FileAccess) -> Image:
var format := f.get_8()
var width := f.get_32()
var height := f.get_32()
var data_size := f.get_32()
var data := f.get_buffer(data_size)
var im := Image.create_from_data(width, height, false, format, data)
return im
func load_image(id: int) -> Image:
var info := _cache_image_info[id] as Dictionary
var timeout := 5.0
var time_before := Time.get_ticks_msec()
# We could just grab `image`, because the thread only reads it.
# However it's still not safe to do that if we write or even lock it,
# so we have to assume it still has ownership of it.
while not info.saved:
OS.delay_msec(8.0)
_logger.debug("Waiting for cached image {0}...".format([id]))
if Time.get_ticks_msec() - time_before > timeout:
_logger.error("Could not get image {0} from cache. Something went wrong.".format([id]))
return null
var fpath := info.path as String
var f := FileAccess.open(fpath, FileAccess.READ)
if f == null:
var err := FileAccess.get_open_error()
_logger.error("Could not load cached image from {0}: {1}" \
.format([fpath, HT_Errors.get_message(err)]))
return null
f.seek(info.data_offset)
var im = _read_image(f)
f = null # close file
assert(im != null)
return im
func clear():
_logger.debug("Clearing image cache")
var dir := DirAccess.open(_cache_dir)
if dir == null:
#var err = DirAccess.get_open_error()
_logger.error("Could not open image file cache directory '{0}'" \
.format([_cache_dir]))
return
dir.include_hidden = false
dir.include_navigational = false
var err := dir.list_dir_begin()
if err != OK:
_logger.error("Could not start list_dir_begin in '{0}'".format([_cache_dir]))
return
# Delete all cache files
while true:
var fpath := dir.get_next()
if fpath == "":
break
if fpath.ends_with(".cache"):
_logger.debug(str("Deleting ", fpath))
err = dir.remove(fpath)
if err != OK:
_logger.error("Failed to delete cache file '{0}': {1}" \
.format([_cache_dir.path_join(fpath), HT_Errors.get_message(err)]))
_cache_image_info.clear()
func _save_thread_func():
# Threads keep a reference to the object of the function they run.
# So if the object is a Reference, and that reference owns the thread... we get a cycle.
# We can break the cycle by removing 1 to the count inside the thread.
# The thread's reference will never die unexpectedly because we stop and destroy the thread
# in the destructor of the reference.
# If that workaround explodes one day, another way could be to use an intermediary instance
# extending Object, and run a function on that instead.
#
# I added this in Godot 3, and it seems to still be relevant in Godot 4 because if I don't
# do it, objects are leaking.
#
# BUT it seems to end up triggering a crash in debug Godot builds due to unrefing RefCounted
# with refcount == 0, so I guess it's wrong now?
# So basically, either I do it and I risk a crash,
# or I don't do it and then it causes a leak...
# TODO Make this shit use `Object`
#
# if USE_THREAD:
# unreference()
while _save_thread_running:
_save_queue_mutex.lock()
var to_save := _save_queue.duplicate(false)
_save_queue.clear()
_save_queue_mutex.unlock()
if len(to_save) == 0:
if USE_THREAD:
_save_semaphore.wait()
continue
var f : FileAccess
var path := ""
for item in to_save:
# Keep re-using the same file if we did not change path.
# It makes I/Os faster.
if item.path != path:
# Close previous file
f = null
path = item.path
f = FileAccess.open(path, FileAccess.READ_WRITE)
if f == null:
var err := FileAccess.get_open_error()
call_deferred("_on_error", "Could not open file {0}: {1}" \
.format([path, HT_Errors.get_message(err)]))
path = ""
continue
f.seek(item.data_offset)
_write_image(f, item.image)
# Notify main thread.
# The thread does not modify data, only reads it.
call_deferred("_on_image_saved", item)
# Workaround some weird behavior in Godot 4:
# when the next loop runs, `f` IS NOT CLEANED UP. A reference is still held before `var f`
# is reached, which means the file is still locked while the thread is waiting on the
# semaphore... so I have to explicitely "close" the file here.
f = null
if not USE_THREAD:
break
func _on_error(msg: String):
_logger.error(msg)
func _on_image_saved(item: Dictionary):
_logger.debug(str("Saved ", item.path))
item.saved = true
# Should remove image from memory (for usually being last reference)
item.image = null

View File

@@ -0,0 +1 @@
uid://c2j8n5pqsnpv4

View File

@@ -0,0 +1,34 @@
@tool
class HT_LoggerBase:
var _context := ""
func _init(p_context):
_context = p_context
func debug(msg: String):
pass
func warn(msg: String):
push_warning("{0}: {1}".format([_context, msg]))
func error(msg: String):
push_error("{0}: {1}".format([_context, msg]))
class HT_LoggerVerbose extends HT_LoggerBase:
func _init(p_context: String):
super(p_context)
func debug(msg: String):
print(_context, ": ", msg)
static func get_for(owner: Object) -> HT_LoggerBase:
# Note: don't store the owner. If it's a Reference, it could create a cycle
var script : Script = owner.get_script()
var context := script.resource_path.get_file()
if OS.is_stdout_verbose():
return HT_LoggerVerbose.new(context)
return HT_LoggerBase.new(context)

View File

@@ -0,0 +1 @@
uid://bcr0harkffkbc

View File

@@ -0,0 +1,549 @@
@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)

View File

@@ -0,0 +1 @@
uid://bikqbdxom87jw

View File

@@ -0,0 +1,109 @@
@tool
# XYZ files are text files containing a list of 3D points.
# They can be found in GIS software as an export format for heightmaps.
# In order to turn it into a heightmap we may calculate bounds first
# to find the origin and then set points in an image.
class HT_XYZBounds:
# Note: it is important for these to be double-precision floats,
# GIS data can have large coordinates
var min_x := 0.0
var min_y := 0.0
var max_x := 0.0
var max_y := 0.0
var line_count := 0
var image_width := 0
var image_height := 0
# TODO `split_float` returns 32-bit floats, despite internally parsing doubles...
# Despite that, I still use it here because it doesn't seem to cause issues and is faster.
# If it becomes an issue, we'll have to switch to `split` and casting to `float`.
static func load_bounds(f: FileAccess) -> HT_XYZBounds:
# It is faster to get line and split floats than using CSV functions
var line := f.get_line()
var floats := line.split_floats(" ")
# We only care about X and Y, it makes less operations to do in the loop.
# Z is the height and will remain as-is at the end.
var min_pos_x := floats[0]
var min_pos_y := floats[1]
var max_pos_x := min_pos_x
var max_pos_y := min_pos_y
# Start at 1 because we just read the first line
var line_count := 1
# We know the file is a series of float triplets
while not f.eof_reached():
line = f.get_line()
# The last line can be empty
if len(line) < 2:
break
floats = line.split_floats(" ")
var pos_x := floats[0]
var pos_y := floats[1]
min_pos_x = minf(min_pos_x, pos_x)
min_pos_y = minf(min_pos_y, pos_y)
max_pos_x = maxf(max_pos_x, pos_x)
max_pos_y = maxf(max_pos_y, pos_y)
line_count += 1
var bounds := HT_XYZBounds.new()
bounds.min_x = min_pos_x
bounds.min_y = min_pos_y
bounds.max_x = max_pos_x
bounds.max_y = max_pos_y
bounds.line_count = line_count
bounds.image_width = int(max_pos_x - min_pos_x) + 1
bounds.image_height = int(max_pos_y - min_pos_y) + 1
return bounds
# Loads points into an image with existing dimensions and format.
# `f` must be positioned at the beginning of the series of points.
# If `bounds` is `null`, it will be computed.
static func load_heightmap(f: FileAccess, dst_image: Image, bounds: HT_XYZBounds):
# We are not going to read the entire file directly in memory, because it can be really big.
# Instead we'll parse it directly and the only thing we retain in memory is the heightmap.
# This can be really slow on big files. If we can assume the file is square and points
# separated by 1 unit each in a grid pattern, it could be a bit faster, but
# parsing points from text really is the main bottleneck (40 seconds to load a 2000x2000 file!).
# Bounds can be precalculated
if bounds == null:
var file_begin := f.get_position()
bounds = load_bounds(f)
f.seek(file_begin)
# Put min coordinates on the GDScript stack so they are faster to access
var min_pos_x := bounds.min_x
var min_pos_y := bounds.min_y
var line_count := bounds.line_count
for i in line_count:
var line := f.get_line()
var floats := line.split_floats(" ")
var x := int(floats[0] - min_pos_x)
var y := int(floats[1] - min_pos_y)
# Make sure the coordinate is inside the image,
# due to float imprecision or potentially non-grid-aligned points.
# Could use `Rect2` to check faster but it uses floats.
# `Rect2i` would be better but is only available in Godot 4.
if x >= 0 and y >= 0 and x < dst_image.get_width() and y < dst_image.get_height():
dst_image.set_pixel(x, y, Color(floats[2], 0, 0))

View File

@@ -0,0 +1 @@
uid://dacow3744l3c4