@tool extends RefCounted const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd") ## Creates procedural textures — GradientTexture2D (wrapping a Gradient) ## and NoiseTexture2D (wrapping a FastNoiseLite). Assigns to a node slot ## (undoable, bundles sub-resources) or saves to a .tres file. const NodeHandler := preload("res://addons/godot_ai/handlers/node_handler.gd") var _undo_redo: EditorUndoRedoManager var _connection: McpConnection func _init(undo_redo: EditorUndoRedoManager, connection: McpConnection = null) -> void: _undo_redo = undo_redo _connection = connection const _FILL_MODES := { "linear": GradientTexture2D.FILL_LINEAR, "radial": GradientTexture2D.FILL_RADIAL, "square": GradientTexture2D.FILL_SQUARE, } const _NOISE_TYPES := { "simplex": FastNoiseLite.TYPE_SIMPLEX, "simplex_smooth": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "perlin": FastNoiseLite.TYPE_PERLIN, "cellular": FastNoiseLite.TYPE_CELLULAR, "value": FastNoiseLite.TYPE_VALUE, "value_cubic": FastNoiseLite.TYPE_VALUE_CUBIC, } # ============================================================================ # gradient_texture_create # ============================================================================ func create_gradient_texture(params: Dictionary) -> Dictionary: var stops: Array = params.get("stops", []) var width: int = params.get("width", 256) var height: int = params.get("height", 1) var fill: String = params.get("fill", "linear") if stops.size() < 2: return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "gradient_texture_create requires at least 2 stops, got %d" % stops.size() ) if not _FILL_MODES.has(fill): return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid fill '%s'. Valid: %s" % [fill, ", ".join(_FILL_MODES.keys())] ) var home_err := McpResourceIO.validate_home(params) if home_err != null: return home_err var gradient := Gradient.new() var offsets := PackedFloat32Array() var colors := PackedColorArray() for i in range(stops.size()): var stop = stops[i] if not stop is Dictionary: return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "stops[%d] must be a dict with 'offset' and 'color' keys" % i ) if not stop.has("offset") or not stop.has("color"): return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "stops[%d] missing 'offset' or 'color' key" % i ) offsets.append(float(stop["offset"])) var color_value = NodeHandler._coerce_value(stop["color"], TYPE_COLOR) var color_err := NodeHandler._check_coerced(color_value, TYPE_COLOR, "stops[%d].color" % i) if color_err != null: return color_err colors.append(color_value) gradient.offsets = offsets gradient.colors = colors var tex := GradientTexture2D.new() tex.gradient = gradient tex.width = width tex.height = height tex.fill = _FILL_MODES[fill] return _finalize(tex, [gradient], params, "Gradient texture", { "texture_class": "GradientTexture2D", "gradient_class": "Gradient", "stop_count": stops.size(), "fill": fill, }) # ============================================================================ # noise_texture_create # ============================================================================ func create_noise_texture(params: Dictionary) -> Dictionary: var noise_type: String = params.get("noise_type", "simplex_smooth") var width: int = params.get("width", 512) var height: int = params.get("height", 512) var frequency: float = params.get("frequency", 0.01) var seed_value: int = params.get("seed", 0) var fractal_octaves: int = params.get("fractal_octaves", 0) # 0 = leave default if not _NOISE_TYPES.has(noise_type): return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid noise_type '%s'. Valid: %s" % [noise_type, ", ".join(_NOISE_TYPES.keys())] ) var home_err := McpResourceIO.validate_home(params) if home_err != null: return home_err var noise := FastNoiseLite.new() noise.noise_type = _NOISE_TYPES[noise_type] noise.frequency = frequency noise.seed = seed_value if fractal_octaves > 0: noise.fractal_octaves = fractal_octaves var tex := NoiseTexture2D.new() tex.noise = noise tex.width = width tex.height = height return _finalize(tex, [noise], params, "Noise texture", { "texture_class": "NoiseTexture2D", "noise_class": "FastNoiseLite", "noise_type": noise_type, }) # ============================================================================ # shared helpers # ============================================================================ func _finalize(tex: Resource, sub_resources: Array, params: Dictionary, label: String, extra: Dictionary) -> Dictionary: var node_path: String = params.get("path", "") var property: String = params.get("property", "") var resource_path: String = params.get("resource_path", "") var overwrite: bool = params.get("overwrite", false) if not resource_path.is_empty(): return McpResourceIO.save_to_disk(tex, resource_path, overwrite, label, extra, _connection) return _assign_texture(tex, sub_resources, node_path, property, label, extra) func _assign_texture(tex: Resource, sub_resources: Array, node_path: String, property: String, label: String, extra: Dictionary) -> Dictionary: 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 found := false var prop_type: int = TYPE_NIL for prop in node.get_property_list(): if prop.name == property: found = true prop_type = prop.get("type", TYPE_NIL) break if not found: return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, "Property '%s' not found on %s" % [property, node.get_class()] ) if prop_type != TYPE_NIL and prop_type != TYPE_OBJECT: return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, "Property '%s' on %s is not an Object slot" % [property, node.get_class()] ) var old_value = node.get(property) _undo_redo.create_action("MCP: Create %s for %s.%s" % [label, node.name, property]) _undo_redo.add_do_property(node, property, tex) _undo_redo.add_undo_property(node, property, old_value) _undo_redo.add_do_reference(tex) for sub in sub_resources: _undo_redo.add_do_reference(sub) _undo_redo.commit_action() var data := { "path": node_path, "property": property, "undoable": true, } data.merge(extra) return {"data": data}