256 lines
8.3 KiB
GDScript
256 lines
8.3 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
## Value coercion helpers for material authoring.
|
|
##
|
|
## Extends node_handler._coerce_value with material-specific cases:
|
|
## - enum-by-name (transparency="alpha" → TRANSPARENCY_ALPHA)
|
|
## - texture path → Texture2D
|
|
## - {r,g,b,a} dict → Color (also handled by node coerce, but we want it inline)
|
|
|
|
|
|
const _ENUM_TABLES := {
|
|
"transparency": {
|
|
"disabled": BaseMaterial3D.TRANSPARENCY_DISABLED,
|
|
"alpha": BaseMaterial3D.TRANSPARENCY_ALPHA,
|
|
"alpha_scissor": BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR,
|
|
"alpha_hash": BaseMaterial3D.TRANSPARENCY_ALPHA_HASH,
|
|
"alpha_depth_pre_pass": BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS,
|
|
},
|
|
"shading_mode": {
|
|
"unshaded": BaseMaterial3D.SHADING_MODE_UNSHADED,
|
|
"per_pixel": BaseMaterial3D.SHADING_MODE_PER_PIXEL,
|
|
"per_vertex": BaseMaterial3D.SHADING_MODE_PER_VERTEX,
|
|
},
|
|
"blend_mode": {
|
|
"mix": BaseMaterial3D.BLEND_MODE_MIX,
|
|
"add": BaseMaterial3D.BLEND_MODE_ADD,
|
|
"sub": BaseMaterial3D.BLEND_MODE_SUB,
|
|
"mul": BaseMaterial3D.BLEND_MODE_MUL,
|
|
},
|
|
"cull_mode": {
|
|
"back": BaseMaterial3D.CULL_BACK,
|
|
"front": BaseMaterial3D.CULL_FRONT,
|
|
"disabled": BaseMaterial3D.CULL_DISABLED,
|
|
},
|
|
"depth_draw_mode": {
|
|
"opaque_only": BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY,
|
|
"always": BaseMaterial3D.DEPTH_DRAW_ALWAYS,
|
|
"disabled": BaseMaterial3D.DEPTH_DRAW_DISABLED,
|
|
},
|
|
"diffuse_mode": {
|
|
"burley": BaseMaterial3D.DIFFUSE_BURLEY,
|
|
"lambert": BaseMaterial3D.DIFFUSE_LAMBERT,
|
|
"lambert_wrap": BaseMaterial3D.DIFFUSE_LAMBERT_WRAP,
|
|
"toon": BaseMaterial3D.DIFFUSE_TOON,
|
|
},
|
|
"specular_mode": {
|
|
"schlick_ggx": BaseMaterial3D.SPECULAR_SCHLICK_GGX,
|
|
"toon": BaseMaterial3D.SPECULAR_TOON,
|
|
"disabled": BaseMaterial3D.SPECULAR_DISABLED,
|
|
},
|
|
"billboard_mode": {
|
|
"disabled": BaseMaterial3D.BILLBOARD_DISABLED,
|
|
"enabled": BaseMaterial3D.BILLBOARD_ENABLED,
|
|
"fixed_y": BaseMaterial3D.BILLBOARD_FIXED_Y,
|
|
"particles": BaseMaterial3D.BILLBOARD_PARTICLES,
|
|
},
|
|
"texture_filter": {
|
|
"nearest": BaseMaterial3D.TEXTURE_FILTER_NEAREST,
|
|
"linear": BaseMaterial3D.TEXTURE_FILTER_LINEAR,
|
|
"nearest_mipmap": BaseMaterial3D.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS,
|
|
"linear_mipmap": BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS,
|
|
},
|
|
}
|
|
|
|
|
|
## Return the enum int for (property, string_name), or null if not a known enum string.
|
|
static func resolve_enum(property: String, value: Variant) -> Variant:
|
|
if not (value is String):
|
|
return null
|
|
if not _ENUM_TABLES.has(property):
|
|
return null
|
|
var table: Dictionary = _ENUM_TABLES[property]
|
|
var key: String = String(value).to_lower()
|
|
if table.has(key):
|
|
return table[key]
|
|
return null
|
|
|
|
|
|
## Parse a color from Color, "#rrggbb", "#rrggbbaa", named (red/blue/...) or dict.
|
|
## Returns null if the input cannot be parsed.
|
|
static func parse_color(value: Variant) -> Variant:
|
|
if value is Color:
|
|
return value
|
|
if value is String:
|
|
var s: String = value
|
|
var sentinel_a := Color(0, 0, 0, 0)
|
|
var sentinel_b := Color(1, 1, 1, 1)
|
|
var a := Color.from_string(s, sentinel_a)
|
|
var b := Color.from_string(s, sentinel_b)
|
|
if a != b:
|
|
return null
|
|
return a
|
|
if value is Dictionary:
|
|
var d: Dictionary = value
|
|
if d.has("r") and d.has("g") and d.has("b"):
|
|
return Color(float(d.r), float(d.g), float(d.b), float(d.get("a", 1.0)))
|
|
if value is Array and value.size() >= 3:
|
|
var arr: Array = value
|
|
var alpha := float(arr[3]) if arr.size() >= 4 else 1.0
|
|
return Color(float(arr[0]), float(arr[1]), float(arr[2]), alpha)
|
|
return null
|
|
|
|
|
|
static func parse_vector3(value: Variant) -> Variant:
|
|
if value is Vector3:
|
|
return value
|
|
if value is Dictionary:
|
|
var d: Dictionary = value
|
|
return Vector3(float(d.get("x", 0)), float(d.get("y", 0)), float(d.get("z", 0)))
|
|
if value is Array and value.size() >= 3:
|
|
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
|
return null
|
|
|
|
|
|
static func parse_vector2(value: Variant) -> Variant:
|
|
if value is Vector2:
|
|
return value
|
|
if value is Dictionary:
|
|
var d: Dictionary = value
|
|
return Vector2(float(d.get("x", 0)), float(d.get("y", 0)))
|
|
if value is Array and value.size() >= 2:
|
|
return Vector2(float(value[0]), float(value[1]))
|
|
return null
|
|
|
|
|
|
## Parse a {stops: [{time, color}]} gradient dict into a Gradient resource.
|
|
static func parse_gradient(value: Variant) -> Variant:
|
|
if value is Gradient:
|
|
return value
|
|
if not (value is Dictionary):
|
|
return null
|
|
var d: Dictionary = value
|
|
if not d.has("stops"):
|
|
return null
|
|
var stops_array = d.get("stops")
|
|
if not (stops_array is Array):
|
|
return null
|
|
var offsets: PackedFloat32Array = PackedFloat32Array()
|
|
var colors: PackedColorArray = PackedColorArray()
|
|
for stop in stops_array:
|
|
if not (stop is Dictionary):
|
|
return null
|
|
var t := float(stop.get("time", 0.0))
|
|
var c = parse_color(stop.get("color"))
|
|
if c == null:
|
|
return null
|
|
offsets.append(t)
|
|
colors.append(c)
|
|
var grad := Gradient.new()
|
|
grad.offsets = offsets
|
|
grad.colors = colors
|
|
return grad
|
|
|
|
|
|
## Load a Texture2D from a res:// / uid:// / user:// path (validate_loadable_path).
|
|
## Returns null on failure (including a path that fails confinement / traversal).
|
|
static func load_texture(path: String) -> Texture2D:
|
|
if not McpPathValidator.validate_loadable_path(path).is_empty():
|
|
return null
|
|
if not ResourceLoader.exists(path):
|
|
return null
|
|
var res := ResourceLoader.load(path)
|
|
if res is Texture2D:
|
|
return res
|
|
return null
|
|
|
|
|
|
## Coerce a JSON-shaped value for a material property.
|
|
## Returns a dict {ok: true, value: ...} on success, or {ok: false, error: "..."} on failure.
|
|
## For properties the coercer doesn't have special logic for, falls back to target_type.
|
|
static func coerce_material_value(property: String, value: Variant, target_type: int) -> Dictionary:
|
|
# Enum-by-name: must match before generic TYPE_INT coercion.
|
|
if _ENUM_TABLES.has(property):
|
|
if value is String:
|
|
var enum_val = resolve_enum(property, value)
|
|
if enum_val == null:
|
|
return {
|
|
"ok": false,
|
|
"error": "Invalid %s value: '%s'. Valid: %s" % [
|
|
property, value, ", ".join(_ENUM_TABLES[property].keys())
|
|
],
|
|
}
|
|
return {"ok": true, "value": int(enum_val)}
|
|
if value is int or value is float:
|
|
return {"ok": true, "value": int(value)}
|
|
|
|
match target_type:
|
|
TYPE_COLOR:
|
|
var c = parse_color(value)
|
|
if c == null:
|
|
return {"ok": false, "error": "Invalid color for %s: %s" % [property, value]}
|
|
return {"ok": true, "value": c}
|
|
TYPE_VECTOR3:
|
|
var v3 = parse_vector3(value)
|
|
if v3 == null:
|
|
return {"ok": false, "error": "Invalid vector3 for %s: %s" % [property, value]}
|
|
return {"ok": true, "value": v3}
|
|
TYPE_VECTOR2:
|
|
var v2 = parse_vector2(value)
|
|
if v2 == null:
|
|
return {"ok": false, "error": "Invalid vector2 for %s: %s" % [property, value]}
|
|
return {"ok": true, "value": v2}
|
|
TYPE_BOOL:
|
|
if value is bool:
|
|
return {"ok": true, "value": value}
|
|
if value is int or value is float:
|
|
return {"ok": true, "value": bool(value)}
|
|
return {"ok": false, "error": "Expected bool for %s" % property}
|
|
TYPE_INT:
|
|
if value is int:
|
|
return {"ok": true, "value": value}
|
|
if value is float:
|
|
return {"ok": true, "value": int(value)}
|
|
return {"ok": false, "error": "Expected int for %s" % property}
|
|
TYPE_FLOAT:
|
|
if value is float:
|
|
return {"ok": true, "value": value}
|
|
if value is int:
|
|
return {"ok": true, "value": float(value)}
|
|
return {"ok": false, "error": "Expected number for %s" % property}
|
|
TYPE_OBJECT:
|
|
if value == null:
|
|
return {"ok": true, "value": null}
|
|
if value is Object:
|
|
return {"ok": true, "value": value}
|
|
if value is String:
|
|
var tex := load_texture(value)
|
|
if tex == null:
|
|
return {"ok": false, "error": "Resource not found or wrong type: %s" % value}
|
|
return {"ok": true, "value": tex}
|
|
return {"ok": false, "error": "Expected resource path (string) for %s" % property}
|
|
TYPE_STRING:
|
|
return {"ok": true, "value": String(value)}
|
|
|
|
# Unknown target type — pass through.
|
|
return {"ok": true, "value": value}
|
|
|
|
|
|
## Serialize a Variant into JSON-friendly shape for responses.
|
|
static func serialize_value(value: Variant) -> Variant:
|
|
if value == null:
|
|
return null
|
|
if value is Color:
|
|
return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
if value is Vector3:
|
|
return {"x": value.x, "y": value.y, "z": value.z}
|
|
if value is Vector2:
|
|
return {"x": value.x, "y": value.y}
|
|
if value is Resource:
|
|
var path := (value as Resource).resource_path
|
|
if path.is_empty():
|
|
return {"type": value.get_class(), "path": ""}
|
|
return {"type": value.get_class(), "path": path}
|
|
return value
|