extends SceneTree const PROP_CANVAS := preload("res://scripts/editors/prop/prop_canvas.gd") const PROP_ANCHORS := preload("res://scripts/props/prop_anchor_data.gd") const STAGE_GIZMOS := preload("res://scripts/stage/stage_gizmos.gd") const PROP_BLOCK := preload("res://scripts/props/prop_block.gd") const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") var failures: int = 0 var checks: int = 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var canvas: PropCanvas = PROP_CANVAS.new() root.add_child(canvas) canvas.set_shape_data([{"shape_type": "circle", "points": PackedVector2Array([Vector2(8, 4), Vector2(4, 8), Vector2(0, 4), Vector2(4, 0)]), "radius": 4.0, "closed": true, "vertex_flags": PackedInt32Array([0, 0, 0, 0]), "reshaped": true}]) var signal_state: Dictionary = {"count": 0} canvas.shape_changed.connect(func(_shapes: Array[Dictionary]) -> void: signal_state["count"] = int(signal_state["count"]) + 1) _check(canvas.can_scale_all_shapes(2.0), "valid document scale preflights") _check(canvas.scale_all_shapes(2.0), "valid document scale commits") var scaled: Dictionary = canvas.shapes[0] var points: PackedVector2Array = scaled["points"] _check(points[0].is_equal_approx(Vector2(16, 8)) and is_equal_approx(float(scaled["radius"]), 8.0) and bool(scaled["reshaped"]), "scale preserves circle points, radius, and reshaped flag") _check(int(signal_state["count"]) == 1, "document scale emits one shape change") var identity_result: bool = canvas.scale_all_shapes(1.0) _check(not identity_result, "identity scale emits no edit") var anchors: Dictionary = {"origin": {"role": "pivot", "position": Vector2(12, 4)}, "primary_grip": {"role": "grip", "position": Vector2(8, -3), "rotation_deg": 20.0, "z_index_offset": 2, "allowed_hands": "left_only"}} var anchor_result: Dictionary = PROP_ANCHORS.try_scale_non_origin(anchors, 2.0) var grip: Dictionary = anchor_result["anchors"]["primary_grip"] _check(bool(anchor_result["ok"]) and (grip["position"] as Vector2).is_equal_approx(Vector2(16, -6)) and (anchor_result["anchors"]["origin"] as Dictionary)["position"] == Vector2.ZERO, "anchor scale preserves metadata and canonical origin") _check(not bool(PROP_ANCHORS.try_scale_non_origin(anchors, -1.0)["ok"]), "invalid anchor scale is rejected") canvas.set_shape_data([{"shape_type": "polygon", "points": PackedVector2Array([Vector2(96, 48), Vector2(288, 48), Vector2(288, 96), Vector2(96, 96)]), "closed": true, "vertex_flags": PackedInt32Array([0, 0, 0, 0])}]) var normalization_factor: float = 96.0 / maxf(canvas.get_shapes_bounds().size.x, canvas.get_shapes_bounds().size.y) _check(canvas.scale_all_shapes(normalization_factor), "normalization scale commits") var normalized_bounds: Rect2 = canvas.get_shapes_bounds() _check(is_equal_approx(maxf(normalized_bounds.size.x, normalized_bounds.size.y), 96.0) and normalized_bounds.get_center().is_equal_approx(Vector2(96, 36)), "normalization reaches 96 px without centering off-origin geometry") canvas.set_shape_data([{"shape_type": "polygon", "points": PackedVector2Array([Vector2(10, 10), Vector2(30, 10), Vector2(30, 30), Vector2(10, 30)]), "closed": true, "vertex_flags": PackedInt32Array([0, 0, 0, 0])}]) canvas.set_anchor_data({"origin": {"role": "pivot", "position": Vector2.ZERO}, "primary_grip": {"role": "grip", "position": Vector2(8, 4)}}) var whole_resize: Dictionary = {"count": 0, "factor": 0.0} canvas.whole_prop_resize_requested.connect(func(factor: float) -> void: whole_resize["count"] = int(whole_resize["count"]) + 1; whole_resize["factor"] = factor) canvas.set_whole_prop_resize_enabled(true) canvas._begin_whole_prop_resize(Vector2(30, 30)) canvas._update_whole_prop_resize(Vector2(40, 40)) _check(canvas.shapes[0]["points"] is PackedVector2Array and (canvas.shapes[0]["points"] as PackedVector2Array)[2].is_equal_approx(Vector2(30, 30)), "whole-prop drag previews without mutating document geometry") canvas._end_whole_prop_resize() _check(int(whole_resize["count"]) == 1 and float(whole_resize["factor"]) > 1.0, "whole-prop drag requests one uniform commit on release") canvas.set_whole_prop_resize_enabled(true) canvas._begin_whole_prop_resize(Vector2(30, 30)) canvas._update_whole_prop_resize(Vector2(40, 40)) canvas._update_whole_prop_resize(Vector2(30, 30)) canvas._end_whole_prop_resize() _check(int(whole_resize["count"]) == 1, "whole-prop drag returning to start does not commit") canvas.set_whole_prop_resize_enabled(true) canvas._begin_whole_prop_resize(Vector2(30, 30)) canvas._update_whole_prop_resize(Vector2(40, 40)) canvas.cancel_whole_prop_resize() _check(not canvas.is_whole_prop_resize_dragging() and int(whole_resize["count"]) == 1, "whole-prop cancellation discards preview without commit") var whole_move: Dictionary = {"count": 0, "delta": Vector2.ZERO} var whole_rotate: Dictionary = {"count": 0, "angle": 0.0} canvas._zoom = 1.0 canvas.whole_prop_move_requested.connect(func(delta: Vector2) -> void: whole_move["count"] = int(whole_move["count"]) + 1; whole_move["delta"] = delta) canvas.whole_prop_rotate_requested.connect(func(angle: float) -> void: whole_rotate["count"] = int(whole_rotate["count"]) + 1; whole_rotate["angle"] = angle) canvas._whole_prop_dragging = true canvas._whole_prop_drag = PROP_CANVAS.WholePropDrag.MOVE canvas._whole_prop_delta = Vector2(11, 9) canvas._end_whole_prop_resize() _check(int(whole_move["count"]) == 1 and (whole_move["delta"] as Vector2).is_equal_approx(Vector2(11, 9)), "whole-prop body drag requests baseline move") canvas.set_snap_enabled(true) canvas.set_grid_size(15) canvas._whole_prop_dragging = true canvas._whole_prop_drag = PROP_CANVAS.WholePropDrag.MOVE canvas._whole_prop_start_pointer = Vector2.ZERO canvas._whole_prop_start_shapes = canvas.shapes.duplicate(true) canvas._whole_prop_start_anchors = canvas.get_anchor_data().duplicate(true) canvas._update_whole_prop_resize(Vector2(17, 13)) var moved_preview: PackedVector2Array = canvas._whole_prop_preview_shapes[0]["points"] var moved_anchor: Vector2 = (canvas._whole_prop_preview_anchors["primary_grip"] as Dictionary)["position"] _check(moved_preview[0].is_equal_approx(Vector2(25, 25)) and moved_anchor.is_equal_approx(Vector2(23, 19)) and (canvas.shapes[0]["points"] as PackedVector2Array)[0].is_equal_approx(Vector2(10, 10)), "whole-prop move previews snapped baseline delta without mutation") canvas._update_whole_prop_resize(Vector2.ZERO) canvas._end_whole_prop_resize() _check(int(whole_move["count"]) == 1, "whole-prop move returning to start does not commit") canvas.set_snap_enabled(false) canvas._begin_whole_prop_resize(Vector2(20, 56)) canvas._update_whole_prop_resize(Vector2(-56, 20)) canvas._end_whole_prop_resize() _check(int(whole_rotate["count"]) == 1 and absf(float(whole_rotate["angle"])) > 0.1, "whole-prop rotation dot requests wrapped rotation") canvas._whole_prop_dragging = true canvas._whole_prop_drag = PROP_CANVAS.WholePropDrag.ROTATE canvas._whole_prop_start_shapes = canvas.shapes.duplicate(true) canvas._whole_prop_start_anchors = canvas.get_anchor_data().duplicate(true) canvas._whole_prop_previous_angle = 3.13 canvas._whole_prop_total_angle = 0.0 canvas._update_whole_prop_resize(Vector2(cos(-3.13), sin(-3.13))) var wrapped_angle: float = canvas._whole_prop_preview_angle var rotated_anchor: Dictionary = canvas._whole_prop_preview_anchors["primary_grip"] _check(absf(wrapped_angle) < 0.1 and not (rotated_anchor["position"] as Vector2).is_equal_approx(Vector2(8, 4)) and not is_equal_approx(float(rotated_anchor["rotation_deg"]), 0.0), "whole-prop rotation wraps across pi and rotates anchor position and angle") canvas._update_whole_prop_resize(Vector2(cos(3.13), sin(3.13))) canvas._end_whole_prop_resize() _check(int(whole_rotate["count"]) == 1, "whole-prop rotation returning to start does not commit") canvas._whole_prop_dragging = true canvas._whole_prop_drag = PROP_CANVAS.WholePropDrag.ROTATE canvas._whole_prop_start_shapes = canvas.shapes.duplicate(true) canvas._whole_prop_start_anchors = canvas.get_anchor_data().duplicate(true) canvas._whole_prop_previous_angle = 0.0 canvas._whole_prop_total_angle = 0.0 canvas._update_whole_prop_resize(Vector2(cos(deg_to_rad(22.0)), sin(deg_to_rad(22.0))), true) var snapped_preview_angle: float = canvas._whole_prop_preview_angle canvas._end_whole_prop_resize() _check(is_equal_approx(snapped_preview_angle, deg_to_rad(15.0)) and is_equal_approx(float(whole_rotate["angle"]), deg_to_rad(15.0)) and int(whole_rotate["count"]) == 2, "Ctrl rotation commits the 15-degree preview angle") var prop: PropBlock = PROP_BLOCK.new() prop.polygon_points = PackedVector2Array([Vector2(-10, -10), Vector2(10, -10), Vector2(10, 10), Vector2(-10, 10)]) prop.freeze = true root.add_child(prop) await process_frame var gizmos: StageGizmos = STAGE_GIZMOS.new() root.add_child(gizmos) gizmos.set_targets([prop]) var bounds: Rect2 = STAGE_SELECTION.get_world_aabb(prop) var corner: Vector2 = bounds.end _check(gizmos.hit_test(corner) == STAGE_GIZMOS.Handle.SCALE, "single prop exposes scale corner") gizmos.begin_drag(corner) gizmos.end_drag() _check(not gizmos.is_dragging(), "stationary scale click ends without a persistent drag") var initial_scale: Vector2 = prop.scale gizmos.begin_drag(corner) gizmos.drag_to(prop.global_position + (corner - prop.global_position) * 2.0) gizmos.end_drag() _check(is_equal_approx(prop.scale.x / initial_scale.x, 2.0) and is_equal_approx(prop.scale.y / initial_scale.y, 2.0), "corner drag applies uniform scale") await physics_frame var space: PhysicsDirectSpaceState2D = root.get_world_2d().direct_space_state var polygon_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() polygon_inside.position = prop.global_position + Vector2(19.0, 0.0) var polygon_outside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() polygon_outside.position = prop.global_position + Vector2(21.0, 0.0) var polygon_inside_hits: Array[Dictionary] = space.intersect_point(polygon_inside) var polygon_outside_hits: Array[Dictionary] = space.intersect_point(polygon_outside) _check(not polygon_inside_hits.is_empty() and polygon_outside_hits.is_empty(), "scaled polygon collision matches its visual extent") prop.global_rotation = deg_to_rad(20.0) prop.begin_play_instance_scale() prop.freeze = false for frame: int in 3: await physics_frame var rotated_local_inside: Vector2 = Vector2(19.0, 0.0).rotated(prop.global_rotation) var rotated_local_outside: Vector2 = Vector2(21.0, 0.0).rotated(prop.global_rotation) polygon_inside.position = prop.global_position + rotated_local_inside polygon_outside.position = prop.global_position + rotated_local_outside _check(not space.intersect_point(polygon_inside).is_empty() and space.intersect_point(polygon_outside).is_empty(), "dynamic rotated polygon collision retains its scaled extent") var circle: PropBlock = PROP_BLOCK.new() circle.shape_type = PROP_BLOCK.ShapeType.CIRCLE circle.radius = 10.0 circle.global_position = Vector2(100.0, 100.0) circle.scale = Vector2(2.0, 2.0) circle.freeze = true root.add_child(circle) await physics_frame var circle_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() circle_inside.position = circle.global_position + Vector2(19.0, 0.0) var circle_outside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() circle_outside.position = circle.global_position + Vector2(21.0, 0.0) _check(not space.intersect_point(circle_inside).is_empty() and space.intersect_point(circle_outside).is_empty(), "scaled circle collision matches its visual extent") circle.begin_play_instance_scale() circle.freeze = false for frame: int in 3: await physics_frame circle_inside.position = circle.global_position + Vector2(19.0, 0.0) circle_outside.position = circle.global_position + Vector2(21.0, 0.0) _check(not space.intersect_point(circle_inside).is_empty() and space.intersect_point(circle_outside).is_empty(), "dynamic circle collision retains its scaled extent") circle.freeze = true circle.end_play_instance_scale() circle.scale = Vector2(2.0, 1.0) circle.begin_play_instance_scale() await physics_frame var ellipse_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() ellipse_inside.position = circle.global_position + Vector2(0.0, 9.0) var ellipse_outside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new() ellipse_outside.position = circle.global_position + Vector2(0.0, 11.0) _check(not space.intersect_point(ellipse_inside).is_empty() and space.intersect_point(ellipse_outside).is_empty(), "legacy non-uniform circle scale uses matching ellipse collision") circle.queue_free() prop.queue_free() gizmos.queue_free() canvas.queue_free() print("RESULT: %d checks, %d failures" % [checks, failures]) quit(1 if failures > 0 else 0) func _check(condition: bool, label: String) -> void: checks += 1 if condition: print("PASS: " + label) else: failures += 1 print("FAIL: " + label)