diff --git a/Tests/Unit/layered_portrait_coverage_test.gd b/Tests/Unit/layered_portrait_coverage_test.gd new file mode 100644 index 000000000..8dc00fc7f --- /dev/null +++ b/Tests/Unit/layered_portrait_coverage_test.gd @@ -0,0 +1,46 @@ +class_name LayeredPortraitCoverageTest +extends GdUnitTestSuite + + +func test_coverage_uses_local_transforms_before_entering_tree() -> void: + var portrait := CanvasGroup.new() + portrait.set_script(preload( + "res://addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd" + )) + # Reproduce the invalid preview transform reported after selecting an empty + # portrait. Coverage must depend only on transforms below the portrait root. + portrait.position = Vector2(INF, INF) + var layer := Node2D.new() + layer.position = Vector2(30, 20) + portrait.add_child(layer) + + var sprite := Sprite2D.new() + sprite.texture = ImageTexture.create_from_image( + Image.create(20, 10, false, Image.FORMAT_RGBA8) + ) + sprite.position = Vector2(5, 7) + layer.add_child(sprite) + + var coverage: Rect2 = portrait._find_largest_coverage_rect() + assert_bool(coverage.position.is_finite()).is_true() + assert_bool(coverage.size.is_finite()).is_true() + assert_vector(coverage.size).is_equal(Vector2(20, 10)) + portrait.free() + + +func test_coverage_includes_scaled_and_rotated_sprite_corners() -> void: + var portrait := CanvasGroup.new() + portrait.set_script(preload( + "res://addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd" + )) + var sprite := Sprite2D.new() + sprite.texture = ImageTexture.create_from_image( + Image.create(20, 10, false, Image.FORMAT_RGBA8) + ) + sprite.scale = Vector2(2, 1) + sprite.rotation = PI / 2.0 + portrait.add_child(sprite) + + var coverage: Rect2 = portrait._find_largest_coverage_rect() + assert_vector(coverage.size).is_equal_approx(Vector2(10, 40), Vector2(0.001, 0.001)) + portrait.free() diff --git a/addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd b/addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd index 58606fc7f..c96138708 100644 --- a/addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd +++ b/addons/dialogic/Modules/LayeredPortrait/layered_portrait.gd @@ -215,24 +215,34 @@ func _find_largest_coverage_rect() -> Rect2: return _cached_coverage_rect var coverage_rect := Rect2(0, 0, 0, 0) + var has_coverage := false for sprite: Sprite2D in _find_sprites_recursively(self): - var sprite_size := sprite.get_rect().size - var sprite_position: Vector2 = sprite.global_position-self.global_position - - if sprite.centered: - sprite_position -= sprite_size/2 - - var sprite_width := sprite_size.x * sprite.scale.x - var sprite_height := sprite_size.y * sprite.scale.y - - var texture_rect := Rect2( - sprite_position.x, - sprite_position.y, - sprite_width, - sprite_height + # Global transforms can be invalid for tool nodes while the editor is + # rebuilding a portrait preview. Build the transform from local values so + # coverage stays finite even before the nodes enter the scene tree. + var relative_transform := sprite.transform + var parent := sprite.get_parent() + while parent != self: + if parent is Node2D: + relative_transform = parent.transform * relative_transform + parent = parent.get_parent() + + var sprite_rect := sprite.get_rect() + var first_corner := relative_transform * sprite_rect.position + var texture_rect := Rect2(first_corner, Vector2.ZERO) + texture_rect = texture_rect.expand(relative_transform * sprite_rect.end) + texture_rect = texture_rect.expand( + relative_transform * Vector2(sprite_rect.end.x, sprite_rect.position.y) + ) + texture_rect = texture_rect.expand( + relative_transform * Vector2(sprite_rect.position.x, sprite_rect.end.y) ) - coverage_rect = coverage_rect.merge(texture_rect) + if has_coverage: + coverage_rect = coverage_rect.merge(texture_rect) + else: + coverage_rect = texture_rect + has_coverage = true coverage_rect.position = _reposition_with_rect(coverage_rect)