Files
nodewars/addons/curved_lines_2d/svg_texture_helper.gd
2026-05-13 18:52:00 +02:00

294 lines
9.6 KiB
GDScript

@tool
extends Node
class_name SVGTextureHelper
# May use a class name if clutter is not a concern
# Map property names → default values
const PROPERTY_MAPPINGS: Dictionary = {
"expand_mode": TextureRect.EXPAND_IGNORE_SIZE,
"expand_icon": true,
"ignore_texture_size": true
}
@export var svg_resource := SVGResource.new(): set = _set_svg_resource
@export var target_property: String = "" : set = _set_target_property
var _available_texture_properties: Array[String] = []
var _is_saving_scene: bool = false
var _cached_texture: Texture2D # Store the actual texture to restore after save
func _ready() -> void:
# Ensure the parent is a Control node.
if not get_parent() is Control:
push_error("SVGTextureHelper must be a child of a Control node.")
queue_free()
return
# Detect available texture properties
_detect_texture_properties()
# Auto-select first available property if none is set
if target_property.is_empty() and not _available_texture_properties.is_empty():
target_property = _available_texture_properties[0]
# Connect to the parent's resize signal to trigger re-renders.
get_parent().resized.connect(_queue_render)
# Change parent texture properties
_update_parent_properties()
# Connect to save signals if in editor
if Engine.is_editor_hint():
_connect_save_signals()
# Perform initial render if we have a resource.
if svg_resource:
_queue_render()
func _connect_save_signals() -> void:
if Engine.is_editor_hint():
if get_tree():
get_tree().node_configuration_warning_changed.connect(_on_scene_tree_changed)
func _on_editor_visibility_changed() -> void:
# This is a heuristic that often correlates with save operations
if Engine.is_editor_hint():
_prepare_for_potential_save()
func _on_scene_tree_changed(node: Node) -> void:
# Another heuristic for detecting editor operations that might include saving
if Engine.is_editor_hint() and is_ancestor_of(node):
_prepare_for_potential_save()
# Override the notification method to catch save notifications
func _notification(what: int) -> void:
match what:
NOTIFICATION_EDITOR_PRE_SAVE:
_prepare_for_save()
NOTIFICATION_EDITOR_POST_SAVE:
_restore_after_save()
# Also catch when the node is about to be saved
NOTIFICATION_WM_CLOSE_REQUEST:
if Engine.is_editor_hint():
_prepare_for_save()
func _prepare_for_potential_save() -> void:
# Use a timer to briefly set texture to null, then restore
# This catches most save scenarios
if _is_saving_scene:
return
_prepare_for_save()
# Restore after a brief delay
get_tree().create_timer(0.1).timeout.connect(_restore_after_save, CONNECT_ONE_SHOT)
func _prepare_for_save() -> void:
if not Engine.is_editor_hint() or not get_parent() or target_property.is_empty():
return
if not _has_property(get_parent(), target_property):
return
_is_saving_scene = true
# Store the current texture
_cached_texture = get_parent().get(target_property) as Texture2D
# Set texture to null to prevent it from being saved
get_parent().set(target_property, null)
print_rich("[color=yellow]SVGTextureHelper: Texture temporarily set to null for saving[/color]")
func _restore_after_save() -> void:
if not Engine.is_editor_hint() or not _is_saving_scene:
return
_is_saving_scene = false
# Restore the cached texture
if get_parent() and not target_property.is_empty() and _cached_texture:
if _has_property(get_parent(), target_property):
get_parent().set(target_property, _cached_texture)
print_rich("[color=green]SVGTextureHelper: Texture restored after save[/color]")
# Clear the cache
_cached_texture = null
func _detect_texture_properties() -> void:
_available_texture_properties.clear()
if not get_parent():
return
# Get all properties from the parent control
var property_list = get_parent().get_property_list()
for prop_info in property_list:
var prop_name: String = prop_info.name
var prop_type = prop_info.type
var prop_class_name = prop_info.class_name
# Check if this property accepts a Texture2D
# This covers properties with type TYPE_OBJECT and class_name "Texture2D"
# or properties that are explicitly documented as texture properties
if (prop_type == TYPE_OBJECT and
(prop_class_name == "Texture2D" or
prop_class_name == "Texture" or
prop_class_name == "ImageTexture" or
prop_class_name == "CompressedTexture2D")):
_available_texture_properties.append(prop_name)
# Also check for commonly named texture properties
elif (prop_name.to_lower().contains("texture") or
prop_name.to_lower().contains("icon") or
prop_name in ["normal", "pressed", "hover", "disabled", "focused"]):
# Verify it can actually accept a texture by checking if it's an Object type
if prop_type == TYPE_OBJECT:
_available_texture_properties.append(prop_name)
# Sort alphabetically for better UX
_available_texture_properties.sort()
# Ensure we have at least some common fallbacks
if _available_texture_properties.is_empty():
# Add common texture property names as fallbacks
var common_properties = ["texture", "icon", "normal", "pressed"]
for prop in common_properties:
if _has_property(get_parent(), prop):
_available_texture_properties.append(prop)
func _set_target_property(new_property: String) -> void:
if target_property == new_property:
return
target_property = new_property
# Re-apply texture if we have one
if svg_resource and svg_resource.texture and get_parent():
_on_texture_updated(svg_resource.texture)
func _update_parent_properties() -> void:
if get_parent() == null:
return
for prop in PROPERTY_MAPPINGS.keys():
if _has_property(get_parent(), prop):
get_parent().set(prop, PROPERTY_MAPPINGS[prop])
func _has_property(target: Object, prop_name: StringName) -> bool:
for info in target.get_property_list():
if info.name == prop_name:
return true
return false
func _set_svg_resource(new_resource: SVGResource) -> void:
if svg_resource == new_resource:
return
# Disconnect from old resource if it exists
if svg_resource:
if svg_resource.is_connected("texture_updated", _on_texture_updated):
svg_resource.texture_updated.disconnect(_on_texture_updated)
# Disconnect from the changed signal
if svg_resource.is_connected("changed", _on_resource_changed):
svg_resource.changed.disconnect(_on_resource_changed)
svg_resource = new_resource
if svg_resource:
svg_resource.texture_updated.connect(_on_texture_updated)
# CONNECT TO THE CHANGED SIGNAL
svg_resource.changed.connect(_on_resource_changed)
# If the resource already has a texture, apply it immediately.
if svg_resource.texture:
_on_texture_updated(svg_resource.texture)
# Queue a render to ensure it's the correct size.
_queue_render()
# Handles resource changes
func _on_resource_changed() -> void:
# This gets called when svg_file_path or render_scale changes
_queue_render()
func _on_texture_updated(new_texture: Texture2D) -> void:
if get_parent() and not target_property.is_empty():
# Don't update texture if we're in the middle of a save operation
if _is_saving_scene:
_cached_texture = new_texture # Update our cache instead
return
# Validate that the property exists before setting it
if _has_property(get_parent(), target_property):
# Set the new texture on the parent control
get_parent().set_deferred(target_property, new_texture)
else:
push_warning("Property '%s' not found on parent node '%s'" % [target_property, get_parent().name])
## Called on resize or when the resource changes.
func _queue_render() -> void:
if svg_resource == null or svg_resource.svg_string.is_empty():
get_parent().set(target_property, null)
return
# Don't render during save operations
if _is_saving_scene:
return
# Check for valid conditions before requesting a render.
if not is_instance_valid(svg_resource) or not is_instance_valid(get_parent()):
return
if Engine.is_editor_hint() or get_tree():
var target_size: Vector2i = get_parent().size
var rescale_factor := 1.0
# If parent size is invalid or zero, use a minimal 1x1 placeholder.
# This prevents rendering a huge texture during initialization in the editor.
# The 'resized' signal will trigger a correct render once the size is calculated.
if target_size.x <= 0 or target_size.y <= 0:
target_size = Vector2i(1, 1)
var stretch_mode : TextureRect.StretchMode = get_parent().stretch_mode if "stretch_mode" in get_parent() else TextureRect.STRETCH_SCALE
if (
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP or
stretch_mode == TextureRect.StretchMode.STRETCH_TILE or
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP_CENTERED
):
rescale_factor = 1.0
else:
rescale_factor = (target_size.x / svg_resource.original_size.x) * svg_resource.render_scale
var image := Image.new()
var error := image.load_svg_from_string(Marshalls.base64_to_utf8(svg_resource.svg_string), rescale_factor)
if error != OK:
push_error("Failed to render SVG: " + str(error))
return
var image_texture := ImageTexture.create_from_image(image)
if (
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP or
stretch_mode == TextureRect.StretchMode.STRETCH_TILE or
stretch_mode == TextureRect.StretchMode.STRETCH_KEEP_CENTERED
):
image_texture.set_size_override(svg_resource.original_size)
else:
get_parent().set(target_property, image_texture)
# Helper function to refresh the property list in the editor
func _refresh_properties() -> void:
if Engine.is_editor_hint():
_detect_texture_properties()
notify_property_list_changed()
# Manual save preparation - you can call this from editor plugins or scripts
func manual_prepare_for_save() -> void:
_prepare_for_save()
# Manual save restoration - you can call this from editor plugins or scripts
func manual_restore_after_save() -> void:
_restore_after_save()