- Implemented test_phase9a_prop_attach.gd to validate anchor schema, alignment math, and preview isolation. - Implemented test_phase9b_prop_centering.gd to check canonical origin presentation, bounds, centering, and save threshold. - Implemented test_phase9c_prop_editor_ux.gd to assess the user experience in the prop editor, including anchor renaming and UI responsiveness. - Added corresponding .uid files for each test. - Created sync_sources.py script to manage markdown file synchronization with the notebook. - Added .gitignore for virtual environment files.
79 lines
4.7 KiB
GDScript
79 lines
4.7 KiB
GDScript
extends SceneTree
|
|
## Phase 9b regression: canonical origin presentation, bounds, centering, and save threshold.
|
|
|
|
const ANCHORS := preload("res://scripts/prop_anchor_data.gd")
|
|
const CANVAS_SCRIPT := preload("res://scripts/prop_canvas.gd")
|
|
const EDITOR := preload("res://scripts/prop_editor.gd")
|
|
|
|
var _checks: int = 0
|
|
var _failures: int = 0
|
|
var _shape_change_count: int = 0
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
_test_anchor_helpers()
|
|
await _test_canvas_bounds_and_translation()
|
|
_test_warning_threshold()
|
|
print("RESULT: %s (%d assertions)" % ["ALL PASSED" if _failures == 0 else "%d FAILED" % _failures, _checks])
|
|
quit(0 if _failures == 0 else 1)
|
|
|
|
func _check(label: String, condition: bool) -> void:
|
|
_checks += 1
|
|
if condition: print("PASS: " + label)
|
|
else:
|
|
_failures += 1
|
|
print("FAIL: " + label)
|
|
|
|
func _test_anchor_helpers() -> void:
|
|
var anchors: Dictionary = ANCHORS.normalize_anchors({
|
|
"primary_grip": {"role": "grip", "position": Vector2(400.0, 200.0), "rotation_deg": 30.0, "z_index_offset": 2, "allowed_hands": "left_only"},
|
|
"emitter": {"role": "emitter", "position": Vector2(450.0, 220.0), "rotation_deg": -15.0, "z_index_offset": -1, "allowed_hands": "both"},
|
|
})
|
|
_check("editable IDs exclude canonical origin", ANCHORS.editable_ids(anchors) == ["primary_grip", "emitter"])
|
|
_check("preferred anchor favors primary grip", ANCHORS.preferred_editable_id(anchors) == "primary_grip")
|
|
var moved: Dictionary = ANCHORS.translated_non_origin(anchors, Vector2(-400.0, -200.0))
|
|
_check("translated anchors preserve origin and metadata", moved["origin"].get("position") == Vector2.ZERO and moved["primary_grip"].get("position") == Vector2.ZERO and is_equal_approx(float(moved["primary_grip"].get("rotation_deg")), 30.0) and int(moved["emitter"].get("z_index_offset")) == -1)
|
|
_check("translated anchors preserve relative vectors", moved["emitter"].get("position") - moved["primary_grip"].get("position") == anchors["emitter"].get("position") - anchors["primary_grip"].get("position"))
|
|
|
|
func _test_canvas_bounds_and_translation() -> void:
|
|
var canvas := CANVAS_SCRIPT.new() as Control
|
|
canvas.size = Vector2(800.0, 600.0)
|
|
canvas.visible = false
|
|
root.add_child(canvas)
|
|
await process_frame
|
|
canvas.call("set_shape_data", [
|
|
{"shape_type": "polygon", "points": [Vector2(400.0, 200.0), Vector2(600.0, 200.0), Vector2(500.0, 300.0)], "closed": true, "vertex_flags": [0, 0, 0]},
|
|
{"shape_type": "line", "points": [Vector2(620.0, 260.0), Vector2(680.0, 260.0)], "closed": false, "vertex_flags": [0, 0]},
|
|
{"shape_type": "circle", "points": [], "radius": 30.0, "closed": true, "vertex_flags": []},
|
|
{"shape_type": "polygon", "points": [], "closed": true, "vertex_flags": []},
|
|
])
|
|
# Place the generated circle explicitly, verifying its perimeter participates in bounds.
|
|
var circle: Dictionary = canvas.shapes[2]
|
|
circle["points"] = PackedVector2Array([Vector2(770.0, 200.0), Vector2(830.0, 200.0), Vector2(800.0, 230.0), Vector2(800.0, 170.0)])
|
|
canvas.shapes[2] = circle
|
|
var bounds: Rect2 = canvas.call("get_shapes_bounds") as Rect2
|
|
_check("bounds union includes polygon line and circle geometry", canvas.call("has_shape_geometry") and bounds.position == Vector2(400.0, 170.0) and bounds.end == Vector2(830.0, 300.0))
|
|
_shape_change_count = 0
|
|
canvas.shape_changed.connect(_on_shape_changed)
|
|
var selected_before: int = canvas.call("get_selected_index") as int
|
|
var did_move: bool = bool(canvas.call("translate_all_shapes", Vector2(-615.0, -235.0)))
|
|
_check("bulk translate commits once", did_move and _shape_change_count == 1)
|
|
var after: Rect2 = canvas.call("get_shapes_bounds") as Rect2
|
|
_check("bulk translation centers exact union bounds", after.get_center().is_zero_approx())
|
|
var rejected_zero: bool = not bool(canvas.call("translate_all_shapes", Vector2.ZERO))
|
|
_check("bulk translation retains selection and rejects zero delta", int(canvas.call("get_selected_index")) == selected_before and rejected_zero and _shape_change_count == 1)
|
|
canvas.call("focus_world_point", Vector2.ZERO)
|
|
_check("focus centers origin without changing zoom", (canvas.get("_pan_offset") as Vector2).is_equal_approx(canvas.size * 0.5) and is_equal_approx(float(canvas.get("_zoom")), 1.0))
|
|
canvas.queue_free()
|
|
|
|
func _on_shape_changed(_shapes: Array[Dictionary]) -> void:
|
|
_shape_change_count += 1
|
|
|
|
func _test_warning_threshold() -> void:
|
|
_check("empty geometry never warns", not EDITOR.should_warn_off_origin(false, Rect2(5000.0, 0.0, 20.0, 20.0)))
|
|
_check("strict threshold does not warn at 1024", not EDITOR.should_warn_off_origin(true, Rect2(1014.0, -10.0, 20.0, 20.0)))
|
|
_check("far small prop warns", EDITOR.should_warn_off_origin(true, Rect2(1025.0, -10.0, 20.0, 20.0)))
|
|
_check("large bounds use diagonal-scaled threshold", not EDITOR.should_warn_off_origin(true, Rect2(1500.0, -500.0, 2000.0, 1000.0)))
|