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

200 lines
6.3 KiB
GDScript

@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}