extends SceneTree const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd") const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") var _failures: int = 0 func _initialize() -> void: call_deferred("_run_preview") func _run_preview() -> void: root.size = Vector2i(1280, 720) root.gui_embed_subwindows = true var stage: SandboxStage = SANDBOX_STAGE.instantiate() as SandboxStage root.add_child(stage) await _wait_frames(10) # Keep any existing user recovery copy intact while removing its modal from # this visual-only preview. No recovery action or document edit is performed. var recovery_dialog: ConfirmationDialog = stage.find_child("AutosaveRecoveryDialog", true, false) as ConfirmationDialog if recovery_dialog != null: STAGE_DIALOG_STYLER.hide(recovery_dialog) await _wait_frames(2) var spawner: StageSpawner = stage.get("_spawner") as StageSpawner var first: StickmanRig = spawner.spawn_stickman_asset("res://content/stickmen/break.stk", Vector2(-18.0, 0.0)) var second: StickmanRig = spawner.spawn_stickman_asset("res://content/stickmen/break.stk", Vector2(18.0, 0.0)) _check(first != null and second != null, "overlapping character fixtures are available") if first == null or second == null: quit(1) return first.stage_name = "Jimmy" second.stage_name = "Timmy" var crate: PropBlock = spawner.spawn_prop_asset("crate", Vector2(170.0, 190.0)) _check(crate != null, "prop inspector fixture is available") if crate == null: quit(1) return crate.stage_name = "Blue Door" var selection: StageSelection = stage.get("_selection") as StageSelection selection.select_only(crate) await _wait_frames(4) var views: StageModalViews = stage.get("_modal_views") as StageModalViews var ui_layer: CanvasLayer = stage.get_node("UI") as CanvasLayer _check(views != null and ui_layer != null, "stage popup UI is available") if views == null or ui_layer == null: quit(1) return var avoid_rects: Array[Rect2] = [ Rect2(Vector2.ZERO, Vector2(1280.0, 72.0)), Rect2(Vector2(0.0, 682.0), Vector2(1280.0, 38.0)), Rect2(Vector2.ZERO, Vector2(120.0, 720.0)), ] var prop_view: StagePropSettingsView = stage.get("_prop_settings_view") as StagePropSettingsView if prop_view != null and prop_view.visible: var prop_card: Control = prop_view.get_card() if prop_card != null: avoid_rects.append(prop_card.get_global_rect().grow(8.0)) for character: StickmanRig in [first, second]: var world_bounds: Rect2 = STAGE_SELECTION.get_world_aabb(character) if world_bounds.has_area(): avoid_rects.append(stage.call("_world_rect_to_viewport_rect", world_bounds).grow(8.0)) var click_position: Vector2 = stage.get_viewport().get_canvas_transform() * Vector2.ZERO views.show_objects_here(ui_layer, [first, second], selection.get_selected().duplicate(), Vector2(1280.0, 720.0), click_position, avoid_rects) await _wait_frames(8) var popup: PopupPanel = views.get_objects_here_popup() _check(popup != null and popup.visible, "anchored chooser is visible") if popup != null: var candidate_button: Button = popup.find_child("ObjectCandidate_0", true, false) as Button if candidate_button != null: candidate_button.grab_focus() var gizmos: StageGizmos = stage.get("_gizmos") as StageGizmos avoid_rects.append(views.get_objects_here_screen_rect().grow(8.0)) if gizmos != null: gizmos.set_candidate_preview_obstacles(avoid_rects) gizmos.set_enabled(true) gizmos.set_candidate_preview(first) await _wait_frames(4) if gizmos != null: var name_pill_style: StyleBoxFlat = gizmos.get("_candidate_preview_name_style") as StyleBoxFlat _check(name_pill_style != null and name_pill_style.corner_radius_top_left >= 8, "candidate name label uses rounded pill corners") _check(name_pill_style != null and name_pill_style.border_width_left >= 3, "candidate name pill keeps a clear cyan outline") var shell: StyleBoxFlat = popup.get_theme_stylebox("panel") as StyleBoxFlat _check(shell != null and shell.corner_radius_top_left >= 12, "chooser shell has rounded corners") _check(shell != null and shell.border_width_left >= 2 and shell.shadow_size > 0, "chooser shell has a border and local shadow") var cancel: Button = popup.find_child("CancelObjectsHere", true, false) as Button var cancel_style: StyleBoxFlat = cancel.get_theme_stylebox("normal") as StyleBoxFlat if cancel != null else null _check(cancel != null and cancel.custom_minimum_size.y >= 48, "Cancel has a large target") _check(cancel_style != null and cancel_style.corner_radius_top_left >= 8, "Cancel has rounded secondary styling") await RenderingServer.frame_post_draw var capture: Image = root.get_texture().get_image() var capture_names: Array[String] = [ "overlap_chooser_popup_style", "character_overlap_preview", "overlap_hover_preview", "overlap_candidate_preview", ] for capture_name: String in capture_names: var capture_path: String = "res://tests/phase20a_captures/%s.png" % capture_name var capture_error: Error = capture.save_png(ProjectSettings.globalize_path(capture_path)) print("PHASE20A_CAPTURE path=%s size=%s error=%d" % [capture_path, capture.get_size(), capture_error]) _check(capture_error == OK, "capture %s is saved" % capture_name) await _verify_overlap_gestures(stage, crate, views, selection) await _wait_frames(2) stage.queue_free() await _wait_frames(2) quit(1 if _failures > 0 else 0) func _verify_overlap_gestures(stage: SandboxStage, selected_prop: PropBlock, views: StageModalViews, selection: StageSelection) -> void: var spawner: StageSpawner = stage.get("_spawner") as StageSpawner var overlapping_prop: PropBlock = spawner.spawn_prop_asset("crate", selected_prop.global_position) as PropBlock _check(overlapping_prop != null, "overlap gesture fixture is available") if overlapping_prop == null: return stage.call("_finalize_placed_node", overlapping_prop) await _wait_frames(4) var gizmos: StageGizmos = stage.get("_gizmos") as StageGizmos var canvas: Transform2D = stage.get_viewport().get_canvas_transform() var click_world: Vector2 = selected_prop.global_position var click_screen: Vector2 = canvas * click_world var candidates: Array[Node2D] = selection.hit_test_all(click_world) _check(candidates.has(selected_prop) and candidates.has(overlapping_prop), "gesture fixture has two visible objects at the click point") selection.select_only(selected_prop) await _wait_frames(2) if gizmos != null: gizmos.set_snap_size(0.0) await _send_mouse_motion(click_screen) await _send_mouse_button(click_screen, true) _check(stage.get("_objects_here_popup") == null and not gizmos.is_dragging(), "overlap press waits without opening chooser or moving") await _send_mouse_motion(click_screen + Vector2(4.0, 0.0), MOUSE_BUTTON_MASK_LEFT) _check(stage.get("_objects_here_popup") == null and not gizmos.is_dragging(), "sub-threshold movement remains a click candidate") await _send_mouse_button(click_screen + Vector2(4.0, 0.0), false) var click_popup: PopupPanel = stage.get("_objects_here_popup") as PopupPanel _check(click_popup != null and click_popup.visible, "release before the threshold opens the overlap chooser") _check(selection.is_selected(selected_prop), "opening the chooser preserves the prior selection") views.dismiss_objects_here() await _wait_frames(2) var start_position: Vector2 = selected_prop.global_position var start_screen: Vector2 = canvas * start_position var end_screen: Vector2 = start_screen + Vector2(24.0, 12.0) var expected_position: Vector2 = canvas.affine_inverse() * end_screen await _send_mouse_motion(start_screen) await _send_mouse_button(start_screen, true) await _send_mouse_motion(end_screen, MOUSE_BUTTON_MASK_LEFT) _check(stage.get("_objects_here_popup") == null, "drag past the threshold never opens the overlap chooser") _check(gizmos.is_dragging(), "drag past the threshold begins moving the selected overlap") _check(selected_prop.global_position.is_equal_approx(expected_position), "selected object follows the overlap drag") _check(overlapping_prop.global_position.is_equal_approx(start_position), "unselected overlapping object stays in place") await _send_mouse_button(end_screen, false) _check(stage.get("_objects_here_popup") == null, "drag release does not reopen the chooser") _check(not gizmos.is_dragging(), "drag release finishes the selected-object move") # Verify rotate and resize handles remain ahead of overlap selection. selected_prop.global_position = start_position overlapping_prop.global_position = start_position selection.select_only(selected_prop) await _wait_frames(2) var prop_bounds: Rect2 = STAGE_SELECTION.get_world_aabb(selected_prop) var scale_handle: Vector2 = Vector2(prop_bounds.end.x, prop_bounds.position.y) var scale_screen: Vector2 = canvas * scale_handle await _send_mouse_motion(scale_screen) await _send_mouse_button(scale_screen, true) _check(gizmos.is_dragging() and gizmos._dragging == StageGizmos.Handle.SCALE, "overlapped resize handle starts scaling immediately") _check(stage.get("_objects_here_popup") == null, "resize handle does not open the overlap chooser") await _send_mouse_button(scale_screen, false) _check(not gizmos.is_dragging(), "resize handle release completes without opening chooser") selected_prop.global_position = start_position await _wait_frames(2) prop_bounds = STAGE_SELECTION.get_world_aabb(selected_prop) var rotate_handle: Vector2 = gizmos._rotate_handle_position(prop_bounds) var rotate_screen: Vector2 = canvas * rotate_handle await _send_mouse_motion(rotate_screen) await _send_mouse_button(rotate_screen, true) _check(gizmos.is_dragging() and gizmos._dragging == StageGizmos.Handle.ROTATE, "overlapped rotate handle starts rotation immediately") _check(stage.get("_objects_here_popup") == null, "rotate handle does not open the overlap chooser") await _send_mouse_button(rotate_screen, false) _check(not gizmos.is_dragging(), "rotate handle release completes without opening chooser") func _send_mouse_button(position: Vector2, pressed: bool) -> void: var event: InputEventMouseButton = InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_LEFT event.position = position event.global_position = position event.pressed = pressed Input.parse_input_event(event) await process_frame func _send_mouse_motion(position: Vector2, button_mask: int = 0) -> void: var event: InputEventMouseMotion = InputEventMouseMotion.new() event.position = position event.global_position = position event.button_mask = button_mask Input.parse_input_event(event) await process_frame func _wait_frames(count: int) -> void: for _index: int in count: await process_frame await RenderingServer.frame_post_draw func _check(condition: bool, label: String) -> void: if condition: print("PHASE20A_CHECK PASS: %s" % label) else: _failures += 1 push_error("PHASE20A_CHECK FAIL: %s" % label)