Replace dasher-pack with unified animation-pack using original Blender bone names
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user