- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency. - Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values. - Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage. - Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
252 lines
16 KiB
GDScript
252 lines
16 KiB
GDScript
extends SceneTree
|
|
|
|
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
|
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
|
|
|
var _checks: int = 0
|
|
var _failures: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("\n=== EDITOR VISUAL HARMONIZATION TEST ===")
|
|
root.size = Vector2i(1280, 800)
|
|
await _test_stickman_workspace_and_transactions()
|
|
await _test_prop_reshape_and_chrome()
|
|
print("----------------------------------------")
|
|
if _failures == 0:
|
|
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
|
quit(0)
|
|
else:
|
|
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
|
quit(1)
|
|
|
|
|
|
func check(label: String, condition: bool) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print("PASS: %s" % label)
|
|
else:
|
|
_failures += 1
|
|
push_error("FAIL: %s" % label)
|
|
|
|
|
|
func _test_stickman_workspace_and_transactions() -> void:
|
|
var main_scene: Control = STICKMAN_SCENE.instantiate() as Control
|
|
root.add_child(main_scene)
|
|
await process_frame
|
|
await process_frame
|
|
var editor: Control = main_scene.find_child("StickmanEditor", true, false) as Control
|
|
var panels: Dictionary = editor.get("_body_part_panels") as Dictionary
|
|
var preview: Control = editor.find_child("WholeStickmanPreview", true, false) as Control
|
|
var menu_bar: Control = editor.get_node("ChromeRoot/MenuBar") as Control
|
|
var top_bar: Control = editor.get_node("ChromeRoot/TopBar") as Control
|
|
var workspace_toolbar: Control = editor.get_node("ChromeRoot/WorkspaceToolbar") as Control
|
|
var workspace_row: Control = editor.get_node("ChromeRoot/WorkspaceRow") as Control
|
|
var status_bar: Control = editor.get_node("ChromeRoot/StatusBar") as Control
|
|
var stickman_save_status: Label = editor.get_node("ChromeRoot/StatusBar/SaveStatus") as Label
|
|
check("Stickman keeps ten live BodyPartPanel instances", panels.size() == 10)
|
|
check("one permanent Whole Stickman preview exists", preview != null)
|
|
check("wide layout adapts to Overview", int(editor.call("get_workspace_view")) == 0)
|
|
check("Stickman chrome rows and workspace do not overlap", top_bar.position.y >= menu_bar.position.y + menu_bar.size.y - 1.0 and workspace_toolbar.position.y >= top_bar.position.y + top_bar.size.y - 1.0 and workspace_row.position.y >= workspace_toolbar.position.y + workspace_toolbar.size.y - 1.0 and status_bar.position.y >= workspace_row.position.y + workspace_row.size.y - 1.0)
|
|
check("Overview body panels begin below the chrome", (panels.get("head") as Control).get_global_rect().position.y >= workspace_row.get_global_rect().position.y - 1.0)
|
|
check("clean new Stickman shows explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
|
|
|
var head: Control = panels.get("head") as Control
|
|
var head_id: int = head.get_instance_id()
|
|
check("head part supports authored shapes", bool(head.call("add_shape", &"rectangle", head.call("drawing_center_world"))))
|
|
check("shape creation marks the document dirty", bool(editor.call("has_unsaved_changes")))
|
|
check("Stickman dirty transition shows explicit status", stickman_save_status.text == "Unsaved changes")
|
|
check("new shape is selected", int(head.call("selected_shape_index")) == 0)
|
|
|
|
var zoom: InputEventMouseButton = InputEventMouseButton.new()
|
|
zoom.button_index = MOUSE_BUTTON_WHEEL_UP
|
|
zoom.pressed = true
|
|
zoom.position = Vector2(100.0, 80.0)
|
|
head.call("_on_drawing_area_gui_input", zoom)
|
|
var pan_down: InputEventMouseButton = InputEventMouseButton.new()
|
|
pan_down.button_index = MOUSE_BUTTON_MIDDLE
|
|
pan_down.pressed = true
|
|
pan_down.position = Vector2(100.0, 80.0)
|
|
head.call("_on_drawing_area_gui_input", pan_down)
|
|
var pan_motion: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
pan_motion.position = Vector2(112.0, 91.0)
|
|
head.call("_on_drawing_area_gui_input", pan_motion)
|
|
var pan_up: InputEventMouseButton = InputEventMouseButton.new()
|
|
pan_up.button_index = MOUSE_BUTTON_MIDDLE
|
|
pan_up.pressed = false
|
|
pan_up.position = Vector2(112.0, 91.0)
|
|
head.call("_on_drawing_area_gui_input", pan_up)
|
|
var saved_view: Dictionary = head.call("get_view_context") as Dictionary
|
|
var saved_pan: Vector2 = saved_view.get("pan_offset", Vector2.ZERO) as Vector2
|
|
check("panel zoom and pan are active before moving it", float(saved_view.get("zoom", 1.0)) > 1.0 and saved_pan.length() > 0.0)
|
|
|
|
var stickman_path: String = "user://editor_visual_harmonization_test.stk"
|
|
check("Stickman saves through its document API", bool(editor.call("save_to_path", stickman_path)))
|
|
check("successful save clears dirty state", not bool(editor.call("has_unsaved_changes")))
|
|
check("saved Stickman shows explicit saved status", stickman_save_status.text == "Saved")
|
|
var stickman_file: FileAccess = FileAccess.open(stickman_path, FileAccess.READ)
|
|
var stickman_json: Variant = JSON.parse_string(stickman_file.get_as_text())
|
|
stickman_file.close()
|
|
var saved_parts: Dictionary = (stickman_json as Dictionary).get("body_parts", {}) as Dictionary
|
|
var saved_head: Dictionary = saved_parts.get("head", {}) as Dictionary
|
|
check("save preserves the existing 1.5 format", str((stickman_json as Dictionary).get("version", "")) == "1.5")
|
|
check("save includes authored head shapes", (saved_head.get("shapes", []) as Array).size() == 1)
|
|
check("save still writes guide offset metadata", saved_head.has("guide_offset"))
|
|
|
|
var before_switch_view: Dictionary = head.call("get_view_context") as Dictionary
|
|
editor.call("request_workspace_view", 1, true)
|
|
check("Focus view keeps the same panel instance", head.get_instance_id() == head_id and int(editor.call("get_workspace_view")) == 1)
|
|
check("Focus view preserves panel zoom and pan", _view_contexts_equal(before_switch_view, head.call("get_view_context") as Dictionary))
|
|
check("view choice does not dirty authored content", not bool(editor.call("has_unsaved_changes")))
|
|
root.size = Vector2i(520, 800)
|
|
await process_frame
|
|
editor.call("_refresh_responsive_workspace")
|
|
check("manual Focus choice survives a narrow resize", int(editor.call("get_workspace_view")) == 1)
|
|
var whole_stickman_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/WholeStickmanButton") as Button
|
|
var back_to_part_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/BackToPartButton") as Button
|
|
var body_part_workspace: Control = editor.get_node("ChromeRoot/WorkspaceRow/BodyPartWorkspace") as Control
|
|
check("narrow Focus keeps part controls and Whole Stickman access visible", body_part_workspace.visible and whole_stickman_button.visible and not preview.visible)
|
|
whole_stickman_button.emit_signal("pressed")
|
|
await process_frame
|
|
check("narrow Whole Stickman control opens the permanent preview", not body_part_workspace.visible and preview.visible and back_to_part_button.visible and int(editor.call("get_workspace_view")) == 1)
|
|
back_to_part_button.emit_signal("pressed")
|
|
await process_frame
|
|
check("Back to Part restores the same live panel at narrow width", body_part_workspace.visible and not preview.visible and whole_stickman_button.visible and head.get_instance_id() == head_id)
|
|
root.size = Vector2i(1280, 800)
|
|
await process_frame
|
|
editor.call("_refresh_responsive_workspace")
|
|
|
|
editor.call("request_active_part", "torso")
|
|
check("part picker switches the active focus panel", str(editor.call("get_active_part_name")) == "torso")
|
|
editor.call("request_active_part", "head")
|
|
check("switching back restores the original head instance", panels.get("head") == head and head.get_instance_id() == head_id)
|
|
check("switching parts preserves head geometry and selection", int(head.call("selected_shape_index")) == 0 and (head.get("shapes") as Array).size() == 1)
|
|
editor.call("request_workspace_view", 0, true)
|
|
check("Overview restores the same authored panel", int(editor.call("get_workspace_view")) == 0 and head.get_instance_id() == head_id)
|
|
|
|
head.call("open_selected_color_picker")
|
|
var picker: PopupPanel = head.get("color_picker_popup") as PopupPanel
|
|
check("open color popup blocks presentation switching", picker.visible and not bool(head.call("can_switch_presentation")))
|
|
editor.call("request_active_part", "torso")
|
|
editor.call("request_workspace_view", 1, true)
|
|
check("deferred view switch waits while popup is open", int(editor.call("get_workspace_view")) == 0 and str(editor.call("get_active_part_name")) == "head")
|
|
picker.hide()
|
|
await process_frame
|
|
await process_frame
|
|
check("deferred part and view switch resumes after popup closes", int(editor.call("get_workspace_view")) == 1 and str(editor.call("get_active_part_name")) == "torso")
|
|
check("popup and workspace changes remain clean", not bool(editor.call("has_unsaved_changes")))
|
|
editor.call("request_active_part", "head")
|
|
var preview_area: Control = preview.get("preview_area") as Control
|
|
await process_frame
|
|
var raw_points: PackedVector2Array = preview.call("_collect_all_raw_points", "head") as PackedVector2Array
|
|
var part_center: Vector2 = preview.call("_compute_center", raw_points) as Vector2
|
|
var initial_part_position: Vector2 = preview.call("get_part_position", "head") as Vector2
|
|
var mouse_down: InputEventMouseButton = InputEventMouseButton.new()
|
|
mouse_down.button_index = MOUSE_BUTTON_LEFT
|
|
mouse_down.pressed = true
|
|
mouse_down.position = part_center
|
|
preview.call("_on_preview_gui_input", mouse_down)
|
|
var drag: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
drag.position = part_center + Vector2(16.0, 12.0)
|
|
preview.call("_on_preview_gui_input", drag)
|
|
var mouse_up: InputEventMouseButton = InputEventMouseButton.new()
|
|
mouse_up.button_index = MOUSE_BUTTON_LEFT
|
|
mouse_up.pressed = false
|
|
mouse_up.position = drag.position
|
|
preview.call("_on_preview_gui_input", mouse_up)
|
|
check("preview drag commits an authored part transform", not (preview.call("get_part_position", "head") as Vector2).is_equal_approx(initial_part_position))
|
|
check("committed preview transform marks document dirty", bool(editor.call("has_unsaved_changes")))
|
|
check("preview input uses the live preview area", preview_area != null and preview_area.size.x > 0.0)
|
|
check("save after transform keeps the existing format", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
|
preview.call("_mirror_part_x")
|
|
check("mirror transform marks document dirty", bool(editor.call("has_unsaved_changes")) and (preview.call("get_part_scale", "head") as Vector2).x < 0.0)
|
|
check("mirror transform can be saved", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
|
preview.call("_bring_forward_part")
|
|
check("part order edit marks document dirty", bool(editor.call("has_unsaved_changes")))
|
|
|
|
check("second shape can be authored", bool(head.call("add_shape", &"line", head.call("drawing_center_world"))))
|
|
editor.call("_on_file_menu_id_pressed", 2)
|
|
var unsaved_dialog: ConfirmationDialog = editor.get("_unsaved_dialog") as ConfirmationDialog
|
|
check("Start Over shows a save/discard/cancel transaction", unsaved_dialog.visible)
|
|
unsaved_dialog.get_cancel_button().emit_signal("pressed")
|
|
await process_frame
|
|
check("Cancel leaves dirty content in place", bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).size() == 2)
|
|
editor.call("_on_file_menu_id_pressed", 2)
|
|
unsaved_dialog.emit_signal("custom_action", "discard")
|
|
await process_frame
|
|
check("Discard clears authored parts and dirty state", not bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).is_empty())
|
|
check("clean new Stickman returns to explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
|
check("Start Over keeps the selected workspace view", int(editor.call("get_workspace_view")) == 1)
|
|
DirAccess.remove_absolute(stickman_path)
|
|
main_scene.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _test_prop_reshape_and_chrome() -> void:
|
|
var editor: Control = PROP_SCENE.instantiate() as Control
|
|
root.add_child(editor)
|
|
await process_frame
|
|
await process_frame
|
|
var canvas: Control = editor.get("_canvas") as Control
|
|
var save_status: Label = editor.get("_save_status") as Label
|
|
check("Prop chrome uses wrapping top and mode rows", editor.get("_top_right") is FlowContainer and editor.get("_mode_bar") is FlowContainer)
|
|
check("Prop action target meets 44 px minimum", (editor.get("_create_point_button") as Button).custom_minimum_size.y >= 44.0)
|
|
check("new Prop has an explicit clean save status", str(save_status.text).ends_with("Not saved yet"))
|
|
check("whole-prop action uses approved discoverable wording", str((editor.get("_whole_prop_resize_button") as Button).text) == "Move, Turn, or Resize Prop")
|
|
check("material inspector uses child-facing grip language", str((editor.get("_friction_label") as Label).text).begins_with("Surface grip:"))
|
|
check("anchor layer control uses the approved label", (editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) is Label and str(((editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) as Label).text) == "Held prop layer:")
|
|
|
|
canvas.call("add_primitive", "box", Vector2(150.0, 150.0))
|
|
var shapes: Array = canvas.call("get_shape_data") as Array
|
|
check("prop primitive is selected for editing", int(canvas.call("get_selected_index")) == 0 and shapes.size() == 1)
|
|
check("Prop edit shows explicit unsaved status", str(save_status.text).ends_with("Unsaved changes"))
|
|
var prop_path: String = "user://editor_visual_harmonization_test.prop"
|
|
var save_result: Variant = await editor.call("_save_to", prop_path, false)
|
|
check("clean Prop save shows explicit saved status", bool(save_result) and str(save_status.text).ends_with("Saved"))
|
|
editor.call("_on_points_mode_pressed")
|
|
var create_button: Button = editor.get("_create_point_button") as Button
|
|
check("Reshape exposes the Create Point action", create_button.visible and create_button.text == "📍 Create Point")
|
|
create_button.emit_signal("pressed")
|
|
check("Create Point arms with a visible Esc hint", bool(canvas.call("is_create_point_armed")) and str((editor.get("_create_point_hint") as Label).text).contains("Press Esc"))
|
|
var escape: InputEventKey = InputEventKey.new()
|
|
escape.keycode = KEY_ESCAPE
|
|
escape.pressed = true
|
|
editor.call("_unhandled_key_input", escape)
|
|
check("Esc cancels the armed point action", not bool(canvas.call("is_create_point_armed")))
|
|
create_button.emit_signal("pressed")
|
|
var internal_shapes: Array = canvas.get("shapes") as Array
|
|
var first_shape: Dictionary = internal_shapes[0] as Dictionary
|
|
var original_points: PackedVector2Array = first_shape.get("points", PackedVector2Array()) as PackedVector2Array
|
|
var edge_midpoint: Vector2 = (original_points[0] + original_points[1]) * 0.5
|
|
var canvas_zoom: float = float(canvas.get("_zoom"))
|
|
var canvas_pan: Vector2 = canvas.get("_pan_offset") as Vector2
|
|
var edge_screen_pos: Vector2 = edge_midpoint * canvas_zoom + canvas_pan
|
|
check("toolbar click target resolves to the visible shape edge", int(canvas.call("hit_test_segment", edge_midpoint)) == 0)
|
|
var edge_click: InputEventMouseButton = InputEventMouseButton.new()
|
|
edge_click.button_index = MOUSE_BUTTON_LEFT
|
|
edge_click.pressed = true
|
|
edge_click.position = edge_screen_pos
|
|
canvas.call("_on_canvas_gui_input", edge_click)
|
|
shapes = canvas.call("get_shape_data") as Array
|
|
var reshaped: Dictionary = shapes[0] as Dictionary
|
|
var vertices: PackedVector2Array = reshaped.get("points", PackedVector2Array()) as PackedVector2Array
|
|
check("armed edge click inserts a point", vertices.size() == 5)
|
|
check("inserted point records reshaped state", bool(reshaped.get("reshaped", false)))
|
|
check("inserted point marks Prop document dirty", bool(editor.get("_dirty")))
|
|
check("Create Point updates Prop status to unsaved", str(save_status.text).ends_with("Unsaved changes"))
|
|
check("one click disarms Create Point", not bool(canvas.call("is_create_point_armed")))
|
|
if FileAccess.file_exists(prop_path):
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(prop_path))
|
|
editor.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _view_contexts_equal(left: Dictionary, right: Dictionary) -> bool:
|
|
var left_pan: Vector2 = left.get("pan_offset", Vector2.ZERO) as Vector2
|
|
var right_pan: Vector2 = right.get("pan_offset", Vector2.ZERO) as Vector2
|
|
return is_equal_approx(float(left.get("zoom", 1.0)), float(right.get("zoom", 1.0))) and left_pan.is_equal_approx(right_pan)
|