Joey Eamigh 9989fab018
addons?
2025-10-10 14:07:23 -04:00

152 lines
5.7 KiB
GDScript

extends RefCounted
## This class is not thread safe.
## Do not call install, remove or load at the same time.
## Any texture loading has to be done on the main thread: https://github.com/godotengine/godot/issues/86796
const Collection := preload("res://addons/icon_explorer/internal/scripts/collection.gd")
const Icon := preload("res://addons/icon_explorer/internal/scripts/icon.gd")
signal collection_installed(id: int, status: Error)
signal collection_removed(id: int, status: Error)
signal load_started()
signal load_finished()
var _loaded_collections: Array[int] = []
var _collections: Array[Collection] = []
var _icons: Array[Icon] = []
var _scene_tree: SceneTree
## no mutex required as loading is suspended while main thread runs
## as this progress is exclusively used in loading the textures
var _load_progress: float
var _processing_thread: Thread
func load_progress() -> float:
return self._load_progress
func _init(scene_tree: SceneTree = null) -> void:
self._scene_tree = scene_tree
for path: String in Collection.get_default_collection_paths():
self.register(load(path.path_join("/collection.gd")).new() as Collection)
func _notification(what: int):
if what == NOTIFICATION_PREDELETE:
if self._processing_thread != null:
self._processing_thread.wait_to_finish()
func collections() -> Array[Collection]:
return self._collections
func get_collection(id: int) -> Collection:
for coll: Collection in self._collections:
if coll.id() == id:
return coll
return null
func installed_collections() -> Array[Collection]:
return self._collections.filter(func(coll: Collection) -> bool: return coll.is_installed())
func icons() -> Array[Icon]:
return self._icons
# register each collection only once
func register(coll: Collection) -> void:
self._collections.append(coll)
coll._id = self._collections.size() - 1
func install(coll: Collection, http: HTTPRequest, version: String) -> void:
if self._processing_thread != null && self._processing_thread.is_alive():
return
if self._processing_thread != null:
self._processing_thread.wait_to_finish()
self._processing_thread = Thread.new()
self._processing_thread.start(self._install.bind(coll, http, version))
# THREAD FUNCTION
func _install(coll: Collection, http: HTTPRequest, version: String) -> void:
var status: Error = coll.install(http, version)
if status != Error.OK:
self._install_done.bind(coll.id(), status).call_deferred()
return
# remove loaded icons and unload collection, to load the new ones
if self._loaded_collections.has(coll.id()):
self._icons = self._icons.filter(func (icon: Icon) -> bool: return icon.collection.id() != coll.id())
self._loaded_collections.remove_at(self._loaded_collections.find(coll.id()))
self._load()
self._install_done.bind(coll.id(), status).call_deferred()
func _install_done(id: int, status: Error) -> void:
self.collection_installed.emit(id, status)
func remove(coll: Collection) -> void:
if self._processing_thread != null && self._processing_thread.is_alive():
return
if self._processing_thread != null:
self._processing_thread.wait_to_finish()
self._processing_thread = Thread.new()
self._processing_thread.start(self._remove.bind(coll))
# THREAD FUNCTION
func _remove(coll: Collection) -> void:
var status: Error = coll.remove()
self._remove_done.bind(coll.id(), status).call_deferred()
func _remove_done(id: int, status: Error) -> void:
self._icons = self._icons.filter(func (icon: Icon) -> bool: return icon.collection.id() != id)
self._loaded_collections.remove_at(self._loaded_collections.find(id))
self.collection_removed.emit(id, status)
func load() -> void:
if self._processing_thread != null && self._processing_thread.is_alive():
return
if self._processing_thread != null:
self._processing_thread.wait_to_finish()
self._processing_thread = Thread.new()
self.load_started.emit()
self._processing_thread.start(self._load)
# thread function
# does not call self.load_started.emit()
func _load() -> void:
var loaded_icons: Array[Icon] = []
var buffers: PackedStringArray = PackedStringArray()
self._load_progress = 0.0
for idx: int in range(self._collections.size()):
var coll: Collection = self._collections[idx]
if !self._loaded_collections.has(coll.id()) && coll.is_installed():
var res: Array = coll.load()
loaded_icons.append_array(res[0])
buffers.append_array(res[1])
self._loaded_collections.append(coll.id())
self._load_done.bind(loaded_icons, buffers).call_deferred()
func _load_done(loaded_icons: Array[Icon], buffers: PackedStringArray) -> void:
for idx: int in range(loaded_icons.size()):
if idx % 50 == 0:
self._load_progress = float(idx + 1) / loaded_icons.size() * 100.0
if self._scene_tree != null:
await self._scene_tree.process_frame
_load_texture(loaded_icons[idx], buffers[idx])
self._icons.append_array(loaded_icons)
self.load_finished.emit()
static func _load_texture(icon: Icon, buffer: String) -> void:
var img: Image = Image.new()
# scale texture to 128
var scale: float = 1.0
if icon.svg_size == Vector2.ZERO:
push_warning("icon %s has no size" % icon.icon_path)
else:
scale = Collection.TEXTURE_SIZE / max(icon.svg_size.x, icon.svg_size.y)
var success: int = img.load_svg_from_string(buffer, scale)
if success != OK:
push_warning("could not load '" + icon.icon_path + "'")
return
img.fix_alpha_edges()
icon.texture = ImageTexture.create_from_image(img)