@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(aa)", named string, {r,g,b[,a]} dict, ## or [r,g,b(,a)] array. Delegates to the canonical parser (#714); returns ## null if the input cannot be parsed. static func parse_color(value: Variant) -> Variant: return McpJsonValues.parse_color(value) static func parse_vector3(value: Variant) -> Variant: return McpJsonValues.parse_vector3(value) static func parse_vector2(value: Variant) -> Variant: return McpJsonValues.parse_vector2(value) ## 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