Phase 14 - Organized directories.

This commit is contained in:
2026-10-03 19:13:39 -04:00
parent 6666173c4f
commit d950fd9d9a
307 changed files with 1452 additions and 1110 deletions
+127
View File
@@ -0,0 +1,127 @@
class_name PropThumbnail
extends Node
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
const PROP_VISUALS := preload("res://scripts/props/prop_visuals.gd")
const SIZE := Vector2i(200, 200)
var _viewport: SubViewport
var _world: Node2D
var _camera: Camera2D
func _ready() -> void:
_viewport = SubViewport.new()
_viewport.size = SIZE
_viewport.transparent_bg = true
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
add_child(_viewport)
_world = Node2D.new()
_viewport.add_child(_world)
_camera = Camera2D.new()
_camera.enabled = true
_viewport.add_child(_camera)
_camera.make_current()
func render(payload: Dictionary, material_preset: int) -> Texture2D:
_clear_world()
var points := _compute_points(payload)
var fill := Color(payload.get("fill_color", Color.WHITE))
var outline := Color(payload.get("outline_color", Color.BLACK))
if material_preset != PROP_BLOCK.MaterialPreset.NONE:
var tint := PROP_BLOCK.tint_for(material_preset)
fill = tint
outline = tint.darkened(0.55)
var outline_width := float(payload.get("outline_width", 2.0))
var visual := Node2D.new()
var poly := Polygon2D.new()
poly.polygon = points
poly.color = fill
var line := Line2D.new()
var loop := points.duplicate()
if not loop.is_empty():
loop.append(points[0])
line.points = loop
line.default_color = outline
line.width = outline_width
line.joint_mode = Line2D.LINE_JOINT_ROUND
line.begin_cap_mode = Line2D.LINE_CAP_ROUND
line.end_cap_mode = Line2D.LINE_CAP_ROUND
visual.add_child(poly)
visual.add_child(line)
_world.add_child(visual)
_frame_camera(_points_bbox(points))
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var tex := _viewport.get_texture()
visual.queue_free()
if tex == null:
return null
var img := tex.get_image()
if _is_blank(img):
return null
return ImageTexture.create_from_image(img)
func render_shapes(shapes: Array[Dictionary]) -> Texture2D:
_clear_world()
var visual := Node2D.new()
_world.add_child(visual)
var bounds: Rect2 = PROP_VISUALS.build(visual, shapes)
_frame_camera(bounds if bounds.size.length_squared() > 0.0 else Rect2(Vector2(-24, -24), Vector2(48, 48)))
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var tex := _viewport.get_texture()
visual.queue_free()
if tex == null or _is_blank(tex.get_image()): return null
return ImageTexture.create_from_image(tex.get_image())
func _compute_points(payload: Dictionary) -> PackedVector2Array:
var shape_type: int = int(payload.get("type", PROP_BLOCK.ShapeType.POLYGON))
if shape_type == PROP_BLOCK.ShapeType.CIRCLE:
var radius := float(payload.get("radius", 24.0))
var loop := PackedVector2Array()
for i: int in PROP_BLOCK.CIRCLE_SEGMENTS:
var angle: float = TAU * float(i) / float(PROP_BLOCK.CIRCLE_SEGMENTS)
loop.append(Vector2(cos(angle), sin(angle)) * radius)
return loop
return payload.get("points", PackedVector2Array())
func _points_bbox(points: PackedVector2Array) -> Rect2:
if points.is_empty():
return Rect2(Vector2(-24, -24), Vector2(48, 48))
var rect := Rect2(points[0], Vector2.ZERO)
for p: Vector2 in points:
rect = rect.expand(p)
if rect.size.x < 1.0 or rect.size.y < 1.0:
return Rect2(Vector2(-24, -24), Vector2(48, 48))
return rect
func _clear_world() -> void:
for child: Node in _world.get_children():
child.queue_free()
func _frame_camera(bbox: Rect2) -> void:
var margin := 12.0
var fit := minf((SIZE.x - margin * 2.0) / bbox.size.x, (SIZE.y - margin * 2.0) / bbox.size.y)
_camera.position = bbox.get_center()
_camera.zoom = Vector2(maxf(fit, 0.05), maxf(fit, 0.05))
func _is_blank(img: Image) -> bool:
if img == null or img.is_empty():
return true
var used := img.get_used_rect()
if used.size.x <= 0 or used.size.y <= 0:
return true
for y: int in range(used.position.y, used.end.y):
for x: int in range(used.position.x, used.end.x):
if img.get_pixel(x, y).a > 0.0:
return false
return true
@@ -0,0 +1 @@
uid://baau4hydvu46y
@@ -0,0 +1,73 @@
class_name StickmanThumbnail
extends Node
const STICKMAN_FACTORY := preload("res://scripts/characters/stickman/stickman_factory.gd")
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
const SIZE := Vector2i(200, 200)
var _viewport: SubViewport
var _world: Node2D
var _camera: Camera2D
func _ready() -> void:
_viewport = SubViewport.new()
_viewport.size = SIZE
_viewport.transparent_bg = true
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
add_child(_viewport)
_world = Node2D.new()
_viewport.add_child(_world)
_camera = Camera2D.new()
_camera.enabled = true
_viewport.add_child(_camera)
_camera.make_current()
func render(stk_data: Dictionary) -> Texture2D:
_clear_world()
var rig: Node2D = STICKMAN_FACTORY.spawn_from_data(stk_data)
if rig == null:
return null
_world.add_child(rig)
rig.position = Vector2.ZERO
var bbox := STAGE_SPAWNER.get_world_aabb(rig)
if not bbox.has_area() or bbox.size.x < 1.0 or bbox.size.y < 1.0:
bbox = Rect2(Vector2(-60, -500), Vector2(120, 500))
_frame_camera(bbox)
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var tex := _viewport.get_texture()
rig.queue_free()
if tex == null:
return null
var img := tex.get_image()
if _is_blank(img):
return null
return ImageTexture.create_from_image(img)
func _clear_world() -> void:
for child: Node in _world.get_children():
child.queue_free()
func _frame_camera(bbox: Rect2) -> void:
var margin := 12.0
var fit := minf((SIZE.x - margin * 2.0) / bbox.size.x, (SIZE.y - margin * 2.0) / bbox.size.y)
_camera.position = bbox.get_center()
_camera.zoom = Vector2(maxf(fit, 0.05), maxf(fit, 0.05))
func _is_blank(img: Image) -> bool:
if img == null or img.is_empty():
return true
var used := img.get_used_rect()
if used.size.x <= 0 or used.size.y <= 0:
return true
for y: int in range(used.position.y, used.end.y):
for x: int in range(used.position.x, used.end.x):
if img.get_pixel(x, y).a > 0.0:
return false
return true
@@ -0,0 +1 @@
uid://5b0eubudwgua
@@ -0,0 +1,94 @@
class_name ThumbnailCache
extends RefCounted
const STICKMEN_DIR := "user://thumbnails/stickmen"
const PROP_DIR := "user://thumbnails/props"
const PROP_VERSION := 1
var _memory: Dictionary = {}
func get_texture(cache_path: String) -> Texture2D:
if _memory.has(cache_path):
return _memory[cache_path] as Texture2D
var texture: Texture2D = load_png(cache_path)
if texture != null:
_memory[cache_path] = texture
return texture
func store_texture(cache_path: String, texture: Texture2D) -> Error:
if texture == null:
return ERR_INVALID_PARAMETER
_memory[cache_path] = texture
return save_png(texture, cache_path)
func forget_memory(cache_path: String) -> void:
_memory.erase(cache_path)
func clear_memory() -> void:
_memory.clear()
func stickman_key(path: String) -> String:
return "%s_%d" % [path.get_file().get_basename(), FileAccess.get_modified_time(path)]
func stickman_png(key: String) -> String:
return STICKMEN_DIR + "/" + key + ".png"
func prop_png(id: String) -> String:
return PROP_DIR + "/" + id + "_v" + str(PROP_VERSION) + ".png"
func prop_file_key(path: String) -> String:
var canonical: String = path.simplify_path()
var digest: String = canonical.sha256_text().substr(0, 12)
return "file_%s_%d" % [digest, FileAccess.get_modified_time(path)]
func prop_file_png(path: String) -> String:
return PROP_DIR + "/" + prop_file_key(path) + ".png"
func load_png(png_path: String) -> Texture2D:
if _memory.has(png_path):
return _memory[png_path] as Texture2D
if not FileAccess.file_exists(png_path):
return null
var img := Image.load_from_file(png_path)
if img == null:
return null
var texture: Texture2D = ImageTexture.create_from_image(img)
_memory[png_path] = texture
return texture
func save_png(tex: Texture2D, png_path: String) -> Error:
if tex == null:
return ERR_INVALID_PARAMETER
var img := tex.get_image()
if img == null:
return ERR_CANT_CREATE
ensure_dir(png_path.get_base_dir())
_memory[png_path] = tex
return img.save_png(png_path)
func ensure_dir(dir: String) -> void:
DirAccess.make_dir_recursive_absolute(dir)
func clean_stale_stickmen(valid_keys: Dictionary) -> void:
if not DirAccess.dir_exists_absolute(STICKMEN_DIR):
return
var dir := DirAccess.open(STICKMEN_DIR)
if dir == null:
return
dir.list_dir_begin()
var fname := dir.get_next()
while fname != "":
if not dir.current_is_dir() and fname.ends_with(".png"):
var key := fname.trim_suffix(".png")
if not valid_keys.has(key):
DirAccess.remove_absolute(STICKMEN_DIR + "/" + fname)
fname = dir.get_next()
dir.list_dir_end()
@@ -0,0 +1 @@
uid://ceffhet5bsvy3