更新godot ai 到最新版本
This commit is contained in:
@@ -13,6 +13,31 @@ const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
||||
|
||||
const _COLOR_HINT := "expected hex #rrggbb, named color, or {r,g,b,a} dict"
|
||||
|
||||
## Theme constants/font sizes and StyleBoxFlat int properties are 32-bit in the
|
||||
## engine; GDScript ints are 64-bit, so an unvalidated 4294967296 narrows to 0
|
||||
## (and 2147483648 to -2147483648) at the property assignment.
|
||||
const _INT32_MIN := -2147483648
|
||||
const _INT32_MAX := 2147483647
|
||||
|
||||
## Native storage kinds for numeric theme destinations, so validation models
|
||||
## what the engine actually keeps:
|
||||
## _NATIVE_INT32 C++ int32 (Theme constants/font sizes, shadow_size)
|
||||
## _NATIVE_FLOAT32 C++ real_t (margins, shadow offsets, Color components)
|
||||
## _NATIVE_INT32_FLOAT32 int accessors backed by real_t storage: StyleBoxFlat
|
||||
## border widths and corner radii take ints but store
|
||||
## real_t, so 16777217 rounds to 16777216 and
|
||||
## 2147483647 reads back as -2147483648
|
||||
const _NATIVE_INT32 := 0
|
||||
const _NATIVE_FLOAT32 := 1
|
||||
const _NATIVE_INT32_FLOAT32 := 2
|
||||
|
||||
## Top-level keys of the StyleBoxFlat patch vocabulary shared by
|
||||
## set_stylebox_flat and stylebox_override. Nested dicts validate their own
|
||||
## keys inside _apply_flat_props; this guards the outer dict so a typo
|
||||
## (`bg_colour`) cannot install an unchanged override plus an undo entry while
|
||||
## reporting success.
|
||||
const _FLAT_PATCH_KEYS := ["bg_color", "border_color", "border", "corners", "margins", "shadow", "anti_aliasing"]
|
||||
|
||||
var _undo_redo: EditorUndoRedoManager
|
||||
var _connection: McpConnection
|
||||
|
||||
@@ -98,7 +123,7 @@ func set_constant(params: Dictionary) -> Dictionary:
|
||||
func(theme, name, cls, val): theme.set_constant(name, cls, int(val)),
|
||||
func(theme, name, cls): theme.clear_constant(name, cls),
|
||||
func(theme, name, cls): return theme.has_constant(name, cls),
|
||||
func(v): return int(v) if (v is int or v is float or (v is String and v.is_valid_int())) else null)
|
||||
func(v): return _parse_int32_value(v))
|
||||
|
||||
|
||||
func set_font_size(params: Dictionary) -> Dictionary:
|
||||
@@ -106,12 +131,31 @@ func set_font_size(params: Dictionary) -> Dictionary:
|
||||
func(theme, name, cls, val): theme.set_font_size(name, cls, int(val)),
|
||||
func(theme, name, cls): theme.clear_font_size(name, cls),
|
||||
func(theme, name, cls): return theme.has_font_size(name, cls),
|
||||
func(v): return int(v) if (v is int or v is float or (v is String and v.is_valid_int())) else null)
|
||||
func(v): return _parse_int32_value(v))
|
||||
|
||||
|
||||
## Theme constant/font_size slots are 32-bit ints. Accepts int/float and
|
||||
## valid-int strings, but rejects non-finite or out-of-int32 values before the
|
||||
## engine narrowing can wrap them (4294967296 -> 0, 2147483648 -> -2147483648).
|
||||
## In-range fractional values keep the existing truncation policy.
|
||||
static func _parse_int32_value(v: Variant) -> Variant:
|
||||
var number: float
|
||||
if v is int or v is float:
|
||||
number = float(v)
|
||||
elif v is String and v.is_valid_int():
|
||||
number = float(v.to_int())
|
||||
else:
|
||||
return null
|
||||
if not is_finite(number) or number < float(_INT32_MIN) or number > float(_INT32_MAX):
|
||||
return null
|
||||
return int(number)
|
||||
|
||||
|
||||
# Shared implementation for scalar Theme slots (color, constant, font_size).
|
||||
# Captures old value, applies new value, saves to disk, registers undo that
|
||||
# restores the old value and saves again.
|
||||
# Captures old value, applies new value, persists it as a pre-flight save, then
|
||||
# registers undo that restores the old value and saves again. A failed
|
||||
# pre-flight save restores the old value and reports an error instead of
|
||||
# leaving memory and disk out of sync.
|
||||
func _set_scalar(
|
||||
params: Dictionary,
|
||||
kind: String,
|
||||
@@ -147,20 +191,34 @@ func _set_scalar(
|
||||
var parsed = parser.call(raw_value)
|
||||
if parsed == null:
|
||||
## color slots want a color hint; constant/font_size are integer slots.
|
||||
var hint := _COLOR_HINT if kind == "color" else "expected an integer"
|
||||
var hint := _COLOR_HINT if kind == "color" else "expected a 32-bit integer"
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Invalid %s value: %s (%s)" % [kind, raw_value, hint])
|
||||
|
||||
var had_before: bool = has_fn.call(theme, name, class_name_param)
|
||||
var before_value = getter.call(theme, name, class_name_param) if had_before else null
|
||||
|
||||
## Pre-flight: apply and persist before the undo action exists, so an
|
||||
## unwritable theme file (read-only, permissions) fails here with the
|
||||
## cached resource restored instead of reporting success while the file
|
||||
## never changed.
|
||||
var save_err: int = _apply_scalar(theme_path, setter, name, class_name_param, parsed)
|
||||
if save_err != OK:
|
||||
if had_before:
|
||||
_apply_scalar(theme_path, setter, name, class_name_param, before_value)
|
||||
else:
|
||||
_clear_scalar(theme_path, clearer, name, class_name_param)
|
||||
return _save_failure(theme_path, save_err)
|
||||
|
||||
_undo_redo.create_action("MCP: Theme set %s %s/%s" % [kind, class_name_param, name])
|
||||
_undo_redo.add_do_method(self, "_apply_scalar", theme_path, setter, name, class_name_param, parsed)
|
||||
if had_before:
|
||||
_undo_redo.add_undo_method(self, "_apply_scalar", theme_path, setter, name, class_name_param, before_value)
|
||||
else:
|
||||
_undo_redo.add_undo_method(self, "_clear_scalar", theme_path, clearer, name, class_name_param)
|
||||
_undo_redo.commit_action()
|
||||
## The do method already ran as the pre-flight save; register without
|
||||
## re-executing so a second save's error cannot be discarded.
|
||||
_undo_redo.commit_action(false)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
@@ -175,22 +233,33 @@ func _set_scalar(
|
||||
}
|
||||
|
||||
|
||||
func _apply_scalar(theme_path: String, setter: Callable, name: String, class_name_param: String, value: Variant) -> void:
|
||||
func _apply_scalar(theme_path: String, setter: Callable, name: String, class_name_param: String, value: Variant) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return
|
||||
return ERR_FILE_CANT_OPEN
|
||||
setter.call(theme, name, class_name_param, value)
|
||||
McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
func _clear_scalar(theme_path: String, clearer: Callable, name: String, class_name_param: String) -> void:
|
||||
func _clear_scalar(theme_path: String, clearer: Callable, name: String, class_name_param: String) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return
|
||||
return ERR_FILE_CANT_OPEN
|
||||
clearer.call(theme, name, class_name_param)
|
||||
McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
## Error dict for a failed pre-flight save. The caller has already restored the
|
||||
## previous slot in memory, so the theme file and the cached resource both hold
|
||||
## the pre-request state and no undo action was committed.
|
||||
static func _save_failure(theme_path: String, save_err: int) -> Dictionary:
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
"Failed to save theme to %s: %s (error %d); the change was rolled back"
|
||||
% [theme_path, error_string(save_err), save_err]
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -209,7 +278,14 @@ func _clear_scalar(theme_path: String, clearer: Callable, name: String, class_na
|
||||
## anti_aliasing (bool)
|
||||
##
|
||||
## Unknown keys inside any nested dict are rejected with INVALID_PARAMS so
|
||||
## typos fail loudly instead of silently being ignored.
|
||||
## typos fail loudly instead of silently being ignored. Numeric values must be
|
||||
## finite and within the native storage's range (32-bit ints, 32-bit floats,
|
||||
## and the float32-backed ints behind border widths/corner radii) and flags
|
||||
## real booleans; invalid input is refused with a structured error before
|
||||
## anything is applied, so a refused call leaves the theme slot and undo
|
||||
## history untouched. A slot change is persisted before the undo action is
|
||||
## registered: if the theme file cannot be written, the previous slot is
|
||||
## restored and the call fails with INTERNAL_ERROR.
|
||||
func set_stylebox_flat(params: Dictionary) -> Dictionary:
|
||||
var load_result := _load_theme_from_params(params)
|
||||
if load_result.has("error"):
|
||||
@@ -226,81 +302,23 @@ func set_stylebox_flat(params: Dictionary) -> Dictionary:
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
|
||||
|
||||
var sb := StyleBoxFlat.new()
|
||||
if params.has("bg_color"):
|
||||
var bg := _parse_color(params.bg_color)
|
||||
if bg == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid bg_color: %s (%s)" % [str(params.bg_color), _COLOR_HINT])
|
||||
sb.bg_color = bg
|
||||
if params.has("border_color"):
|
||||
var bc := _parse_color(params.border_color)
|
||||
if bc == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid border_color: %s (%s)" % [str(params.border_color), _COLOR_HINT])
|
||||
sb.border_color = bc
|
||||
|
||||
# border: {all, top, bottom, left, right} — int widths
|
||||
if params.has("border"):
|
||||
var err := _apply_sides(sb, params.border, "border",
|
||||
["top", "bottom", "left", "right"],
|
||||
"border_width_",
|
||||
TYPE_INT)
|
||||
if err != "":
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err)
|
||||
|
||||
# corners: {all, top_left, top_right, bottom_left, bottom_right} — int radii
|
||||
if params.has("corners"):
|
||||
var err2 := _apply_sides(sb, params.corners, "corners",
|
||||
["top_left", "top_right", "bottom_left", "bottom_right"],
|
||||
"corner_radius_",
|
||||
TYPE_INT)
|
||||
if err2 != "":
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err2)
|
||||
|
||||
# margins: {all, top, bottom, left, right} — float padding
|
||||
if params.has("margins"):
|
||||
var err3 := _apply_sides(sb, params.margins, "margins",
|
||||
["top", "bottom", "left", "right"],
|
||||
"content_margin_",
|
||||
TYPE_FLOAT)
|
||||
if err3 != "":
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, err3)
|
||||
|
||||
# shadow: {color, size, offset_x, offset_y}
|
||||
if params.has("shadow"):
|
||||
if typeof(params.shadow) != TYPE_DICTIONARY:
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "'shadow' must be a dict with color/size/offset_x/offset_y")
|
||||
var shadow: Dictionary = params.shadow
|
||||
var allowed_shadow_keys := {"color": true, "size": true, "offset_x": true, "offset_y": true}
|
||||
for k in shadow.keys():
|
||||
if not allowed_shadow_keys.has(k):
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown key in 'shadow': %s (valid: color, size, offset_x, offset_y)" % k)
|
||||
if shadow.has("color"):
|
||||
var sc := _parse_color(shadow.color)
|
||||
if sc == null:
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Invalid shadow.color: %s (%s)" % [str(shadow.color), _COLOR_HINT])
|
||||
sb.shadow_color = sc
|
||||
if shadow.has("size"):
|
||||
sb.shadow_size = int(shadow.size)
|
||||
if shadow.has("offset_x") or shadow.has("offset_y"):
|
||||
sb.shadow_offset = Vector2(
|
||||
float(shadow.get("offset_x", 0)),
|
||||
float(shadow.get("offset_y", 0)),
|
||||
)
|
||||
|
||||
if params.has("anti_aliasing"):
|
||||
sb.anti_aliasing = bool(params.anti_aliasing)
|
||||
var applied := _apply_flat_props(sb, params)
|
||||
if applied.has("error"):
|
||||
return applied
|
||||
|
||||
var had_before := theme.has_stylebox(name, class_name_param)
|
||||
var before_sb: StyleBox = theme.get_stylebox(name, class_name_param) if had_before else null
|
||||
|
||||
_undo_redo.create_action("MCP: Theme set stylebox %s/%s" % [class_name_param, name])
|
||||
_undo_redo.add_do_method(self, "_apply_stylebox", theme_path, name, class_name_param, sb)
|
||||
if had_before:
|
||||
_undo_redo.add_undo_method(self, "_apply_stylebox", theme_path, name, class_name_param, before_sb)
|
||||
else:
|
||||
_undo_redo.add_undo_method(self, "_clear_stylebox", theme_path, name, class_name_param)
|
||||
_undo_redo.commit_action()
|
||||
## Pre-flight: apply and persist before the undo action exists, so an
|
||||
## unwritable theme file fails here with the cached resource restored
|
||||
## instead of reporting success while the file never changed.
|
||||
var save_err: int = _apply_stylebox(theme_path, name, class_name_param, sb)
|
||||
if save_err != OK:
|
||||
if had_before:
|
||||
_apply_stylebox(theme_path, name, class_name_param, before_sb)
|
||||
else:
|
||||
_clear_stylebox(theme_path, name, class_name_param)
|
||||
return _save_failure(theme_path, save_err)
|
||||
_commit_stylebox(theme_path, name, class_name_param, sb, before_sb, had_before)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
@@ -332,50 +350,514 @@ func set_stylebox_flat(params: Dictionary) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
## Parse a {all, <side1>, <side2>, ...} dict and apply it to StyleBoxFlat via
|
||||
## its set_<prop_prefix><side> properties. Returns "" on success, an error
|
||||
## message on failure. Validates that only known keys are present.
|
||||
func _apply_sides(sb: StyleBoxFlat, sides_dict: Variant, dict_name: String,
|
||||
side_names: Array, prop_prefix: String, value_type: int) -> String:
|
||||
## Apply the StyleBoxFlat property vocabulary shared by `set_stylebox_flat`
|
||||
## and `stylebox_override` to an existing StyleBoxFlat. Returns `{ok: true}`
|
||||
## or the error dict to surface.
|
||||
func _apply_flat_props(sb: StyleBoxFlat, params: Dictionary) -> Dictionary:
|
||||
if params.has("bg_color"):
|
||||
var bg := _parse_color(params.bg_color)
|
||||
if bg == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid bg_color: %s (%s)" % [str(params.bg_color), _COLOR_HINT])
|
||||
sb.bg_color = bg
|
||||
if params.has("border_color"):
|
||||
var bc := _parse_color(params.border_color)
|
||||
if bc == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid border_color: %s (%s)" % [str(params.border_color), _COLOR_HINT])
|
||||
sb.border_color = bc
|
||||
|
||||
# border: {all, top, bottom, left, right} — int widths (real_t storage)
|
||||
if params.has("border"):
|
||||
var border_result := _apply_sides(sb, params.border, "border",
|
||||
["top", "bottom", "left", "right"],
|
||||
"border_width_",
|
||||
_NATIVE_INT32_FLOAT32)
|
||||
if border_result.has("error"):
|
||||
return border_result
|
||||
|
||||
# corners: {all, top_left, top_right, bottom_left, bottom_right} — int radii (real_t storage)
|
||||
if params.has("corners"):
|
||||
var corners_result := _apply_sides(sb, params.corners, "corners",
|
||||
["top_left", "top_right", "bottom_left", "bottom_right"],
|
||||
"corner_radius_",
|
||||
_NATIVE_INT32_FLOAT32)
|
||||
if corners_result.has("error"):
|
||||
return corners_result
|
||||
|
||||
# margins: {all, top, bottom, left, right} — float padding
|
||||
if params.has("margins"):
|
||||
var margins_result := _apply_sides(sb, params.margins, "margins",
|
||||
["top", "bottom", "left", "right"],
|
||||
"content_margin_",
|
||||
_NATIVE_FLOAT32)
|
||||
if margins_result.has("error"):
|
||||
return margins_result
|
||||
|
||||
# shadow: {color, size, offset_x, offset_y}
|
||||
if params.has("shadow"):
|
||||
if typeof(params.shadow) != TYPE_DICTIONARY:
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "'shadow' must be a dict with color/size/offset_x/offset_y")
|
||||
var shadow: Dictionary = params.shadow
|
||||
var allowed_shadow_keys := {"color": true, "size": true, "offset_x": true, "offset_y": true}
|
||||
for k in shadow.keys():
|
||||
if not allowed_shadow_keys.has(k):
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown key in 'shadow': %s (valid: color, size, offset_x, offset_y)" % k)
|
||||
if shadow.has("color"):
|
||||
var sc := _parse_color(shadow.color)
|
||||
if sc == null:
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Invalid shadow.color: %s (%s)" % [str(shadow.color), _COLOR_HINT])
|
||||
sb.shadow_color = sc
|
||||
if shadow.has("size"):
|
||||
var size_result := _parse_number_field("shadow", "size", shadow.size, _NATIVE_INT32)
|
||||
if size_result.has("error"):
|
||||
return size_result
|
||||
sb.shadow_size = size_result.value
|
||||
if shadow.has("offset_x") or shadow.has("offset_y"):
|
||||
## Missing components keep the stylebox's current offset: on the
|
||||
## stylebox_override path `sb` is a duplicate of the resolved
|
||||
## stylebox, and a patch must not silently reset the other axis.
|
||||
var offset_x := sb.shadow_offset.x
|
||||
var offset_y := sb.shadow_offset.y
|
||||
if shadow.has("offset_x"):
|
||||
var offset_x_result := _parse_number_field("shadow", "offset_x", shadow.offset_x, _NATIVE_FLOAT32)
|
||||
if offset_x_result.has("error"):
|
||||
return offset_x_result
|
||||
offset_x = offset_x_result.value
|
||||
if shadow.has("offset_y"):
|
||||
var offset_y_result := _parse_number_field("shadow", "offset_y", shadow.offset_y, _NATIVE_FLOAT32)
|
||||
if offset_y_result.has("error"):
|
||||
return offset_y_result
|
||||
offset_y = offset_y_result.value
|
||||
sb.shadow_offset = Vector2(offset_x, offset_y)
|
||||
|
||||
if params.has("anti_aliasing"):
|
||||
var anti_aliasing_result := _parse_bool_field("anti_aliasing", params.anti_aliasing)
|
||||
if anti_aliasing_result.has("error"):
|
||||
return anti_aliasing_result
|
||||
sb.anti_aliasing = anti_aliasing_result.value
|
||||
return {"ok": true}
|
||||
|
||||
|
||||
## Top-level keys in `patch` that are not part of the StyleBoxFlat vocabulary.
|
||||
static func _unknown_flat_keys(patch: Dictionary) -> Array:
|
||||
var unknown: Array = []
|
||||
for key in patch.keys():
|
||||
if not _FLAT_PATCH_KEYS.has(key):
|
||||
unknown.append(str(key))
|
||||
return unknown
|
||||
|
||||
|
||||
## Record one stylebox set as an undoable action (apply new, restore or clear
|
||||
## the previous slot). Shared by set_stylebox_flat and set_stylebox_texture.
|
||||
## The caller has already applied and persisted the new stylebox as its
|
||||
## pre-flight save, so the action is committed without re-executing the do
|
||||
## method.
|
||||
func _commit_stylebox(
|
||||
theme_path: String, name: String, class_name_param: String,
|
||||
sb: StyleBox, before_sb: StyleBox, had_before: bool
|
||||
) -> void:
|
||||
_undo_redo.create_action("MCP: Theme set stylebox %s/%s" % [class_name_param, name])
|
||||
_undo_redo.add_do_method(self, "_apply_stylebox", theme_path, name, class_name_param, sb)
|
||||
if had_before:
|
||||
_undo_redo.add_undo_method(self, "_apply_stylebox", theme_path, name, class_name_param, before_sb)
|
||||
else:
|
||||
_undo_redo.add_undo_method(self, "_clear_stylebox", theme_path, name, class_name_param)
|
||||
_undo_redo.commit_action(false)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# theme_set_stylebox_texture
|
||||
# ============================================================================
|
||||
|
||||
## Compose a StyleBoxTexture (9-slice) and assign it to a theme slot.
|
||||
##
|
||||
## Parameters (beyond theme_path / class_name / name):
|
||||
## texture_path res:// path to a Texture2D
|
||||
## region {position: {x,y}, size: {x,y}} or [x,y,w,h]
|
||||
## margins {all|left|top|right|bottom: float} texture margins
|
||||
## axis_stretch_horizontal "stretch" | "tile" | "tile_fit"
|
||||
## axis_stretch_vertical "stretch" | "tile" | "tile_fit"
|
||||
## modulate_color Color
|
||||
## draw_center bool
|
||||
##
|
||||
## Numeric values must be finite and within the native storage's range, and
|
||||
## flags real booleans; invalid input is refused with a structured error before
|
||||
## anything is applied. A slot change is persisted before the undo action is
|
||||
## registered: if the theme file cannot be written, the previous slot is
|
||||
## restored and the call fails with INTERNAL_ERROR.
|
||||
func set_stylebox_texture(params: Dictionary) -> Dictionary:
|
||||
var load_result := _load_theme_from_params(params)
|
||||
if load_result.has("error"):
|
||||
return load_result
|
||||
var theme: Theme = load_result.theme
|
||||
var theme_path: String = load_result.path
|
||||
|
||||
var class_name_param: String = params.get("class_name", "")
|
||||
if class_name_param.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: class_name")
|
||||
var name: String = params.get("name", "")
|
||||
if name.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
|
||||
var texture_path: String = params.get("texture_path", "")
|
||||
if texture_path.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: texture_path")
|
||||
var path_err = McpPathValidator.loadable_error(texture_path, "texture_path")
|
||||
if path_err != null:
|
||||
return path_err
|
||||
if not ResourceLoader.exists(texture_path):
|
||||
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Texture not found: %s" % texture_path)
|
||||
var texture := ResourceLoader.load(texture_path)
|
||||
if texture == null or not (texture is Texture2D):
|
||||
var got := texture.get_class() if texture != null else "null"
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Resource at %s is not a Texture2D (got %s)" % [texture_path, got])
|
||||
|
||||
var sb := StyleBoxTexture.new()
|
||||
sb.texture = texture
|
||||
if params.has("region"):
|
||||
var region := _parse_rect2(params.region)
|
||||
if region == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Invalid region: %s (expected {position, size} or [x,y,w,h])" % str(params.region))
|
||||
sb.region_rect = region
|
||||
if params.has("margins"):
|
||||
var margin_result := _apply_texture_margins(sb, params.margins)
|
||||
if margin_result.has("error"):
|
||||
return margin_result
|
||||
if params.has("modulate_color"):
|
||||
var mc := _parse_color(params.modulate_color)
|
||||
if mc == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Invalid modulate_color: %s (%s)" % [str(params.modulate_color), _COLOR_HINT])
|
||||
sb.modulate_color = mc
|
||||
if params.has("draw_center"):
|
||||
var draw_center_result := _parse_bool_field("draw_center", params.draw_center)
|
||||
if draw_center_result.has("error"):
|
||||
return draw_center_result
|
||||
sb.draw_center = draw_center_result.value
|
||||
if params.has("axis_stretch_horizontal"):
|
||||
var ash := _parse_axis_stretch(str(params.axis_stretch_horizontal))
|
||||
if ash == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Invalid axis_stretch_horizontal '%s'. Valid: stretch, tile, tile_fit" % str(params.axis_stretch_horizontal))
|
||||
sb.axis_stretch_horizontal = ash
|
||||
if params.has("axis_stretch_vertical"):
|
||||
var asv := _parse_axis_stretch(str(params.axis_stretch_vertical))
|
||||
if asv == null:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Invalid axis_stretch_vertical '%s'. Valid: stretch, tile, tile_fit" % str(params.axis_stretch_vertical))
|
||||
sb.axis_stretch_vertical = asv
|
||||
|
||||
var had_before := theme.has_stylebox(name, class_name_param)
|
||||
var before_sb: StyleBox = theme.get_stylebox(name, class_name_param) if had_before else null
|
||||
## Pre-flight: apply and persist before the undo action exists, so an
|
||||
## unwritable theme file fails here with the cached resource restored
|
||||
## instead of reporting success while the file never changed.
|
||||
var save_err: int = _apply_stylebox(theme_path, name, class_name_param, sb)
|
||||
if save_err != OK:
|
||||
if had_before:
|
||||
_apply_stylebox(theme_path, name, class_name_param, before_sb)
|
||||
else:
|
||||
_clear_stylebox(theme_path, name, class_name_param)
|
||||
return _save_failure(theme_path, save_err)
|
||||
_commit_stylebox(theme_path, name, class_name_param, sb, before_sb, had_before)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"path": theme_path,
|
||||
"class_name": class_name_param,
|
||||
"name": name,
|
||||
"stylebox_class": "StyleBoxTexture",
|
||||
"texture_path": texture_path,
|
||||
"region": _serialize_value(sb.region_rect),
|
||||
"margins": {
|
||||
"left": sb.texture_margin_left,
|
||||
"top": sb.texture_margin_top,
|
||||
"right": sb.texture_margin_right,
|
||||
"bottom": sb.texture_margin_bottom,
|
||||
},
|
||||
"draw_center": sb.draw_center,
|
||||
"undoable": true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# theme_set_font / theme_set_icon
|
||||
# ============================================================================
|
||||
|
||||
## Assign a Font resource to a theme font slot (button/body/heading fonts).
|
||||
func set_font(params: Dictionary) -> Dictionary:
|
||||
return _set_resource_slot(params, "font", "font_path", "Font")
|
||||
|
||||
|
||||
## Assign a Texture2D to a theme icon slot (checkbox marks, dropdown arrows).
|
||||
func set_icon(params: Dictionary) -> Dictionary:
|
||||
return _set_resource_slot(params, "icon", "texture_path", "Texture2D")
|
||||
|
||||
|
||||
## Shared implementation for font/icon slots: load the resource, verify its
|
||||
## class, then set/clear the slot as one undoable action.
|
||||
func _set_resource_slot(
|
||||
params: Dictionary, kind: String, path_param: String, expected_class: String
|
||||
) -> Dictionary:
|
||||
var load_result := _load_theme_from_params(params)
|
||||
if load_result.has("error"):
|
||||
return load_result
|
||||
var theme: Theme = load_result.theme
|
||||
var theme_path: String = load_result.path
|
||||
|
||||
var class_name_param: String = params.get("class_name", "")
|
||||
if class_name_param.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: class_name")
|
||||
var name: String = params.get("name", "")
|
||||
if name.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
|
||||
var resource_path: String = params.get(path_param, "")
|
||||
if resource_path.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % path_param)
|
||||
var path_err = McpPathValidator.loadable_error(resource_path, path_param)
|
||||
if path_err != null:
|
||||
return path_err
|
||||
if not ResourceLoader.exists(resource_path):
|
||||
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "%s not found: %s" % [expected_class, resource_path])
|
||||
var loaded := ResourceLoader.load(resource_path)
|
||||
if loaded == null or not _is_instance_of_class(loaded, expected_class):
|
||||
var got := loaded.get_class() if loaded != null else "null"
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Resource at %s is not a %s (got %s)" % [resource_path, expected_class, got])
|
||||
|
||||
var had_before := theme.has_font(name, class_name_param) if kind == "font" else theme.has_icon(name, class_name_param)
|
||||
var before_value = theme.get_font(name, class_name_param) if kind == "font" else theme.get_icon(name, class_name_param)
|
||||
if not had_before:
|
||||
before_value = null
|
||||
|
||||
## Pre-flight: apply and persist before the undo action exists, so an
|
||||
## unwritable theme file fails here with the cached resource restored
|
||||
## instead of reporting success while the file never changed.
|
||||
var save_err: int = _apply_slot(theme_path, kind, name, class_name_param, loaded)
|
||||
if save_err != OK:
|
||||
if had_before:
|
||||
_apply_slot(theme_path, kind, name, class_name_param, before_value)
|
||||
else:
|
||||
_clear_slot(theme_path, kind, name, class_name_param)
|
||||
return _save_failure(theme_path, save_err)
|
||||
|
||||
_undo_redo.create_action("MCP: Theme set %s %s/%s" % [kind, class_name_param, name])
|
||||
_undo_redo.add_do_method(self, "_apply_slot", theme_path, kind, name, class_name_param, loaded)
|
||||
if had_before:
|
||||
_undo_redo.add_undo_method(self, "_apply_slot", theme_path, kind, name, class_name_param, before_value)
|
||||
else:
|
||||
_undo_redo.add_undo_method(self, "_clear_slot", theme_path, kind, name, class_name_param)
|
||||
## The do method already ran as the pre-flight save; register without
|
||||
## re-executing so a second save's error cannot be discarded.
|
||||
_undo_redo.commit_action(false)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"path": theme_path,
|
||||
"kind": kind,
|
||||
"class_name": class_name_param,
|
||||
"name": name,
|
||||
"resource_path": resource_path,
|
||||
"resource_class": loaded.get_class(),
|
||||
"undoable": true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
## True when `resource` is an instance of `class_name` (or a subclass).
|
||||
static func _is_instance_of_class(resource: Resource, expected_class: String) -> bool:
|
||||
return ClassDB.is_parent_class(resource.get_class(), expected_class)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# theme_stylebox_override — per-node override
|
||||
# ============================================================================
|
||||
|
||||
## Duplicate the stylebox a Control resolves for `slot`, apply a StyleBoxFlat
|
||||
## patch (same keys as set_stylebox_flat), and attach it as a per-node
|
||||
## override. Undo restores the previous override, or removes the override when
|
||||
## the node had none. Patch values are validated the same way as
|
||||
## set_stylebox_flat — unknown top-level keys are refused — and a refused
|
||||
## patch leaves the node's override untouched. The action is committed to the
|
||||
## Control's scene history, so the editor's scene undo reverts it (a method
|
||||
## bound to this handler object would land in GLOBAL_HISTORY instead).
|
||||
func stylebox_override(params: Dictionary) -> Dictionary:
|
||||
var node_path: String = params.get("path", "")
|
||||
if node_path.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
|
||||
var slot: String = params.get("slot", "")
|
||||
if slot.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: slot")
|
||||
var patch: Variant = params.get("patch", {})
|
||||
if typeof(patch) != TYPE_DICTIONARY:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "'patch' must be a dict of StyleBoxFlat properties")
|
||||
var unknown_keys := _unknown_flat_keys(patch)
|
||||
if not unknown_keys.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown patch key(s): %s (valid: %s)" % [", ".join(unknown_keys), ", ".join(_FLAT_PATCH_KEYS)])
|
||||
|
||||
var resolved := McpNodeValidator.resolve_or_error(node_path, "path")
|
||||
if resolved.has("error"):
|
||||
return resolved
|
||||
var node: Node = resolved.node
|
||||
if not node is Control:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Node %s is not a Control (got %s)" % [node_path, node.get_class()])
|
||||
var control := node as Control
|
||||
|
||||
var base: StyleBox = control.get_theme_stylebox(slot)
|
||||
if base == null:
|
||||
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND,
|
||||
"No stylebox resolves for slot '%s' on %s" % [slot, node.get_class()])
|
||||
var patched: StyleBox = base.duplicate()
|
||||
if not patched is StyleBoxFlat:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Slot '%s' resolves to %s; stylebox_override patches StyleBoxFlat slots only"
|
||||
% [slot, patched.get_class()])
|
||||
var applied := _apply_flat_props(patched as StyleBoxFlat, patch)
|
||||
if applied.has("error"):
|
||||
return applied
|
||||
|
||||
var had_override: bool = control.has_theme_stylebox_override(slot)
|
||||
var scene_root: Node = resolved.scene_root
|
||||
_undo_redo.create_action("MCP: Stylebox override %s on %s" % [slot, node.name],
|
||||
UndoRedo.MERGE_DISABLE, scene_root)
|
||||
_undo_redo.add_do_method(self, "_apply_node_stylebox", control, slot, patched)
|
||||
if had_override:
|
||||
_undo_redo.add_undo_method(self, "_apply_node_stylebox", control, slot, base)
|
||||
else:
|
||||
_undo_redo.add_undo_method(self, "_remove_node_stylebox", control, slot)
|
||||
_undo_redo.commit_action()
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"path": McpScenePath.from_node(node, resolved.scene_root),
|
||||
"slot": slot,
|
||||
"stylebox_class": patched.get_class(),
|
||||
"overrode_existing": had_override,
|
||||
"undoable": true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func _apply_node_stylebox(control: Control, slot: String, sb: StyleBox) -> void:
|
||||
control.add_theme_stylebox_override(slot, sb)
|
||||
|
||||
|
||||
func _remove_node_stylebox(control: Control, slot: String) -> void:
|
||||
control.remove_theme_stylebox_override(slot)
|
||||
|
||||
|
||||
## Strict finite-number parser for stylebox numeric fields. Accepts int/float
|
||||
## and numeric strings (the McpJsonValues.parse_float vocabulary) and returns
|
||||
## `{"value": <int|float>}`; non-numeric input and non-finite results return a
|
||||
## structured error instead, so a bad value can neither raise an engine
|
||||
## conversion error nor be silently stored as 0.
|
||||
##
|
||||
## The value is validated against the *native destination* before narrowing
|
||||
## (see the _NATIVE_* kinds), and the returned value is the narrowed one, so
|
||||
## what was validated is what gets stored.
|
||||
static func _parse_number_field(dict_name: String, key: String, raw: Variant, storage: int) -> Dictionary:
|
||||
var parsed: Variant = McpJsonValues.parse_float(raw)
|
||||
if parsed == null:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"'%s.%s' must be a number, got %s" % [dict_name, key, type_string(typeof(raw))])
|
||||
var number := float(parsed)
|
||||
if not is_finite(number):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"'%s.%s' must be finite, got %s" % [dict_name, key, str(number)])
|
||||
match storage:
|
||||
_NATIVE_INT32:
|
||||
if number < float(_INT32_MIN) or number > float(_INT32_MAX):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"'%s.%s' is out of range for a 32-bit integer (got %s)" % [dict_name, key, str(number)])
|
||||
return {"value": int(number)}
|
||||
_NATIVE_FLOAT32:
|
||||
var narrowed: float = PackedFloat32Array([number])[0]
|
||||
if not is_finite(narrowed):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"'%s.%s' is out of range for a 32-bit float (got %s)" % [dict_name, key, str(number)])
|
||||
return {"value": narrowed}
|
||||
_NATIVE_INT32_FLOAT32:
|
||||
## Must survive the float32 round-trip and stay in int32, or the
|
||||
## int getter reads back a rounded or wrapped value.
|
||||
var narrowed: float = PackedFloat32Array([number])[0]
|
||||
if not is_finite(narrowed) or narrowed < float(_INT32_MIN) or narrowed > float(_INT32_MAX) \
|
||||
or int(narrowed) != int(number):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"'%s.%s' is out of range for the float-backed 32-bit int storage (got %s)" % [dict_name, key, str(number)])
|
||||
return {"value": int(narrowed)}
|
||||
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Unknown numeric storage kind: %d" % storage)
|
||||
|
||||
|
||||
## Strict bool parser for stylebox flags. `bool("false")` is true in GDScript,
|
||||
## so a stringified flag would silently invert the caller's intent.
|
||||
static func _parse_bool_field(name: String, raw: Variant) -> Dictionary:
|
||||
if raw is bool:
|
||||
return {"value": raw}
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"'%s' must be a boolean, got %s" % [name, type_string(typeof(raw))])
|
||||
|
||||
|
||||
## Parse a {all, <side1>, <side2>, ...} dict into StyleBox numeric properties.
|
||||
## Every value is parsed and validated before anything is assigned, so an
|
||||
## invalid value can neither raise an engine conversion error nor leave a
|
||||
## partially applied stylebox. `sb` is a StyleBoxFlat or StyleBoxTexture (both
|
||||
## expose `set`); `storage` is one of the _NATIVE_* kinds. Returns
|
||||
## {"ok": true} or the error dict to surface.
|
||||
static func _apply_sides(sb: Object, sides_dict: Variant, dict_name: String,
|
||||
side_names: Array, prop_prefix: String, storage: int) -> Dictionary:
|
||||
if typeof(sides_dict) != TYPE_DICTIONARY:
|
||||
return "'%s' must be a dict with 'all' and/or side-specific keys" % dict_name
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"'%s' must be a dict with 'all' and/or side-specific keys" % dict_name)
|
||||
var valid_keys := {"all": true}
|
||||
for s in side_names:
|
||||
valid_keys[s] = true
|
||||
for k in sides_dict.keys():
|
||||
if not valid_keys.has(k):
|
||||
return "Unknown key in '%s': %s (valid: all, %s)" % [
|
||||
dict_name, k, ", ".join(side_names)
|
||||
]
|
||||
# Apply `all` first, then override with side-specific keys.
|
||||
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS,
|
||||
"Unknown key in '%s': %s (valid: all, %s)" % [dict_name, k, ", ".join(side_names)])
|
||||
var parsed_values := {}
|
||||
if sides_dict.has("all"):
|
||||
var all_val: Variant = sides_dict.all
|
||||
for s in side_names:
|
||||
var v: Variant = int(all_val) if value_type == TYPE_INT else float(all_val)
|
||||
sb.set(prop_prefix + s, v)
|
||||
var all_result := _parse_number_field(dict_name, "all", sides_dict.all, storage)
|
||||
if all_result.has("error"):
|
||||
return all_result
|
||||
parsed_values["all"] = all_result.value
|
||||
for s in side_names:
|
||||
if sides_dict.has(s):
|
||||
var v2: Variant = int(sides_dict[s]) if value_type == TYPE_INT else float(sides_dict[s])
|
||||
sb.set(prop_prefix + s, v2)
|
||||
return ""
|
||||
var side_result := _parse_number_field(dict_name, s, sides_dict[s], storage)
|
||||
if side_result.has("error"):
|
||||
return side_result
|
||||
parsed_values[s] = side_result.value
|
||||
# Every value parsed: apply `all` first, then the side-specific overrides.
|
||||
if parsed_values.has("all"):
|
||||
for s in side_names:
|
||||
sb.set(prop_prefix + s, parsed_values["all"])
|
||||
for s in side_names:
|
||||
if parsed_values.has(s):
|
||||
sb.set(prop_prefix + s, parsed_values[s])
|
||||
return {"ok": true}
|
||||
|
||||
|
||||
func _apply_stylebox(theme_path: String, name: String, class_name_param: String, sb: StyleBox) -> void:
|
||||
func _apply_stylebox(theme_path: String, name: String, class_name_param: String, sb: StyleBox) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return
|
||||
return ERR_FILE_CANT_OPEN
|
||||
theme.set_stylebox(name, class_name_param, sb)
|
||||
McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
func _clear_stylebox(theme_path: String, name: String, class_name_param: String) -> void:
|
||||
func _clear_stylebox(theme_path: String, name: String, class_name_param: String) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return
|
||||
return ERR_FILE_CANT_OPEN
|
||||
theme.clear_stylebox(name, class_name_param)
|
||||
McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -461,9 +943,17 @@ static func _validate_res_path(path: String, required_suffix: String, param_name
|
||||
## Returns null if the input cannot be parsed.
|
||||
## Delegates to the canonical parser (#714) — gains [r,g,b(,a)] array
|
||||
## support and strict key/component checking, same shapes as every other
|
||||
## color-accepting handler.
|
||||
## color-accepting handler. Color components are 32-bit floats, so a finite
|
||||
## double like 1e40 narrows to INF at construction; the parsed value is
|
||||
## re-checked for finiteness and refused instead of storing an infinite color.
|
||||
static func _parse_color(value: Variant) -> Variant:
|
||||
return McpJsonValues.parse_color(value)
|
||||
var parsed: Variant = McpJsonValues.parse_color(value)
|
||||
if parsed == null:
|
||||
return null
|
||||
var color := parsed as Color
|
||||
if not (is_finite(color.r) and is_finite(color.g) and is_finite(color.b) and is_finite(color.a)):
|
||||
return null
|
||||
return color
|
||||
|
||||
|
||||
static func _serialize_value(value: Variant) -> Variant:
|
||||
@@ -473,4 +963,94 @@ static func _serialize_value(value: Variant) -> Variant:
|
||||
return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
||||
if value is Vector2:
|
||||
return {"x": value.x, "y": value.y}
|
||||
if value is Rect2:
|
||||
return {
|
||||
"position": {"x": value.position.x, "y": value.position.y},
|
||||
"size": {"x": value.size.x, "y": value.size.y},
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
## Set a font/icon slot from inside an undo action. Stylebox slots use the
|
||||
## dedicated `_apply_stylebox` path (they need the StyleBox-typed setter).
|
||||
func _apply_slot(theme_path: String, kind: String, name: String, class_name_param: String, value: Variant) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return ERR_FILE_CANT_OPEN
|
||||
match kind:
|
||||
"font":
|
||||
theme.set_font(name, class_name_param, value)
|
||||
"icon":
|
||||
theme.set_icon(name, class_name_param, value)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
func _clear_slot(theme_path: String, kind: String, name: String, class_name_param: String) -> int:
|
||||
var theme: Theme = ResourceLoader.load(theme_path)
|
||||
if theme == null:
|
||||
push_warning("MCP: Failed to load theme for undo/redo: %s" % theme_path)
|
||||
return ERR_FILE_CANT_OPEN
|
||||
match kind:
|
||||
"font":
|
||||
theme.clear_font(name, class_name_param)
|
||||
"icon":
|
||||
theme.clear_icon(name, class_name_param)
|
||||
return McpResourceIO.guarded_save(theme, theme_path, _connection)
|
||||
|
||||
|
||||
## Parse a 9-slice region from {position: {x,y}, size: {x,y}} or [x,y,w,h].
|
||||
## Non-finite components are rejected (null), so a NaN/INF region can never be
|
||||
## committed to a stylebox slot.
|
||||
static func _parse_rect2(value: Variant) -> Variant:
|
||||
if value is Rect2:
|
||||
return value if _is_finite_rect2(value) else null
|
||||
if value is Dictionary:
|
||||
var d: Dictionary = value
|
||||
if not (d.has("position") and d.has("size")):
|
||||
return null
|
||||
var pos := McpJsonValues.parse_vector2(d.position)
|
||||
var size := McpJsonValues.parse_vector2(d.size)
|
||||
if pos == null or size == null:
|
||||
return null
|
||||
var rect := Rect2(pos, size)
|
||||
return rect if _is_finite_rect2(rect) else null
|
||||
if value is Array:
|
||||
var arr: Array = value
|
||||
if arr.size() != 4:
|
||||
return null
|
||||
for item in arr:
|
||||
if not (item is int or item is float):
|
||||
return null
|
||||
var rect := Rect2(float(arr[0]), float(arr[1]), float(arr[2]), float(arr[3]))
|
||||
return rect if _is_finite_rect2(rect) else null
|
||||
return null
|
||||
|
||||
|
||||
static func _is_finite_rect2(rect: Rect2) -> bool:
|
||||
return (
|
||||
is_finite(rect.position.x)
|
||||
and is_finite(rect.position.y)
|
||||
and is_finite(rect.size.x)
|
||||
and is_finite(rect.size.y)
|
||||
)
|
||||
|
||||
|
||||
## StyleBoxTexture axis stretch mode by name. Returns null on an unknown name.
|
||||
static func _parse_axis_stretch(value: String) -> Variant:
|
||||
match value:
|
||||
"stretch":
|
||||
return StyleBoxTexture.AXIS_STRETCH_MODE_STRETCH
|
||||
"tile":
|
||||
return StyleBoxTexture.AXIS_STRETCH_MODE_TILE
|
||||
"tile_fit":
|
||||
return StyleBoxTexture.AXIS_STRETCH_MODE_TILE_FIT
|
||||
return null
|
||||
|
||||
|
||||
## Apply {all, left, top, right, bottom} texture margins to a StyleBoxTexture.
|
||||
## Shares the flat side parser's validate-then-apply contract: every value is
|
||||
## parsed before any margin is assigned. Returns {"ok": true} or the error dict.
|
||||
static func _apply_texture_margins(sb: StyleBoxTexture, margins: Variant) -> Dictionary:
|
||||
return _apply_sides(sb, margins, "margins", ["left", "top", "right", "bottom"],
|
||||
"texture_margin_", _NATIVE_FLOAT32)
|
||||
|
||||
Reference in New Issue
Block a user