extends SceneTree ## Phase 9b regression: canonical origin presentation, bounds, centering, and save threshold. const ANCHORS := preload("res://scripts/props/prop_anchor_data.gd") const CANVAS_SCRIPT := preload("res://scripts/editors/prop/prop_canvas.gd") const EDITOR := preload("res://scripts/editors/prop/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)))