Replace dasher-pack with unified animation-pack using original Blender bone names
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
||||
|
||||
## Replaces all points on a Curve / Curve2D / Curve3D resource. The point
|
||||
## list shape depends on resource type (see `set_points` for the schemas).
|
||||
##
|
||||
## Dedicated tool rather than a property set because Curve2D/Curve3D.add_point
|
||||
## is a method call, not a property — resource_create's `properties` dict can't
|
||||
## reach it.
|
||||
|
||||
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
|
||||
|
||||
|
||||
func set_points(params: Dictionary) -> Dictionary:
|
||||
var node_path: String = params.get("path", "")
|
||||
var property: String = params.get("property", "")
|
||||
var resource_path: String = params.get("resource_path", "")
|
||||
var new_points: Array = params.get("points", [])
|
||||
|
||||
var home_err := McpResourceIO.validate_home(params)
|
||||
if home_err != null:
|
||||
return home_err
|
||||
var has_file_target := not resource_path.is_empty()
|
||||
if not (new_points is Array):
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "points must be an array")
|
||||
|
||||
var curve: Resource
|
||||
var node: Node = null
|
||||
var curve_created := false
|
||||
if has_file_target:
|
||||
var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
|
||||
if rpath_err != null:
|
||||
return rpath_err
|
||||
if not ResourceLoader.exists(resource_path):
|
||||
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
|
||||
# ResourceLoader.load() returns Godot's cached Resource. Duplicate
|
||||
# before mutating so: (a) open scenes holding a reference to this
|
||||
# .tres don't silently see the new points outside any undo action,
|
||||
# and (b) if ResourceSaver.save() fails we haven't corrupted the
|
||||
# in-memory cache (cache/disk divergence). Also guard against
|
||||
# ResourceLoader.exists() succeeding but load() returning null
|
||||
# (corrupt .tres, unregistered class) — otherwise curve.get_class()
|
||||
# on the response line below would crash the plugin.
|
||||
var loaded_curve: Resource = ResourceLoader.load(resource_path)
|
||||
if loaded_curve == null:
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
"Failed to load curve from %s (file exists but load returned null — may be corrupt)" % resource_path
|
||||
)
|
||||
curve = loaded_curve.duplicate()
|
||||
else:
|
||||
var _scene_check := McpNodeValidator.require_scene_or_error()
|
||||
if _scene_check.has("error"):
|
||||
return _scene_check
|
||||
var scene_root: Node = _scene_check.scene_root
|
||||
node = McpScenePath.resolve(node_path, scene_root)
|
||||
if node == null:
|
||||
return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
|
||||
if not (property in node):
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.PROPERTY_NOT_ON_CLASS,
|
||||
"Property '%s' not found on %s" % [property, node.get_class()]
|
||||
)
|
||||
curve = node.get(property)
|
||||
# Auto-create a fresh Curve subclass if the slot is empty. Infer the
|
||||
# concrete class from the property's hint_string (e.g. Path3D.curve's
|
||||
# hint is "Curve3D"). Creation is bundled into the same undo action
|
||||
# as the point-set below, so Ctrl-Z rolls back both.
|
||||
if curve == null:
|
||||
var inferred := _infer_curve_class(node, property)
|
||||
if inferred.is_empty():
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.INVALID_PARAMS,
|
||||
"Curve slot on %s.%s is null and the Curve class can't be inferred from the property hint — create one first with resource_create (type=Curve3D/Curve2D/Curve)" % [node.get_class(), property]
|
||||
)
|
||||
curve = ClassDB.instantiate(inferred)
|
||||
if curve == null:
|
||||
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % inferred)
|
||||
curve_created = true
|
||||
|
||||
if not (curve is Curve or curve is Curve2D or curve is Curve3D):
|
||||
return ErrorCodes.make(
|
||||
ErrorCodes.WRONG_TYPE,
|
||||
"Resource is %s — must be Curve, Curve2D, or Curve3D" % curve.get_class()
|
||||
)
|
||||
|
||||
var coerced := _coerce_points(curve, new_points)
|
||||
if coerced.has("error"):
|
||||
return coerced.error
|
||||
|
||||
var new_snapshot: Array = coerced.snapshot
|
||||
|
||||
if has_file_target:
|
||||
_apply_snapshot_to_curve(curve, new_snapshot)
|
||||
# curve_set_points EDITS an existing .tres, so override the default
|
||||
# "delete to revert" message via extra_fields.
|
||||
return McpResourceIO.save_to_disk(curve, resource_path, true, "Curve", {
|
||||
"curve_class": curve.get_class(),
|
||||
"point_count": new_snapshot.size(),
|
||||
"reason": "File save is persistent; edit the .tres file manually to revert",
|
||||
}, _connection)
|
||||
|
||||
# Inline (node-attached) path: swap the curve property so the action lands
|
||||
# cleanly in scene history, mirroring the resource-swap pattern used by
|
||||
# material_handler::assign_material. When curve_created is true the
|
||||
# "old" value is null — undo clears the slot back to empty.
|
||||
var new_curve: Resource = curve if curve_created else curve.duplicate()
|
||||
_apply_snapshot_to_curve(new_curve, new_snapshot)
|
||||
var old_curve: Resource = null if curve_created else curve
|
||||
|
||||
_undo_redo.create_action("MCP: Set %d points on %s.%s" % [new_snapshot.size(), node.name, property])
|
||||
_undo_redo.add_do_property(node, property, new_curve)
|
||||
_undo_redo.add_undo_property(node, property, old_curve)
|
||||
_undo_redo.add_do_reference(new_curve)
|
||||
_undo_redo.commit_action()
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"path": node_path,
|
||||
"property": property,
|
||||
"curve_class": new_curve.get_class(),
|
||||
"point_count": new_snapshot.size(),
|
||||
"curve_created": curve_created,
|
||||
"undoable": true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
## Infer the concrete Curve class to instantiate for a null property slot.
|
||||
## Reads the property's hint_string (set by Godot on resource-typed exports)
|
||||
## to get the exact accepted class name (e.g. "Curve3D" for Path3D.curve).
|
||||
## Returns empty string if no viable curve class can be determined.
|
||||
static func _infer_curve_class(node: Node, property: String) -> String:
|
||||
for prop in node.get_property_list():
|
||||
if prop.name != property:
|
||||
continue
|
||||
var hint_string: String = prop.get("hint_string", "")
|
||||
if hint_string.is_empty():
|
||||
return ""
|
||||
if not ClassDB.class_exists(hint_string):
|
||||
return ""
|
||||
if hint_string == "Curve" or hint_string == "Curve2D" or hint_string == "Curve3D":
|
||||
return hint_string
|
||||
# Some custom properties may list a parent class; require an exact
|
||||
# match against our three supported types to avoid surprises.
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
## Convert input `points` into a normalized snapshot of typed values for
|
||||
## the given curve type. Returns {snapshot: Array} on success or
|
||||
## {error: ...} on failure.
|
||||
static func _coerce_points(curve: Resource, points: Array) -> Dictionary:
|
||||
var snapshot: Array = []
|
||||
if curve is Curve:
|
||||
for i in range(points.size()):
|
||||
var p = points[i]
|
||||
if not (p is Dictionary) or not p.has("offset") or not p.has("value"):
|
||||
return {"error": ErrorCodes.make(
|
||||
ErrorCodes.INVALID_PARAMS,
|
||||
"Curve points[%d] must be {offset, value, [left_tangent, right_tangent]}" % i
|
||||
)}
|
||||
snapshot.append({
|
||||
"offset": float(p["offset"]),
|
||||
"value": float(p["value"]),
|
||||
"left_tangent": float(p.get("left_tangent", 0.0)),
|
||||
"right_tangent": float(p.get("right_tangent", 0.0)),
|
||||
})
|
||||
elif curve is Curve2D:
|
||||
var zero2 := {"x": 0, "y": 0}
|
||||
for i in range(points.size()):
|
||||
var p2 = points[i]
|
||||
if not (p2 is Dictionary) or not p2.has("position"):
|
||||
return {"error": ErrorCodes.make(
|
||||
ErrorCodes.INVALID_PARAMS,
|
||||
"Curve2D points[%d] must have 'position' (and optional 'in', 'out')" % i
|
||||
)}
|
||||
var axes2 := {
|
||||
"position": p2["position"],
|
||||
"in": p2.get("in", zero2),
|
||||
"out": p2.get("out", zero2),
|
||||
}
|
||||
var coerced2 := {}
|
||||
for field in ["position", "in", "out"]:
|
||||
var v = NodeHandler._coerce_value(axes2[field], TYPE_VECTOR2)
|
||||
var err := NodeHandler._check_coerced(v, TYPE_VECTOR2, "Curve2D points[%d].%s" % [i, field])
|
||||
if err != null:
|
||||
return {"error": err}
|
||||
coerced2[field] = v
|
||||
snapshot.append(coerced2)
|
||||
else: # Curve3D
|
||||
var zero3 := {"x": 0, "y": 0, "z": 0}
|
||||
for i in range(points.size()):
|
||||
var p3 = points[i]
|
||||
if not (p3 is Dictionary) or not p3.has("position"):
|
||||
return {"error": ErrorCodes.make(
|
||||
ErrorCodes.INVALID_PARAMS,
|
||||
"Curve3D points[%d] must have 'position' (and optional 'in', 'out', 'tilt')" % i
|
||||
)}
|
||||
var axes3 := {
|
||||
"position": p3["position"],
|
||||
"in": p3.get("in", zero3),
|
||||
"out": p3.get("out", zero3),
|
||||
}
|
||||
var coerced3 := {}
|
||||
for field in ["position", "in", "out"]:
|
||||
var v = NodeHandler._coerce_value(axes3[field], TYPE_VECTOR3)
|
||||
var err := NodeHandler._check_coerced(v, TYPE_VECTOR3, "Curve3D points[%d].%s" % [i, field])
|
||||
if err != null:
|
||||
return {"error": err}
|
||||
coerced3[field] = v
|
||||
coerced3["tilt"] = float(p3.get("tilt", 0.0))
|
||||
snapshot.append(coerced3)
|
||||
return {"snapshot": snapshot}
|
||||
|
||||
|
||||
func _apply_snapshot_to_curve(curve: Resource, snapshot: Array) -> void:
|
||||
curve.clear_points()
|
||||
if curve is Curve:
|
||||
for p: Dictionary in snapshot:
|
||||
curve.add_point(
|
||||
Vector2(p.offset, p.value),
|
||||
p.left_tangent,
|
||||
p.right_tangent
|
||||
)
|
||||
elif curve is Curve2D:
|
||||
for p: Dictionary in snapshot:
|
||||
curve.add_point(p.position, p["in"], p.out)
|
||||
elif curve is Curve3D:
|
||||
for i in range(snapshot.size()):
|
||||
var p: Dictionary = snapshot[i]
|
||||
curve.add_point(p.position, p["in"], p.out)
|
||||
curve.set_point_tilt(i, p.tilt)
|
||||
Reference in New Issue
Block a user