Files

307 lines
11 KiB
GDScript

@tool
extends RefCounted
# Shared JSON-to-Godot type coercion for scene.set_property, resource.set_property,
# and any other tool that assigns a user-provided value into a typed property slot.
#
# Contract:
# coerce(value, target_type: int) -> Variant
#
# Returns the coerced value on success. On mismatch, returns a Dictionary with
# a single "_error" key describing the failure — callers check `is Dictionary`
# and forward the message as a tool-level error.
#
# New types go here. Don't re-add `_coerce` to individual tool files; route
# through Coerce.coerce() instead.
static func coerce(value, target_type: int):
match target_type:
TYPE_BOOL:
# Godot 4 has no `bool()` constructor (it errors at runtime). Convert via
# truthiness, but parse strings explicitly so "false"/"0" don't read as
# truthy the way any non-empty string otherwise would.
if value is String:
return value.strip_edges().to_lower() in ["true", "1", "yes", "on"]
return true if value else false
TYPE_INT:
return int(value)
TYPE_FLOAT:
return float(value)
TYPE_STRING, TYPE_STRING_NAME:
return String(value)
TYPE_NODE_PATH:
return NodePath(String(value))
TYPE_VECTOR2:
if value is Array and value.size() == 2:
return Vector2(value[0], value[1])
return {"_error": "Vector2 expects [x, y]"}
TYPE_VECTOR2I:
if value is Array and value.size() == 2:
return Vector2i(int(value[0]), int(value[1]))
return {"_error": "Vector2i expects [x, y]"}
TYPE_VECTOR3:
if value is Array and value.size() == 3:
return Vector3(value[0], value[1], value[2])
return {"_error": "Vector3 expects [x, y, z]"}
TYPE_VECTOR3I:
if value is Array and value.size() == 3:
return Vector3i(int(value[0]), int(value[1]), int(value[2]))
return {"_error": "Vector3i expects [x, y, z]"}
TYPE_VECTOR4:
if value is Array and value.size() == 4:
return Vector4(value[0], value[1], value[2], value[3])
return {"_error": "Vector4 expects [x, y, z, w]"}
TYPE_VECTOR4I:
if value is Array and value.size() == 4:
return Vector4i(int(value[0]), int(value[1]), int(value[2]), int(value[3]))
return {"_error": "Vector4i expects [x, y, z, w]"}
TYPE_RECT2:
if value is Array and value.size() == 4:
return Rect2(value[0], value[1], value[2], value[3])
return {"_error": "Rect2 expects [x, y, width, height]"}
TYPE_RECT2I:
if value is Array and value.size() == 4:
return Rect2i(int(value[0]), int(value[1]), int(value[2]), int(value[3]))
return {"_error": "Rect2i expects [x, y, width, height]"}
TYPE_QUATERNION:
if value is Array and value.size() == 4:
return Quaternion(value[0], value[1], value[2], value[3])
return {"_error": "Quaternion expects [x, y, z, w]"}
TYPE_TRANSFORM2D:
# TRS form: {origin: [x,y], rotation: radians, scale: [x,y]} — all optional.
if value is Dictionary:
var origin = value.get("origin", [0, 0])
var rotation: float = float(value.get("rotation", 0.0))
var scale_v = value.get("scale", [1, 1])
var skew: float = float(value.get("skew", 0.0))
if not (origin is Array and origin.size() == 2 and scale_v is Array and scale_v.size() == 2):
return {"_error": "Transform2D origin/scale expect [x, y]"}
return Transform2D(
rotation,
Vector2(scale_v[0], scale_v[1]),
skew,
Vector2(origin[0], origin[1])
)
return {"_error": "Transform2D expects {origin: [x,y], rotation: radians, scale: [x,y], skew?}"}
TYPE_TRANSFORM3D:
# TRS form: {origin: [x,y,z], rotation: [x,y,z] euler radians, scale: [x,y,z]}.
if value is Dictionary:
var origin = value.get("origin", [0, 0, 0])
var rotation = value.get("rotation", [0, 0, 0])
var scale_v = value.get("scale", [1, 1, 1])
if not (origin is Array and origin.size() == 3
and rotation is Array and rotation.size() == 3
and scale_v is Array and scale_v.size() == 3):
return {"_error": "Transform3D origin/rotation/scale expect [x, y, z]"}
var basis := Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2]))
basis = basis.scaled(Vector3(scale_v[0], scale_v[1], scale_v[2]))
return Transform3D(basis, Vector3(origin[0], origin[1], origin[2]))
return {"_error": "Transform3D expects {origin: [x,y,z], rotation: [x,y,z] (euler rad), scale: [x,y,z]}"}
TYPE_BASIS:
if value is Dictionary:
var rotation = value.get("rotation", [0, 0, 0])
var scale_v = value.get("scale", [1, 1, 1])
if not (rotation is Array and rotation.size() == 3
and scale_v is Array and scale_v.size() == 3):
return {"_error": "Basis rotation/scale expect [x, y, z]"}
return Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2])).scaled(
Vector3(scale_v[0], scale_v[1], scale_v[2]))
return {"_error": "Basis expects {rotation: [x,y,z] (euler rad), scale: [x,y,z]}"}
TYPE_AABB:
if value is Dictionary:
var pos = value.get("position", [0, 0, 0])
var size = value.get("size", [0, 0, 0])
if not (pos is Array and pos.size() == 3 and size is Array and size.size() == 3):
return {"_error": "AABB position/size expect [x, y, z]"}
return AABB(Vector3(pos[0], pos[1], pos[2]), Vector3(size[0], size[1], size[2]))
return {"_error": "AABB expects {position: [x,y,z], size: [x,y,z]}"}
TYPE_PLANE:
if value is Dictionary:
var normal = value.get("normal", [0, 1, 0])
var d: float = float(value.get("d", 0.0))
if not (normal is Array and normal.size() == 3):
return {"_error": "Plane normal expects [x, y, z]"}
return Plane(Vector3(normal[0], normal[1], normal[2]), d)
return {"_error": "Plane expects {normal: [x,y,z], d: float}"}
TYPE_COLOR:
if value is Array and (value.size() == 3 or value.size() == 4):
var a := float(value[3]) if value.size() == 4 else 1.0
return Color(value[0], value[1], value[2], a)
if value is String:
return Color(value)
return {"_error": "Color expects [r,g,b(,a)] or '#rrggbb(aa)'"}
TYPE_PACKED_STRING_ARRAY:
if value is Array:
var out := PackedStringArray()
for s in value:
out.append(String(s))
return out
return {"_error": "PackedStringArray expects an array of strings"}
TYPE_PACKED_INT32_ARRAY:
if value is Array:
var out := PackedInt32Array()
for i in value:
out.append(int(i))
return out
return {"_error": "PackedInt32Array expects an array of numbers"}
TYPE_PACKED_INT64_ARRAY:
if value is Array:
var out := PackedInt64Array()
for i in value:
out.append(int(i))
return out
return {"_error": "PackedInt64Array expects an array of numbers"}
TYPE_PACKED_FLOAT32_ARRAY:
if value is Array:
var out := PackedFloat32Array()
for f in value:
out.append(float(f))
return out
return {"_error": "PackedFloat32Array expects an array of numbers"}
TYPE_PACKED_FLOAT64_ARRAY:
if value is Array:
var out := PackedFloat64Array()
for f in value:
out.append(float(f))
return out
return {"_error": "PackedFloat64Array expects an array of numbers"}
TYPE_PACKED_VECTOR2_ARRAY:
if value is Array:
var out := PackedVector2Array()
for v in value:
if v is Array and v.size() == 2:
out.append(Vector2(v[0], v[1]))
else:
return {"_error": "PackedVector2Array expects [[x, y], ...]"}
return out
return {"_error": "PackedVector2Array expects an array of [x, y] pairs"}
TYPE_PACKED_VECTOR3_ARRAY:
if value is Array:
var out := PackedVector3Array()
for v in value:
if v is Array and v.size() == 3:
out.append(Vector3(v[0], v[1], v[2]))
else:
return {"_error": "PackedVector3Array expects [[x, y, z], ...]"}
return out
return {"_error": "PackedVector3Array expects an array of [x, y, z] triples"}
TYPE_PACKED_COLOR_ARRAY:
if value is Array:
var out := PackedColorArray()
for c in value:
if c is Array and (c.size() == 3 or c.size() == 4):
var a := float(c[3]) if c.size() == 4 else 1.0
out.append(Color(c[0], c[1], c[2], a))
elif c is String:
out.append(Color(c))
else:
return {"_error": "PackedColorArray expects [[r,g,b(,a)], ...] or ['#hex', ...]"}
return out
return {"_error": "PackedColorArray expects an array of colors"}
TYPE_OBJECT:
# Auto-load a Resource when the caller passes a "res://..." or "uid://..." path string.
# Without this, Godot silently drops the assignment and leaves the slot null.
if value is String and (value.begins_with("res://") or value.begins_with("uid://")):
var loaded := ResourceLoader.load(value)
if loaded == null:
return {"_error": "failed to load resource: %s" % value}
return loaded
return value
_:
return value
# Inverse of coerce(): convert a Godot value back to a JSON-native form so the
# response echo is unambiguous data rather than Godot's str() stringification
# (which turns Vector2 into "(x, y)", Color into "(r, g, b, a)", null Object
# into "<Object#null>", etc., all of which look like success strings).
static func to_json(value):
match typeof(value):
TYPE_VECTOR2:
return [value.x, value.y]
TYPE_VECTOR2I:
return [int(value.x), int(value.y)]
TYPE_VECTOR3:
return [value.x, value.y, value.z]
TYPE_VECTOR3I:
return [int(value.x), int(value.y), int(value.z)]
TYPE_VECTOR4:
return [value.x, value.y, value.z, value.w]
TYPE_VECTOR4I:
return [int(value.x), int(value.y), int(value.z), int(value.w)]
TYPE_RECT2:
return [value.position.x, value.position.y, value.size.x, value.size.y]
TYPE_RECT2I:
return [int(value.position.x), int(value.position.y), int(value.size.x), int(value.size.y)]
TYPE_COLOR:
return [value.r, value.g, value.b, value.a]
TYPE_QUATERNION:
return [value.x, value.y, value.z, value.w]
TYPE_NODE_PATH:
return String(value)
TYPE_STRING_NAME:
return String(value)
TYPE_TRANSFORM2D:
return {
"origin": [value.origin.x, value.origin.y],
"x": [value.x.x, value.x.y],
"y": [value.y.x, value.y.y],
}
TYPE_TRANSFORM3D:
return {
"origin": [value.origin.x, value.origin.y, value.origin.z],
"basis": to_json(value.basis),
}
TYPE_BASIS:
return [
[value.x.x, value.x.y, value.x.z],
[value.y.x, value.y.y, value.y.z],
[value.z.x, value.z.y, value.z.z],
]
TYPE_AABB:
return {
"position": [value.position.x, value.position.y, value.position.z],
"size": [value.size.x, value.size.y, value.size.z],
}
TYPE_PLANE:
return {
"normal": [value.normal.x, value.normal.y, value.normal.z],
"d": value.d,
}
TYPE_OBJECT:
if value == null:
return null
if value is Resource and value.resource_path != "":
return {"class": value.get_class(), "resource_path": value.resource_path}
return {"class": (value as Object).get_class()}
TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, \
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY:
return Array(value)
TYPE_PACKED_VECTOR2_ARRAY:
var out_v2: Array = []
for v in value:
out_v2.append([v.x, v.y])
return out_v2
TYPE_PACKED_VECTOR3_ARRAY:
var out_v3: Array = []
for v in value:
out_v3.append([v.x, v.y, v.z])
return out_v3
TYPE_PACKED_COLOR_ARRAY:
var out_c: Array = []
for c in value:
out_c.append([c.r, c.g, c.b, c.a])
return out_c
TYPE_ARRAY:
var out_a: Array = []
for item in value:
out_a.append(to_json(item))
return out_a
TYPE_DICTIONARY:
var out_d: Dictionary = {}
for k in value:
out_d[str(k)] = to_json(value[k])
return out_d
_:
return value