- Implement tests for PropFile, including preset conversion, file dictionary conversion, round-trip serialization, version rejection, default values for missing keys, flattening, sanitization, and save/load functionality. - Create tests for PropEditor, covering window title formatting, mode button text, safe saving, and recent file list management. - Add tests for PropTheme, validating fallback behavior, invalid input handling, and default values for primitives and UI tokens. - Introduce tests for ThumbnailCache, ensuring proper key formatting, PNG handling, and behavior with missing files.
388 lines
18 KiB
GDScript
388 lines
18 KiB
GDScript
# Phase 9 Prop Editor - headless regression checks for PropCanvas math
|
|
# (vertex insert/remove, mirror, z-order, clipboard, transforms, sanitize).
|
|
# No GUI event injection; the public math APIs are driven directly.
|
|
# Run with:
|
|
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\tmp\stickman" --script "res://tests/test_prop_editor_canvas.gd"
|
|
|
|
extends SceneTree
|
|
|
|
const PROP_CANVAS := preload("res://scripts/prop_canvas.gd")
|
|
|
|
var _checks: int = 0
|
|
var _failures: int = 0
|
|
var _changed_count: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("\n=== PHASE 9 PROP CANVAS TEST ===")
|
|
await _test_primitives()
|
|
await _test_vertex_insert_remove()
|
|
await _test_mirror()
|
|
await _test_z_order()
|
|
await _test_clipboard()
|
|
await _test_transforms()
|
|
await _test_sanitize_and_radius()
|
|
await _test_import_export_and_clear()
|
|
await _test_edit_mode()
|
|
await _test_gizmo_constants()
|
|
await _test_create_point_on_edge()
|
|
await _test_segment_hit_test()
|
|
await _test_reshaped_flag()
|
|
await _test_selection_halo()
|
|
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
|
|
print("FAIL: %s" % label)
|
|
|
|
|
|
func _new_canvas() -> PropCanvas:
|
|
var canvas: PropCanvas = PROP_CANVAS.new()
|
|
root.add_child(canvas)
|
|
canvas.size = Vector2(800, 600)
|
|
_changed_count = 0
|
|
canvas.shape_changed.connect(func(_shapes: Array[Dictionary]) -> void: _changed_count += 1)
|
|
return canvas
|
|
|
|
|
|
func _free_canvas(canvas: PropCanvas) -> void:
|
|
canvas.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _bbox(pts: Array) -> Rect2:
|
|
var first: Dictionary = pts[0]
|
|
var bbox := Rect2(Vector2(float(first.get("x", 0.0)), float(first.get("y", 0.0))), Vector2.ZERO)
|
|
for p: Dictionary in pts:
|
|
bbox = bbox.expand(Vector2(float(p.get("x", 0.0)), float(p.get("y", 0.0))))
|
|
return bbox
|
|
|
|
|
|
func _export_points(sd: Dictionary) -> Array:
|
|
return sd.get("points", []) as Array
|
|
|
|
|
|
func _test_primitives() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
var box: Dictionary = canvas.add_primitive("box", Vector2(100, 100))
|
|
check("box appended", canvas.shapes.size() == 1)
|
|
check("box type", str(box.get("shape_type", "")) == "polygon")
|
|
check("box 4 points", _export_points(box).size() == 4)
|
|
check("box closed", bool(box.get("closed", false)))
|
|
check("add emits shape_changed", _changed_count == 1)
|
|
check("box selected", canvas.get_selected_index() == 0)
|
|
|
|
var big: Dictionary = canvas.add_primitive("box", Vector2(200, 200), {"size_x": 100.0, "size_y": 100.0})
|
|
var big_bbox: Rect2 = _bbox(_export_points(big))
|
|
check("box params respected", is_equal_approx(big_bbox.size.x, 100.0) and is_equal_approx(big_bbox.size.y, 100.0))
|
|
|
|
var ball: Dictionary = canvas.add_primitive("ball", Vector2(300, 300))
|
|
check("ball type", str(ball.get("shape_type", "")) == "circle")
|
|
check("ball radius default", is_equal_approx(float(ball.get("radius", 0.0)), 24.0))
|
|
check("ball points 12", _export_points(ball).size() == 12)
|
|
|
|
var line: Dictionary = canvas.add_primitive("line", Vector2(400, 400))
|
|
check("line type", str(line.get("shape_type", "")) == "line")
|
|
check("line open", not bool(line.get("closed", true)))
|
|
check("line 2 points", _export_points(line).size() == 2)
|
|
|
|
var plank: Dictionary = canvas.add_primitive("plank", Vector2(500, 500))
|
|
check("plank polygon", str(plank.get("shape_type", "")) == "polygon")
|
|
var triangle: Dictionary = canvas.add_primitive("triangle", Vector2(600, 600))
|
|
check("triangle 3 points", _export_points(triangle).size() == 3)
|
|
check("triangle closed", bool(triangle.get("closed", false)))
|
|
var circle: Dictionary = canvas.add_primitive("circle", Vector2(700, 700), {"radius": 50.0})
|
|
check("circle radius param", is_equal_approx(float(circle.get("radius", 0.0)), 50.0))
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_vertex_insert_remove() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(100, 100))
|
|
canvas.insert_vertex(0, 2, Vector2(124, 100))
|
|
check("insert grows point list", _export_points(canvas.get_shape_data()[0]).size() == 5)
|
|
var flags: Array = canvas.get_shape_data()[0].get("vertex_flags", []) as Array
|
|
check("inserted vertex flagged USER_CREATED", int(flags[2]) == 1)
|
|
check("original vertices stay 0", int(flags[0]) == 0 and int(flags[4]) == 0)
|
|
canvas.remove_vertex(0, 2)
|
|
check("remove shrinks point list", _export_points(canvas.get_shape_data()[0]).size() == 4)
|
|
check("flags aligned after remove", (canvas.get_shape_data()[0].get("vertex_flags", []) as Array).size() == 4)
|
|
|
|
canvas.remove_vertex(0, 0)
|
|
canvas.remove_vertex(0, 0)
|
|
check("polygon with 2 points becomes open", bool(canvas.shapes[0].get("closed", true)) == false)
|
|
canvas.remove_vertex(0, 0)
|
|
check("below 2 vertices drops shape", canvas.shapes.size() == 0)
|
|
check("selection cleared after drop", canvas.get_selected_index() == -1)
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_mirror() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
var tri: Dictionary = canvas.add_primitive("triangle", Vector2(100, 100))
|
|
var before: Array = _export_points(tri)
|
|
var bbox: Rect2 = _bbox(before)
|
|
canvas.mirror_x(0)
|
|
var after_x: Array = _export_points(canvas.get_shape_data()[0])
|
|
check("mirror_x flips x about center", is_equal_approx(float(after_x[0].get("x", 0.0)), 2.0 * bbox.get_center().x - float(before[0].get("x", 0.0))))
|
|
check("mirror_x keeps y", is_equal_approx(float(after_x[0].get("y", 0.0)), float(before[0].get("y", 0.0))))
|
|
check("mirror_x keeps bbox center", _bbox(after_x).get_center().is_equal_approx(bbox.get_center()))
|
|
canvas.mirror_y(0)
|
|
var after_y: Array = _export_points(canvas.get_shape_data()[0])
|
|
check("mirror_y flips y about center", is_equal_approx(float(after_y[0].get("y", 0.0)), 2.0 * bbox.get_center().y - float(after_x[0].get("y", 0.0))))
|
|
check("mirror_y keeps bbox center", _bbox(after_y).get_center().is_equal_approx(bbox.get_center()))
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_z_order() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(0, 0), {"fill_color": "#FF0000", "outline_color": "#000000"})
|
|
canvas.add_primitive("ball", Vector2(100, 0), {"fill_color": "#00FF00", "outline_color": "#000000"})
|
|
canvas.add_primitive("line", Vector2(200, 0), {"fill_color": "#0000FF", "outline_color": "#000000"})
|
|
canvas.bring_forward(1)
|
|
check("bring_forward swaps up", str(canvas.get_shape_data()[1].get("shape_type", "")) == "line")
|
|
check("bring_forward swaps down", str(canvas.get_shape_data()[2].get("shape_type", "")) == "circle")
|
|
canvas.send_back(2)
|
|
check("send_back swaps down", str(canvas.get_shape_data()[1].get("shape_type", "")) == "circle")
|
|
canvas.bring_forward(2)
|
|
check("bring_forward at top is no-op", str(canvas.get_shape_data()[2].get("shape_type", "")) == "line")
|
|
canvas.send_back(0)
|
|
check("send_back at bottom is no-op", str(canvas.get_shape_data()[0].get("shape_type", "")) == "polygon")
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_clipboard() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(50, 50), {"fill_color": "#123456", "outline_color": "#000000"})
|
|
var copied: Dictionary = canvas.copy_shape(0)
|
|
check("copy returns deep dict", not copied.is_empty() and copied.has("points"))
|
|
canvas.set_shape_fill_color(0, "#ABCDEF")
|
|
check("copy is independent", str(copied.get("fill_color", "")) == "#123456")
|
|
canvas.paste_shape(copied, Vector2(100, 100))
|
|
check("paste appends", canvas.shapes.size() == 2)
|
|
var pasted_pts: Array = _export_points(canvas.get_shape_data()[1])
|
|
var pasted_bbox: Rect2 = _bbox(pasted_pts)
|
|
check("paste offsets by (16,16)", pasted_bbox.get_center().is_equal_approx(Vector2(116, 116)))
|
|
check("paste preserves color", str(canvas.get_shape_data()[1].get("fill_color", "")) == "#123456")
|
|
check("paste selects new shape", canvas.get_selected_index() == 1)
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_transforms() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(100, 100))
|
|
canvas.translate_selected(Vector2(10, -5))
|
|
var pts: Array = _export_points(canvas.get_shape_data()[0])
|
|
check("translate moves all vertices", _bbox(pts).get_center().is_equal_approx(Vector2(110, 95)))
|
|
|
|
canvas.add_primitive("box", Vector2(200, 200))
|
|
canvas.select(1)
|
|
canvas.rotate_selected(PI / 2.0)
|
|
var rotated_pts: Array = _export_points(canvas.get_shape_data()[1])
|
|
# Box corners at (224,224) rotate 90 degrees ccw about (200,200) -> (176,224).
|
|
var found: bool = false
|
|
for p: Dictionary in rotated_pts:
|
|
if Vector2(float(p.get("x", 0.0)), float(p.get("y", 0.0))).is_equal_approx(Vector2(176, 224)):
|
|
found = true
|
|
check("rotate 90 degrees about bbox center", found)
|
|
check("rotate keeps bbox center", _bbox(rotated_pts).get_center().is_equal_approx(Vector2(200, 200)))
|
|
|
|
canvas.scale_selected(Vector2(2.0, 1.0))
|
|
var scaled_pts: Array = _export_points(canvas.get_shape_data()[1])
|
|
var scaled_bbox: Rect2 = _bbox(scaled_pts)
|
|
check("scale stretches width", is_equal_approx(scaled_bbox.size.x, 96.0))
|
|
check("scale keeps height", is_equal_approx(scaled_bbox.size.y, 48.0))
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_sanitize_and_radius() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.set_grid_size(16)
|
|
canvas.set_snap_enabled(true)
|
|
# Rectangle with one collinear point and one off-grid point; sanitize must
|
|
# grid-snap, drop the collinear vertex, and enforce clockwise winding.
|
|
canvas.set_shape_data([{
|
|
"shape_type": "polygon",
|
|
"points": [
|
|
{"x": 0.0, "y": 0.0},
|
|
{"x": 16.0, "y": 0.0},
|
|
{"x": 32.0, "y": 0.0},
|
|
{"x": 32.0, "y": 32.0},
|
|
{"x": 0.0, "y": 33.0},
|
|
],
|
|
"fill_color": "#000000",
|
|
"outline_color": "#000000",
|
|
"outline_width": 2.0,
|
|
"closed": true,
|
|
"vertex_flags": [0, 1, 0, 0, 0],
|
|
}])
|
|
canvas.sanitize_shape(0)
|
|
var pts: Array = _export_points(canvas.get_shape_data()[0])
|
|
check("sanitize drops collinear vertex", pts.size() == 4)
|
|
var packed := PackedVector2Array()
|
|
for p: Dictionary in pts:
|
|
packed.append(Vector2(float(p.get("x", 0.0)), float(p.get("y", 0.0))))
|
|
check("sanitize enforces clockwise", Geometry2D.is_polygon_clockwise(packed))
|
|
var min_y: float = INF
|
|
var max_y: float = -INF
|
|
for v: Vector2 in packed:
|
|
min_y = minf(min_y, v.y)
|
|
max_y = maxf(max_y, v.y)
|
|
check("sanitize grid-snaps", is_equal_approx(min_y, 0.0) and is_equal_approx(max_y, 32.0))
|
|
var flags: Array = canvas.get_shape_data()[0].get("vertex_flags", []) as Array
|
|
check("sanitize keeps flags aligned", flags.size() == 4)
|
|
|
|
var circle: Dictionary = canvas.add_primitive("circle", Vector2(300, 300), {"radius": 40.0})
|
|
var before_count: int = _export_points(circle).size()
|
|
canvas.sanitize_shape(1)
|
|
check("sanitize skips circles", _export_points(canvas.get_shape_data()[1]).size() == before_count)
|
|
canvas.set_shape_radius(1, 80.0)
|
|
var radius_pts: Array = _export_points(canvas.get_shape_data()[1])
|
|
check("set_shape_radius resizes points", is_equal_approx(float(canvas.get_shape_data()[1].get("radius", 0.0)), 80.0))
|
|
var radius_bbox: Rect2 = _bbox(radius_pts)
|
|
check("radius bbox matches", is_equal_approx(radius_bbox.size.x, 160.0) and is_equal_approx(radius_bbox.size.y, 160.0))
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_import_export_and_clear() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.set_shape_data([{
|
|
"shape_type": "polygon",
|
|
"points": [{"x": 0.0, "y": 0.0}, {"x": 10.0, "y": 0.0}, {"x": 10.0, "y": 10.0}],
|
|
"fill_color": "#111111",
|
|
"outline_color": "#222222",
|
|
"outline_width": 2.0,
|
|
"closed": true,
|
|
"vertex_flags": [0, 1, 0],
|
|
}])
|
|
check("import plain {x,y} data", canvas.shapes.size() == 1)
|
|
check("import preserves flags", (canvas.shapes[0].get("vertex_flags", PackedInt32Array()) as PackedInt32Array).size() == 3)
|
|
var exported: Array[Dictionary] = canvas.get_shape_data()
|
|
check("export plain format", exported.size() == 1 and (exported[0].get("points", []) as Array)[0] is Dictionary)
|
|
var before_changed: int = _changed_count
|
|
canvas.clear()
|
|
check("clear empties shapes", canvas.shapes.is_empty())
|
|
check("clear emits no shape_changed", _changed_count == before_changed)
|
|
check("export after clear empty", canvas.get_shape_data().is_empty())
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_edit_mode() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
check("default mode is TRANSFORM", canvas.get_edit_mode() == PROP_CANVAS.EditMode.TRANSFORM)
|
|
var mode_fires: Array = [0]
|
|
canvas.mode_changed.connect(func(_mode: int) -> void: mode_fires[0] += 1)
|
|
canvas.set_edit_mode(PROP_CANVAS.EditMode.POINTS)
|
|
check("set_edit_mode(POINTS) applies", canvas.get_edit_mode() == PROP_CANVAS.EditMode.POINTS)
|
|
check("mode_changed fired once on change", int(mode_fires[0]) == 1)
|
|
canvas.set_edit_mode(PROP_CANVAS.EditMode.POINTS)
|
|
check("re-setting same mode does not fire", int(mode_fires[0]) == 1)
|
|
canvas.set_edit_mode(99)
|
|
check("unknown mode clamps to TRANSFORM", canvas.get_edit_mode() == PROP_CANVAS.EditMode.TRANSFORM)
|
|
check("clamp fires mode_changed", int(mode_fires[0]) == 2)
|
|
canvas.set_edit_mode(PROP_CANVAS.EditMode.POINTS)
|
|
canvas.add_primitive("box", Vector2(100, 100))
|
|
canvas.clear()
|
|
check("clear keeps the mode", canvas.get_edit_mode() == PROP_CANVAS.EditMode.POINTS)
|
|
canvas.set_shape_data([])
|
|
check("set_shape_data keeps the mode", canvas.get_edit_mode() == PROP_CANVAS.EditMode.POINTS)
|
|
canvas.select(0)
|
|
canvas.deselect()
|
|
check("select/deselect keep the mode", canvas.get_edit_mode() == PROP_CANVAS.EditMode.POINTS)
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_gizmo_constants() -> void:
|
|
check("rotation dot radius constant", is_equal_approx(PROP_CANVAS.ROTATION_CIRCLE_RADIUS, 5.0))
|
|
check("rotation dot hit radius constant", is_equal_approx(PROP_CANVAS.ROTATION_CIRCLE_HIT_RADIUS, 12.0))
|
|
check("rotation dot offset constant", is_equal_approx(PROP_CANVAS.ROTATION_CIRCLE_OFFSET, 26.0))
|
|
check("scale cross size constant", is_equal_approx(PROP_CANVAS.SCALE_CROSS_SIZE, 5.0))
|
|
check("scale cross hit radius constant", is_equal_approx(PROP_CANVAS.SCALE_CROSS_HIT_RADIUS, 12.0))
|
|
check("vertex handle radius constant", is_equal_approx(PROP_CANVAS.HANDLE_RADIUS, 3.0))
|
|
var constant_map: Dictionary = (PROP_CANVAS as Script).get_script_constant_map()
|
|
check("rotate ring constant removed", not constant_map.has("ROTATE_RING_RADIUS"))
|
|
check("scale circle constant removed", not constant_map.has("SCALE_HANDLE_DISTANCE"))
|
|
|
|
|
|
func _test_create_point_on_edge() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(100, 100))
|
|
var created: Array = []
|
|
canvas.vertex_created.connect(func(s_idx: int, v_idx: int) -> void: created.append([s_idx, v_idx]))
|
|
# Cursor at (110, 60): the nearest segment is the top edge (76,76)-(124,76);
|
|
# the projection onto it is (110, 76) - NOT the edge midpoint (100, 76).
|
|
var idx: int = canvas.create_point_on_edge(Vector2(110, 60))
|
|
check("create_point_on_edge returns valid index", idx >= 0)
|
|
var pts: Array = _export_points(canvas.get_shape_data()[0])
|
|
check("create point grows the vertex list", pts.size() == 5)
|
|
var inserted: Dictionary = pts[idx]
|
|
check("inserted point is the cursor projection", Vector2(float(inserted.get("x", 0.0)), float(inserted.get("y", 0.0))).is_equal_approx(Vector2(110, 76)))
|
|
var flags: Array = canvas.get_shape_data()[0].get("vertex_flags", []) as Array
|
|
check("inserted vertex flagged USER_CREATED", int(flags[idx]) == 1)
|
|
check("vertex_created emitted with shape and vertex index", created.size() == 1 and int(created[0][0]) == 0 and int(created[0][1]) == idx)
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_segment_hit_test() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("box", Vector2(100, 100))
|
|
check("segment hit above top edge", canvas.hit_test_segment(Vector2(110, 70)) == 0)
|
|
check("segment hit left of left edge", canvas.hit_test_segment(Vector2(70, 90)) == 0)
|
|
check("segment hit misses interior", canvas.hit_test_segment(Vector2(100, 100)) == -1)
|
|
check("segment hit misses empty canvas", canvas.hit_test_segment(Vector2(400, 400)) == -1)
|
|
canvas.add_primitive("line", Vector2(300, 300))
|
|
check("segment hit on open line", canvas.hit_test_segment(Vector2(300, 306)) == 1)
|
|
canvas.select(1)
|
|
check("segment hit frontmost wins", canvas.hit_test_segment(Vector2(300, 306)) == 1)
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_reshaped_flag() -> void:
|
|
var canvas: PropCanvas = _new_canvas()
|
|
canvas.add_primitive("circle", Vector2(100, 100), {"radius": 40.0})
|
|
check("new circle not reshaped", not bool(canvas.shapes[0].get("reshaped", true)))
|
|
check("export carries reshaped false", not bool(canvas.get_shape_data()[0].get("reshaped", true)))
|
|
canvas.insert_vertex(0, 0, Vector2(60, 60))
|
|
check("insert vertex reshapes", bool(canvas.shapes[0].get("reshaped", false)))
|
|
canvas.set_shape_radius(0, 60.0)
|
|
check("set_shape_radius clears reshaped", not bool(canvas.shapes[0].get("reshaped", true)))
|
|
canvas.remove_vertex(0, 0)
|
|
check("remove vertex reshapes", bool(canvas.shapes[0].get("reshaped", false)))
|
|
canvas.add_primitive("circle", Vector2(300, 300), {"radius": 40.0})
|
|
canvas.select(1)
|
|
canvas.scale_selected(Vector2(2.0, 2.0))
|
|
check("uniform scale keeps circle primitive", not bool(canvas.shapes[1].get("reshaped", true)))
|
|
canvas.scale_selected(Vector2(2.0, 1.0))
|
|
check("non-uniform scale reshapes circle", bool(canvas.shapes[1].get("reshaped", false)))
|
|
canvas.add_primitive("box", Vector2(500, 500))
|
|
canvas.select(2)
|
|
canvas.translate_selected(Vector2(10, 0))
|
|
check("translate does not reshape", not bool(canvas.shapes[2].get("reshaped", true)))
|
|
canvas.rotate_selected(0.5)
|
|
check("rotate does not reshape", not bool(canvas.shapes[2].get("reshaped", true)))
|
|
canvas.mirror_x(2)
|
|
check("mirror does not reshape", not bool(canvas.shapes[2].get("reshaped", true)))
|
|
canvas.insert_vertex(2, 0, Vector2(400, 400))
|
|
check("vertex insert reshapes polygon", bool(canvas.shapes[2].get("reshaped", false)))
|
|
await _free_canvas(canvas)
|
|
|
|
|
|
func _test_selection_halo() -> void:
|
|
check("selection halo is dark", PROP_CANVAS.SELECTION_HALO_COLOR.get_luminance() < 0.3)
|
|
check("selection core is bright", PROP_CANVAS.SELECTION_CORE_COLOR.get_luminance() > 0.7)
|