first commit
This commit is contained in:
65
addons/zylann.hterrain/util/direct_mesh_instance.gd
Executable file
65
addons/zylann.hterrain/util/direct_mesh_instance.gd
Executable 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)
|
||||
|
||||
1
addons/zylann.hterrain/util/direct_mesh_instance.gd.uid
Normal file
1
addons/zylann.hterrain/util/direct_mesh_instance.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://6gbprtd5bkc5
|
||||
48
addons/zylann.hterrain/util/direct_multimesh_instance.gd
Executable file
48
addons/zylann.hterrain/util/direct_multimesh_instance.gd
Executable 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d08q4dkqyljl8
|
||||
58
addons/zylann.hterrain/util/errors.gd
Executable file
58
addons/zylann.hterrain/util/errors.gd
Executable 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])
|
||||
|
||||
1
addons/zylann.hterrain/util/errors.gd.uid
Normal file
1
addons/zylann.hterrain/util/errors.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3g2wirxbp7s7
|
||||
203
addons/zylann.hterrain/util/grid.gd
Executable file
203
addons/zylann.hterrain/util/grid.gd
Executable 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
|
||||
|
||||
1
addons/zylann.hterrain/util/grid.gd.uid
Normal file
1
addons/zylann.hterrain/util/grid.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b753i2lh1lcjk
|
||||
291
addons/zylann.hterrain/util/image_file_cache.gd
Executable file
291
addons/zylann.hterrain/util/image_file_cache.gd
Executable 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
|
||||
|
||||
|
||||
1
addons/zylann.hterrain/util/image_file_cache.gd.uid
Normal file
1
addons/zylann.hterrain/util/image_file_cache.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c2j8n5pqsnpv4
|
||||
34
addons/zylann.hterrain/util/logger.gd
Executable file
34
addons/zylann.hterrain/util/logger.gd
Executable 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)
|
||||
|
||||
1
addons/zylann.hterrain/util/logger.gd.uid
Normal file
1
addons/zylann.hterrain/util/logger.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bcr0harkffkbc
|
||||
549
addons/zylann.hterrain/util/util.gd
Executable file
549
addons/zylann.hterrain/util/util.gd
Executable 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)
|
||||
|
||||
1
addons/zylann.hterrain/util/util.gd.uid
Normal file
1
addons/zylann.hterrain/util/util.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bikqbdxom87jw
|
||||
109
addons/zylann.hterrain/util/xyz_format.gd
Executable file
109
addons/zylann.hterrain/util/xyz_format.gd
Executable 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))
|
||||
|
||||
1
addons/zylann.hterrain/util/xyz_format.gd.uid
Normal file
1
addons/zylann.hterrain/util/xyz_format.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dacow3744l3c4
|
||||
Reference in New Issue
Block a user