Skip to content

Commit b0e57fa

Browse files
committed
Enable loading image textures on demand to support thousands of files
1 parent 362254c commit b0e57fa

6 files changed

Lines changed: 173 additions & 50 deletions

File tree

MainApplication.tscn

Lines changed: 13 additions & 42 deletions
Large diffs are not rendered by default.

components/PreviewContainer.tscn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kx105"]
88
resource_local_to_scene = true
9+
content_margin_left = 2.0
10+
content_margin_right = 2.0
911
bg_color = Color(0.109804, 0.109804, 0.109804, 1)
1012
border_width_left = 1
1113
border_width_top = 1

scripts/Application.gd

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ class ImageThreadWorker extends Resource:
150150

151151

152152
func load_image_texture(path: String) -> ImageTexture:
153-
154-
var loaded_image := Image.load_from_file(path)
155-
156-
if loaded_image:
157-
return ImageTexture.create_from_image(loaded_image)
158153

159-
return null
154+
var loaded_image := Image.new()
155+
var error := loaded_image.load(path)
156+
157+
if error != OK:
158+
return null
159+
160+
return ImageTexture.create_from_image(loaded_image)

scripts/ImageScrollManager.gd

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
extends ScrollContainer
2+
3+
const image_container_scene := preload("res://components/PreviewContainer.tscn")
4+
5+
@onready
6+
var container := %ImagePreviews
7+
8+
var preview_list : Array[PreviewContainer] = []
9+
var old_visible_items_start = -1
10+
var old_visible_items_end = -1
11+
12+
var visible_items: int = -1
13+
var item_width: int = -1
14+
@export
15+
var loading_buffer: int = 32
16+
var old_scroll_position: int = -1
17+
18+
var image_update_timer: Timer
19+
20+
var filling_images : bool = false
21+
22+
23+
# Called when the node enters the scene tree for the first time.
24+
func _ready() -> void:
25+
26+
Application.pre_clear.connect(clear_images)
27+
Application.new_files_loaded.connect(create_images)
28+
29+
resized.connect(func(): old_scroll_position = -1)
30+
31+
image_update_timer = Timer.new()
32+
image_update_timer.wait_time = 0.25
33+
add_child(image_update_timer)
34+
image_update_timer.timeout.connect(load_and_unload_images)
35+
image_update_timer.start()
36+
37+
func load_and_unload_images():
38+
39+
if preview_list.size() == 0:
40+
return
41+
42+
item_width = preview_list[0].size.x
43+
visible_items = ceil(size.x / item_width)
44+
45+
# We need to check every frame if the shown items changed
46+
# This is because we cant passively listen to changes
47+
var current_scroll_item: int = floor(scroll_horizontal / item_width)
48+
49+
50+
51+
# Check if our item index would change considering shown items
52+
if current_scroll_item != old_scroll_position:
53+
54+
# Load and unload all images which we need
55+
56+
# First determine which indexes should be visible
57+
var start_visible := current_scroll_item - loading_buffer
58+
59+
if start_visible < 0:
60+
start_visible = 0
61+
62+
var end_visible := current_scroll_item + visible_items + loading_buffer
63+
if end_visible >= preview_list.size():
64+
end_visible = preview_list.size() - 1
65+
66+
# Check that we actually have items
67+
if end_visible - start_visible <= 0:
68+
return
69+
70+
# Unload the items which were visible previously
71+
for i in range(old_visible_items_start, old_visible_items_end + 1):
72+
73+
74+
# Check that we are not in the region of the new visibility
75+
if i <= end_visible and i >= start_visible:
76+
continue
77+
78+
# print("Unloading Image - " + str(i))
79+
80+
# Unload the image at this offset
81+
preview_list[i].unload_image()
82+
83+
# Now start loading the images in the visible section
84+
85+
for i in range(start_visible, end_visible + 1):
86+
87+
# Check that we are not in the region of the old visibility
88+
if i <= old_visible_items_end and i >= old_visible_items_start:
89+
continue
90+
91+
# print("Loading Image - " + str(i))
92+
preview_list[i].load_image()
93+
94+
95+
96+
old_visible_items_start = start_visible
97+
old_visible_items_end = end_visible
98+
99+
old_scroll_position = current_scroll_item
100+
101+
102+
func clear_images() -> void:
103+
filling_images = false
104+
for child in container.get_children():
105+
child.queue_free()
106+
preview_list.clear()
107+
108+
# Creating all objects in a single frame can be too much, depending on the amount of data loaded
109+
# So we move this to process actually
110+
func create_images() -> void:
111+
filling_images = true
112+
113+
func _process(delta: float) -> void:
114+
if filling_images:
115+
116+
# Iterate the next 500 images we want to add
117+
118+
for i in range(preview_list.size(), min(Application.currently_loaded_files.size(), preview_list.size() + 500)):
119+
var image : ProcessedImage = Application.currently_loaded_files[i]
120+
121+
var child : PreviewContainer = image_container_scene.instantiate()
122+
123+
child.image_reference = image
124+
125+
container.add_child(child)
126+
127+
preview_list.append(child)
128+
129+
if preview_list.size() == Application.currently_loaded_files.size():
130+
filling_images = false

scripts/PreviewContainer.gd

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ var label: Label = %Label
1212
@onready
1313
var texture : TextureRect = %Texture
1414

15+
var is_active: bool = false
16+
1517
# Called when the node enters the scene tree for the first time.
1618
func _ready() -> void:
1719

@@ -23,8 +25,6 @@ func _ready() -> void:
2325

2426
# Set the context of the Label to our image_reference index
2527
update_label()
26-
# Update image
27-
load_image()
2828

2929
resized.connect(update_label)
3030

@@ -45,6 +45,9 @@ func set_texture() -> void:
4545
if image_reference == null:
4646
return
4747

48+
if image_reference.was_already_exported:
49+
pass
50+
4851
if image_reference.preview_texture != null:
4952
texture.texture = image_reference.preview_texture
5053

@@ -60,3 +63,12 @@ func load_image() -> void:
6063
return
6164

6265
image_reference.load_image()
66+
67+
func unload_image() -> void:
68+
69+
texture.texture = preload("res://assets/icons/Image.svg")
70+
71+
if image_reference == null:
72+
return
73+
74+
image_reference.unload_image()

scripts/ProcessedImage.gd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func _init(index: int, source_path: String) -> void:
2323

2424
func load_image() -> void:
2525

26+
if original_texture != null and preview_texture != null:
27+
return
28+
2629
if is_loading_image:
2730
return
2831

@@ -39,12 +42,16 @@ func load_image() -> void:
3942

4043
func unload_image() -> bool:
4144

45+
original_texture = null
46+
preview_texture = null
47+
4248
was_unloaded.emit()
4349

4450
return false
4551

4652
func export_image(position: Rect2) -> bool:
4753

54+
was_already_exported = true
4855
was_exported.emit()
4956
return false
5057

0 commit comments

Comments
 (0)