Files
tekton/addons/godot_ai/handlers/material_handler.gd
T

789 lines
27 KiB
GDScript

@tool
extends RefCounted
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
## Handles Material authoring: creating .tres files, setting BaseMaterial3D
## properties / shader uniforms, assigning to nodes, high-level presets.
##
## File-resource lifecycle mirrors ThemeHandler (create/load/mutate/save).
## Undo pattern mirrors AnimationHandler (single create_action bundles
## every dependency spawn).
const MaterialValues := preload("res://addons/godot_ai/handlers/material_values.gd")
const MaterialPresets := preload("res://addons/godot_ai/handlers/material_presets.gd")
const _TYPE_TO_CLASS := {
"standard": "StandardMaterial3D",
"orm": "ORMMaterial3D",
"canvas_item": "CanvasItemMaterial",
"shader": "ShaderMaterial",
}
const _SUPPORTED_SUFFIXES := [".tres", ".material", ".res"]
var _undo_redo: EditorUndoRedoManager
func _init(undo_redo: EditorUndoRedoManager) -> void:
_undo_redo = undo_redo
# ============================================================================
# material_create
# ============================================================================
func create_material(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var type_str: String = params.get("type", "standard")
var shader_path: String = params.get("shader_path", "")
var overwrite: bool = params.get("overwrite", false)
var err := _validate_material_path(path, "path", true)
if err != null:
return err
if not _TYPE_TO_CLASS.has(type_str):
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())]
)
var existed_before := FileAccess.file_exists(path)
if existed_before and not overwrite:
return ErrorCodes.make(
ErrorCodes.INVALID_PARAMS,
"Material already exists at %s (pass overwrite=true to replace)" % path
)
var mat := _instantiate_material(type_str)
if mat == null:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate material")
if type_str == "shader":
if shader_path.is_empty():
return ErrorCodes.make(
ErrorCodes.INVALID_PARAMS,
"ShaderMaterial requires shader_path (res:// / uid:// / user:// path to a .gdshader)"
)
var shader_path_err = McpPathValidator.loadable_error(shader_path, "shader_path")
if shader_path_err != null:
return shader_path_err
if not ResourceLoader.exists(shader_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Shader not found: %s" % shader_path)
var shader_res := ResourceLoader.load(shader_path)
if not (shader_res is Shader):
return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Shader" % shader_path)
(mat as ShaderMaterial).shader = shader_res
var dir_path := path.get_base_dir()
var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
return ErrorCodes.make(
ErrorCodes.INTERNAL_ERROR,
"Failed to create directory: %s (error %d)" % [dir_path, mkdir_err]
)
var save_err := ResourceSaver.save(mat, path)
if save_err != OK:
return ErrorCodes.make(
ErrorCodes.INTERNAL_ERROR,
"Failed to save material to %s (error %d)" % [path, save_err]
)
var efs := EditorInterface.get_resource_filesystem()
if efs != null:
efs.update_file(path)
return {
"data": {
"path": path,
"type": type_str,
"class": mat.get_class(),
"shader_path": shader_path,
"overwritten": existed_before,
"undoable": false,
"reason": "File creation is persistent; delete the file manually to revert",
}
}
# ============================================================================
# material_set_param
# ============================================================================
func set_param(params: Dictionary) -> Dictionary:
var load_result := _load_material_from_path(params.get("path", ""), true)
if load_result.has("error"):
return load_result
var mat: Material = load_result.material
var mat_path: String = load_result.path
var property: String = params.get("param", "")
if property.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param")
if not ("value" in params):
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
var raw_value = params.get("value")
# Probe the property. We allow any property present in get_property_list,
# plus `shader` on ShaderMaterial.
var prop_type: int = TYPE_NIL
var property_exists := false
for prop in mat.get_property_list():
if prop.name == property:
property_exists = true
prop_type = prop.get("type", TYPE_NIL)
break
if not property_exists:
return ErrorCodes.make(
ErrorCodes.PROPERTY_NOT_ON_CLASS,
McpPropertyErrors.build_message(mat, property)
)
var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type)
if not coerced.ok:
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
var new_value = coerced.value
var old_value = mat.get(property)
_undo_redo.create_action("MCP: Set material %s.%s" % [mat_path.get_file(), property])
_undo_redo.add_do_method(self, "_apply_param", mat_path, property, new_value, false)
_undo_redo.add_undo_method(self, "_apply_param", mat_path, property, old_value, false)
_undo_redo.commit_action()
return {
"data": {
"path": mat_path,
"property": property,
"value": MaterialValues.serialize_value(new_value),
"previous_value": MaterialValues.serialize_value(old_value),
"undoable": true,
}
}
# ============================================================================
# material_set_shader_param
# ============================================================================
func set_shader_param(params: Dictionary) -> Dictionary:
var load_result := _load_material_from_path(params.get("path", ""), true)
if load_result.has("error"):
return load_result
var mat: Material = load_result.material
var mat_path: String = load_result.path
if not (mat is ShaderMaterial):
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"Material at %s is %s, not ShaderMaterial" % [mat_path, mat.get_class()]
)
var shader_mat := mat as ShaderMaterial
if shader_mat.shader == null:
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"ShaderMaterial at %s has no shader assigned" % mat_path
)
var param_name: String = params.get("param", "")
if param_name.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param")
if not ("value" in params):
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value")
# Verify the uniform exists in the shader.
var uniform_type := _shader_uniform_type(shader_mat.shader, param_name)
if uniform_type == TYPE_NIL:
return ErrorCodes.make(
ErrorCodes.PROPERTY_NOT_ON_CLASS,
"Shader uniform '%s' not declared on shader at %s" % [param_name, shader_mat.shader.resource_path]
)
var raw_value = params.get("value")
var coerced := MaterialValues.coerce_material_value(param_name, raw_value, uniform_type)
if not coerced.ok:
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
var new_value = coerced.value
var old_value = shader_mat.get_shader_parameter(param_name)
_undo_redo.create_action("MCP: Set shader param %s.%s" % [mat_path.get_file(), param_name])
_undo_redo.add_do_method(self, "_apply_shader_param", mat_path, param_name, new_value)
_undo_redo.add_undo_method(self, "_apply_shader_param", mat_path, param_name, old_value)
_undo_redo.commit_action()
return {
"data": {
"path": mat_path,
"param": param_name,
"value": MaterialValues.serialize_value(new_value),
"previous_value": MaterialValues.serialize_value(old_value),
"undoable": true,
}
}
# ============================================================================
# material_get
# ============================================================================
func get_material(params: Dictionary) -> Dictionary:
var load_result := _load_material_from_path(params.get("path", ""))
if load_result.has("error"):
return load_result
var mat: Material = load_result.material
var mat_path: String = load_result.path
var properties: Array[Dictionary] = []
for prop in mat.get_property_list():
var usage: int = prop.get("usage", 0)
if not (usage & PROPERTY_USAGE_EDITOR):
continue
var name: String = prop.name
if name.begins_with("shader_parameter/"):
continue # handled below
var value = mat.get(name)
if value == null and prop.type != TYPE_NIL:
continue
properties.append({
"name": name,
"type": type_string(prop.type),
"value": MaterialValues.serialize_value(value),
})
var shader_params: Array[Dictionary] = []
if mat is ShaderMaterial:
var shader_mat := mat as ShaderMaterial
if shader_mat.shader != null:
for u in shader_mat.shader.get_shader_uniform_list():
var u_name: String = u.get("name", "")
if u_name.is_empty():
continue
shader_params.append({
"name": u_name,
"type": type_string(u.get("type", TYPE_NIL)),
"value": MaterialValues.serialize_value(shader_mat.get_shader_parameter(u_name)),
})
var reverse_type_map := _reverse_type_map()
var shader_path_str := ""
if mat is ShaderMaterial:
var sm := mat as ShaderMaterial
if sm.shader != null:
shader_path_str = sm.shader.resource_path
return {
"data": {
"path": mat_path,
"class": mat.get_class(),
"type": reverse_type_map.get(mat.get_class(), ""),
"properties": properties,
"property_count": properties.size(),
"shader_parameters": shader_params,
"shader_path": shader_path_str,
}
}
# ============================================================================
# material_list
# ============================================================================
func list_materials(params: Dictionary) -> Dictionary:
var root: String = params.get("root", "res://")
var type_filter: String = params.get("type", "")
var root_err = McpPathValidator.path_error(root, "root")
if root_err != null:
return root_err
var efs := EditorInterface.get_resource_filesystem()
if efs == null:
return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "EditorFileSystem not available")
var results: Array[Dictionary] = []
var start_dir := efs.get_filesystem_path(root)
if start_dir == null:
start_dir = efs.get_filesystem()
_scan_materials(start_dir, type_filter, root, results)
return {"data": {"materials": results, "count": results.size()}}
func _scan_materials(dir: EditorFileSystemDirectory, type_filter: String, root: String, out: Array[Dictionary]) -> void:
if dir == null:
return
for i in dir.get_file_count():
var file_path := dir.get_file_path(i)
if not file_path.begins_with(root):
continue
var file_type := dir.get_file_type(i)
var is_material := file_type == "Material" or ClassDB.is_parent_class(file_type, "Material")
if not is_material:
# Some material variants serialize as specific classes.
if not (file_type in _TYPE_TO_CLASS.values()):
continue
if not type_filter.is_empty():
if file_type != type_filter and not ClassDB.is_parent_class(file_type, type_filter):
continue
out.append({"path": file_path, "class": file_type})
for i in dir.get_subdir_count():
_scan_materials(dir.get_subdir(i), type_filter, root, out)
# ============================================================================
# material_assign
# ============================================================================
func assign_material(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
if node_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
if _resolved.has("error"):
return _resolved
var node: Node = _resolved.node
var _scene_root: Node = _resolved.scene_root
var slot: String = params.get("slot", "override")
var resource_path: String = params.get("resource_path", "")
var create_if_missing: bool = params.get("create_if_missing", false)
var type_str: String = params.get("type", "standard")
var slot_result := _resolve_slot_property(node, slot)
if slot_result.has("error"):
return slot_result
var property: String = slot_result.property
# Load or create the material.
var mat: Material = null
var material_created := false
if not resource_path.is_empty():
var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
if rpath_err != null:
return rpath_err
if not ResourceLoader.exists(resource_path):
if create_if_missing:
# We'd need to create a new file here — refuse; callers should
# use material_create first or omit resource_path to get an
# inline material.
return ErrorCodes.make(
ErrorCodes.RESOURCE_NOT_FOUND,
"Resource not found: %s. Create it first with material_create or omit resource_path for an inline material." % resource_path
)
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
var loaded := ResourceLoader.load(resource_path)
if not (loaded is Material):
var loaded_class := "null"
if loaded != null:
loaded_class = loaded.get_class()
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"Resource at %s is not a Material (got %s)" % [resource_path, loaded_class]
)
mat = loaded
else:
if not create_if_missing:
return ErrorCodes.make(
ErrorCodes.INVALID_PARAMS,
"Missing resource_path (pass create_if_missing=true to create a new inline material)"
)
if not _TYPE_TO_CLASS.has(type_str):
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"Invalid material type '%s'" % type_str
)
mat = _instantiate_material(type_str)
material_created = true
var old_value = node.get(property)
_undo_redo.create_action("MCP: Assign material to %s.%s" % [node.name, property])
_undo_redo.add_do_property(node, property, mat)
_undo_redo.add_undo_property(node, property, old_value)
if material_created:
_undo_redo.add_do_reference(mat)
_undo_redo.commit_action()
return {
"data": {
"node_path": node_path,
"property": property,
"slot": slot,
"resource_path": resource_path,
"material_class": mat.get_class(),
"material_created": material_created,
"undoable": true,
}
}
# ============================================================================
# material_apply_to_node
# ============================================================================
func apply_to_node(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
if node_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
var type_str: String = params.get("type", "standard")
if not _TYPE_TO_CLASS.has(type_str):
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())]
)
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
if _resolved.has("error"):
return _resolved
var node: Node = _resolved.node
var _scene_root: Node = _resolved.scene_root
var slot: String = params.get("slot", "override")
var slot_result := _resolve_slot_property(node, slot)
if slot_result.has("error"):
return slot_result
var property: String = slot_result.property
var mat := _instantiate_material(type_str)
var props_to_set: Dictionary = params.get("params", {})
var applied: Array[String] = []
for prop_name in props_to_set:
var apply_err := _apply_one_param_on_instance(mat, String(prop_name), props_to_set[prop_name])
if apply_err != null:
return apply_err
applied.append(String(prop_name))
var save_to: String = params.get("save_to", "")
var saved := false
if not save_to.is_empty():
var save_err_validation := _validate_material_path(save_to, "save_to", true)
if save_err_validation != null:
return save_err_validation
var dir_path := save_to.get_base_dir()
var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
var save_err := ResourceSaver.save(mat, save_to)
if save_err != OK:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material to %s (error %d)" % [save_to, save_err])
var efs := EditorInterface.get_resource_filesystem()
if efs != null:
efs.update_file(save_to)
# Prefer the on-disk reference (keeps the scene ref small), but fall
# back to the in-memory material if the reload fails — otherwise a null
# would clear the slot and crash mat.get_class() below.
var reloaded := ResourceLoader.load(save_to)
if reloaded != null:
mat = reloaded
saved = true
var old_value = node.get(property)
_undo_redo.create_action("MCP: Apply %s material to %s" % [type_str, node.name])
_undo_redo.add_do_property(node, property, mat)
_undo_redo.add_undo_property(node, property, old_value)
_undo_redo.add_do_reference(mat)
_undo_redo.commit_action()
return {
"data": {
"node_path": node_path,
"property": property,
"slot": slot,
"type": type_str,
"class": mat.get_class(),
"applied_params": applied,
"material_created": true,
"saved_to": save_to if saved else "",
"undoable": true,
}
}
# ============================================================================
# material_apply_preset
# ============================================================================
func apply_preset(params: Dictionary) -> Dictionary:
var preset_name: String = params.get("preset", "")
if preset_name.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset")
var overrides: Dictionary = params.get("overrides", {})
var blueprint = MaterialPresets.build(preset_name, overrides)
if blueprint == null:
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(MaterialPresets.list())]
)
var type_str: String = blueprint.get("type", "standard")
var preset_params: Dictionary = blueprint.get("params", {})
var path: String = params.get("path", "")
var node_path: String = params.get("node_path", "")
if path.is_empty() and node_path.is_empty():
return ErrorCodes.make(
ErrorCodes.MISSING_REQUIRED_PARAM,
"Pass at least one of: path (save to disk), node_path (assign to node)"
)
# If both path and node_path, save to disk, then assign the saved resource.
# If only path, save to disk.
# If only node_path, inline material via apply_to_node.
if not node_path.is_empty() and path.is_empty():
# Inline
var inline_result := apply_to_node({
"node_path": node_path,
"type": type_str,
"params": preset_params,
"slot": params.get("slot", "override"),
})
if inline_result.has("data"):
inline_result.data["preset"] = preset_name
inline_result.data["assigned"] = true
inline_result.data["path"] = ""
inline_result.data["saved_to_disk"] = false
inline_result.data["reason"] = "Inline material assigned to node"
return inline_result
# Save-to-disk path.
var existed_before := FileAccess.file_exists(path)
if existed_before and not params.get("overwrite", false):
return ErrorCodes.make(
ErrorCodes.INVALID_PARAMS,
"Material already exists at %s (pass overwrite=true to replace)" % path
)
var path_err := _validate_material_path(path, "path", true)
if path_err != null:
return path_err
var mat := _instantiate_material(type_str)
for prop_name in preset_params:
var apply_err := _apply_one_param_on_instance(mat, String(prop_name), preset_params[prop_name])
if apply_err != null:
return apply_err
var dir_path := path.get_base_dir()
var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path)
if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path)
var save_err := ResourceSaver.save(mat, path)
if save_err != OK:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material: %s" % path)
var efs := EditorInterface.get_resource_filesystem()
if efs != null:
efs.update_file(path)
var assigned := false
if not node_path.is_empty():
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
if _resolved.has("error"):
return _resolved
var node: Node = _resolved.node
var _scene_root: Node = _resolved.scene_root
var slot_result := _resolve_slot_property(node, params.get("slot", "override"))
if slot_result.has("error"):
return slot_result
var property: String = slot_result.property
var saved_mat := ResourceLoader.load(path)
var old_value = node.get(property)
_undo_redo.create_action("MCP: Apply preset %s to %s" % [preset_name, node.name])
_undo_redo.add_do_property(node, property, saved_mat)
_undo_redo.add_undo_property(node, property, old_value)
_undo_redo.commit_action()
assigned = true
return {
"data": {
"preset": preset_name,
"type": type_str,
"path": path,
"node_path": node_path,
"material_created": true,
"assigned": assigned,
"saved_to_disk": true,
"undoable": assigned, # assign is undoable; save is not
"reason": "" if assigned else "File save is not undoable",
}
}
# ============================================================================
# Undo-callable: applies a param on the loaded resource and saves.
# ============================================================================
func _apply_param(mat_path: String, property: String, value: Variant, _is_shader: bool) -> void:
var mat: Material = ResourceLoader.load(mat_path)
if mat == null:
return
mat.set(property, value)
ResourceSaver.save(mat, mat_path)
func _apply_shader_param(mat_path: String, param_name: String, value: Variant) -> void:
var mat: Material = ResourceLoader.load(mat_path)
if mat == null or not (mat is ShaderMaterial):
return
(mat as ShaderMaterial).set_shader_parameter(param_name, value)
ResourceSaver.save(mat, mat_path)
# ============================================================================
# Helpers
# ============================================================================
static func _instantiate_material(type_str: String) -> Material:
match type_str:
"standard":
return StandardMaterial3D.new()
"orm":
return ORMMaterial3D.new()
"canvas_item":
return CanvasItemMaterial.new()
"shader":
return ShaderMaterial.new()
return null
static func _reverse_type_map() -> Dictionary:
var out := {}
for k in _TYPE_TO_CLASS:
out[_TYPE_TO_CLASS[k]] = k
return out
static func _validate_material_path(path: String, param_name: String, for_write: bool = false) -> Variant:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
var path_err := McpPathValidator.validate_resource_path(path, for_write)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, path_err])
var has_suffix := false
for s in _SUPPORTED_SUFFIXES:
if path.ends_with(s):
has_suffix = true
break
if not has_suffix:
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"%s must end with one of %s (got %s)" % [param_name, ", ".join(_SUPPORTED_SUFFIXES), path]
)
return null
func _load_material_from_path(path: String, for_write: bool = false) -> Dictionary:
var err := _validate_material_path(path, "path", for_write)
if err != null:
return err
if not ResourceLoader.exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % path)
var res := ResourceLoader.load(path)
if res == null or not (res is Material):
return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material" % path)
return {"material": res, "path": path}
## Map a slot name to a Godot property name on the given node.
## Returns {property: "..."} or an error dict.
func _resolve_slot_property(node: Node, slot: String) -> Dictionary:
if slot == "override":
if node is MeshInstance3D or node is CSGShape3D:
return {"property": "material_override"}
if node is CanvasItem:
return {"property": "material"}
if node is GPUParticles3D or node is GPUParticles2D or node is CPUParticles3D or node is CPUParticles2D:
return {"property": "material_override"} if node is GeometryInstance3D else {"property": "material"}
return ErrorCodes.make(
ErrorCodes.PROPERTY_NOT_ON_CLASS,
"Slot 'override' not supported on %s" % node.get_class()
)
if slot == "canvas":
if node is CanvasItem:
return {"property": "material"}
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"Slot 'canvas' requires a CanvasItem (got %s)" % node.get_class()
)
if slot == "process":
if node is GPUParticles3D or node is GPUParticles2D:
return {"property": "process_material"}
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"Slot 'process' requires a GPUParticles2D/3D (got %s)" % node.get_class()
)
if slot.begins_with("surface_"):
if not (node is MeshInstance3D):
return ErrorCodes.make(
ErrorCodes.WRONG_TYPE,
"Slot '%s' requires a MeshInstance3D (got %s)" % [slot, node.get_class()]
)
var idx_str := slot.substr(len("surface_"))
if not idx_str.is_valid_int():
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid surface slot: %s" % slot)
var idx := int(idx_str)
var mi := node as MeshInstance3D
var surf_count := mi.mesh.get_surface_count() if mi.mesh != null else 0
if idx < 0 or idx >= surf_count:
return ErrorCodes.make(
ErrorCodes.INVALID_PARAMS,
"Surface index %d out of range (mesh has %d surfaces)" % [idx, surf_count]
)
return {"property": "surface_material_override/%d" % idx}
return ErrorCodes.make(
ErrorCodes.VALUE_OUT_OF_RANGE,
"Unknown slot '%s'. Valid: override, canvas, process, surface_N" % slot
)
## Apply one property to an in-memory material instance; returns null on
## success or an error dict on failure.
func _apply_one_param_on_instance(mat: Material, property: String, raw_value: Variant) -> Variant:
var prop_type: int = TYPE_NIL
var property_exists := false
for prop in mat.get_property_list():
if prop.name == property:
property_exists = true
prop_type = prop.get("type", TYPE_NIL)
break
if not property_exists:
return ErrorCodes.make(
ErrorCodes.PROPERTY_NOT_ON_CLASS,
McpPropertyErrors.build_message(mat, property)
)
var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type)
if not coerced.ok:
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error))
mat.set(property, coerced.value)
return null
## Inspect a shader to get the Variant type of a uniform. Returns TYPE_NIL if
## the uniform is not declared.
static func _shader_uniform_type(shader: Shader, name: String) -> int:
if shader == null:
return TYPE_NIL
for u in shader.get_shader_uniform_list():
if u.get("name", "") == name:
return int(u.get("type", TYPE_NIL))
return TYPE_NIL