# Structure for addons/godot_mcp/command_handler.gd
@tool
extends RefCounted
var editor_plugin: EditorPlugin = null
# Initialize with reference to the editor plugin
func set_editor_plugin(plugin):
editor_plugin = plugin
# Main command handling function
func handle_command(command_type, params):
match command_type:
"GET_SCENE_INFO":
return handle_get_scene_info()
"OPEN_SCENE":
return handle_open_scene(params)
"SAVE_SCENE":
return handle_save_scene()
"CREATE_CHILD_OBJECT":
return handle_create_child_object(params)
"NEW_SCENE":
return handle_new_scene(params)
"CREATE_OBJECT":
return handle_create_object(params)
"DELETE_OBJECT":
return handle_delete_object(params)
"FIND_OBJECTS_BY_NAME":
return handle_find_objects_by_name(params)
"GET_OBJECT_PROPERTIES":
return handle_get_object_properties(params)
"SET_PROPERTY":
return handle_set_property(params)
"SET_COLLISION_SHAPE":
return handle_set_collision_shape(params)
"SET_OBJECT_TRANSFORM":
return handle_set_object_transform(params)
"CREATE_CHILD_OBJECT":
return handle_create_child_object(params)
"GET_ASSET_LIST":
return handle_get_asset_list(params)
"VIEW_SCRIPT":
return handle_view_script(params)
"SET_NESTED_PROPERTY":
return handle_set_nested_property(params)
"SET_PARENT":
return handle_set_parent(params)
"CREATE_SCRIPT":
return handle_create_script(params)
"UPDATE_SCRIPT":
return handle_update_script(params)
"LIST_SCRIPTS":
return handle_list_scripts(params)
"DELETE_SCRIPT":
return handle_delete_script(params)
"DELETE_FILE":
return handle_delete_file(params)
"EDITOR_CONTROL":
return handle_editor_control(params)
"SET_MATERIAL":
return handle_set_material(params)
"IMPORT_ASSET":
return handle_import_asset(params)
"SET_MESH":
return handle_set_mesh(params)
"CREATE_PREFAB":
return handle_create_packed_scene(params)
"INSTANTIATE_PREFAB":
return handle_instantiate_prefab(params)
"SHOW_MESSAGE":
return handle_show_message(params)
"REIMPORT_ASSET":
return handle_reimport_asset(params)
"IMPORT_GLB_SCENE":
return handle_import_glb_scene(params)
_:
return {"error": "Unknown command type: " + command_type}
# Scene commands
func handle_get_scene_info():
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
var scene_info = {
"name": current_scene.name,
"path": current_scene.scene_file_path,
"hierarchy": _get_hierarchy_recursive(current_scene),
"root_objects": []
}
# Get root-level nodes
for child in current_scene.get_children():
scene_info.root_objects.append({
"name": child.name,
"type": child.get_class()
})
return scene_info
# Helper function to recursively build hierarchy
func _get_hierarchy_recursive(node):
var hierarchy = {
"name": node.name,
"type": node.get_class(),
"children": []
}
# Add transform info for spatial nodes
if node is Node3D:
hierarchy["transform"] = {
"position": [node.position.x, node.position.y, node.position.z],
"rotation": [node.rotation_degrees.x, node.rotation_degrees.y, node.rotation_degrees.z],
"scale": [node.scale.x, node.scale.y, node.scale.z]
}
elif node is Node2D:
hierarchy["transform"] = {
"position": [node.position.x, node.position.y],
"rotation": node.rotation_degrees,
"scale": [node.scale.x, node.scale.y]
}
# Add script info if available
if node.get_script():
hierarchy["script"] = node.get_script().resource_path
# Add all child nodes recursively
for child in node.get_children():
hierarchy["children"].append(_get_hierarchy_recursive(child))
return hierarchy
func handle_open_scene(params):
if not params.has("scene_path"):
return {"error": "Missing required parameter: scene_path"}
var scene_path = params.scene_path
var editor_interface = editor_plugin.get_editor_interface()
# Check if file exists
if not FileAccess.file_exists(scene_path):
return {"error": "Scene file does not exist: " + scene_path}
# Save current scene if needed
if params.get("save_current", false):
editor_interface.save_scene()
# Open the scene
editor_interface.open_scene_from_path(scene_path)
return {"message": "Scene opened successfully: " + scene_path}
func handle_save_scene():
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
editor_interface.save_scene()
return {"message": "Scene saved successfully: " + current_scene.scene_file_path}
func handle_new_scene(params):
if not params.has("scene_path"):
return {"error": "Missing required parameter: scene_path"}
var scene_path = params.scene_path
var editor_interface = editor_plugin.get_editor_interface()
# Check if file exists and handle overwrite
if FileAccess.file_exists(scene_path) and not params.get("overwrite", false):
return {"error": "Scene file already exists. Use overwrite=true to replace it."}
# Create directory if needed
var directory = scene_path.get_base_dir()
if not DirAccess.dir_exists_absolute(directory):
var error = DirAccess.make_dir_recursive_absolute(directory)
if error != OK:
return {"error": "Failed to create directory: " + directory}
# Create a new scene with a Node as root
var root_node = Node.new()
root_node.name = scene_path.get_file().get_basename()
# Create a packed scene and save it
var packed_scene = PackedScene.new()
var result = packed_scene.pack(root_node)
if result != OK:
return {"error": "Failed to pack scene: " + str(result)}
var error = ResourceSaver.save(packed_scene, scene_path)
if error != OK:
return {"error": "Failed to save new scene: " + str(error)}
# Open the newly created scene
editor_interface.open_scene_from_path(scene_path)
return {"message": "New scene created successfully: " + scene_path}
# Object commands
func handle_create_object(params):
var type = params.get("type", "EMPTY")
var name = params.get("name", "")
var location = params.get("location", [0, 0, 0])
var rotation = params.get("rotation", [0, 0, 0])
var scale = params.get("scale", [1, 1, 1])
var replace_if_exists = params.get("replace_if_exists", false)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Check if name already exists and handle replacement
if name != "":
var existing_node = current_scene.find_child(name, true, false)
if existing_node and not replace_if_exists:
return {"error": "Node with name '" + name + "' already exists. Use replace_if_exists=true to replace it."}
elif existing_node and replace_if_exists:
existing_node.queue_free()
# Create the node based on type (case-insensitive)
var node
var upper_type = type.to_upper()
# Handle common 3D node types
match upper_type:
"NODE", "EMPTY":
node = Node3D.new()
"NODE3D":
node = Node3D.new()
"SPATIAL": # For compatibility with Godot 3.x terminology
node = Node3D.new()
"MESH", "MESHINSTANCE3D":
node = MeshInstance3D.new()
"CUBE", "BOX":
node = MeshInstance3D.new()
node.mesh = BoxMesh.new()
"SPHERE":
node = MeshInstance3D.new()
node.mesh = SphereMesh.new()
"CYLINDER":
node = MeshInstance3D.new()
node.mesh = CylinderMesh.new()
"PLANE":
node = MeshInstance3D.new()
node.mesh = PlaneMesh.new()
"CAMERA", "CAMERA3D":
node = Camera3D.new()
"LIGHT", "DIRECTIONALLIGHT", "DIRECTIONALLIGHT3D":
node = DirectionalLight3D.new()
"SPOTLIGHT", "SPOTLIGHT3D":
node = SpotLight3D.new()
"OMNILIGHT", "OMNILIGHT3D":
node = OmniLight3D.new()
"RIGIDBODY", "RIGIDBODY3D":
node = RigidBody3D.new()
"STATICBODY", "STATICBODY3D":
node = StaticBody3D.new()
"CHARACTERBODY", "CHARACTERBODY3D":
node = CharacterBody3D.new()
"AREA", "AREA3D":
node = Area3D.new()
"COLLISION", "COLLISIONSHAPE3D":
node = CollisionShape3D.new()
# Add a default sphere shape
node.shape = SphereShape3D.new()
# Handle common 2D node types
"NODE2D":
node = Node2D.new()
"SPRITE", "SPRITE2D":
node = Sprite2D.new()
"CAMERA2D":
node = Camera2D.new()
"AREA2D":
node = Area2D.new()
"COLLISION2D", "COLLISIONSHAPE2D":
node = CollisionShape2D.new()
# Add a default circle shape
node.shape = CircleShape2D.new()
"RIGIDBODY2D":
node = RigidBody2D.new()
"STATICBODY2D":
node = StaticBody2D.new()
"CHARACTERBODY2D":
node = CharacterBody2D.new()
# Handle UI node types
"CONTROL":
node = Control.new()
"PANEL":
node = Panel.new()
"BUTTON":
node = Button.new()
"LABEL":
node = Label.new()
"LINEEDIT":
node = LineEdit.new()
"TEXTEDIT":
node = TextEdit.new()
"CONTAINER":
node = Container.new()
"VBOX", "VBOXCONTAINER":
node = VBoxContainer.new()
"HBOX", "HBOXCONTAINER":
node = HBoxContainer.new()
_:
# Try to create the node directly by class name using ClassDB
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
node = ClassDB.instantiate(type)
else:
return {"error": "Unsupported object type: " + type}
# Set node name if provided
if name != "":
node.name = name
# Add to scene
current_scene.add_child(node)
node.owner = current_scene
# Set transform for Node3D or Node2D objects
if node is Node3D:
node.position = Vector3(location[0], location[1], location[2])
node.rotation_degrees = Vector3(rotation[0], rotation[1], rotation[2])
node.scale = Vector3(scale[0], scale[1], scale[2])
elif node is Node2D:
node.position = Vector2(location[0], location[1])
node.rotation_degrees = rotation[0]
node.scale = Vector2(scale[0], scale[1])
return {
"name": node.name,
"type": node.get_class(),
"path": current_scene.get_path_to(node)
}
func handle_delete_object(params):
if not params.has("name"):
return {"error": "Missing required parameter: name"}
var name_or_path = params.name
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name or path
var node = null
# Check if it's a path (contains /)
if "/" in name_or_path:
# Try direct path lookup first
node = current_scene.get_node_or_null(name_or_path)
# If that fails, try with leading slash
if not node and not name_or_path.begins_with("/"):
node = current_scene.get_node_or_null("/" + name_or_path)
# If still not found, try parsing the path components
if not node:
var parts = name_or_path.split("/")
var current = current_scene
for part in parts:
if part == "":
continue
var found = false
for child in current.get_children():
if child.name == part:
current = child
found = true
break
if not found:
# Try searching recursively for this part
var found_node = current.find_child(part, true, false)
if found_node:
current = found_node
else:
# Part not found, path is invalid
return {"error": "Could not find part '" + part + "' in path: " + name_or_path}
node = current
else:
# Simple name lookup for non-path names
node = current_scene.find_child(name_or_path, true, false)
if not node:
return {"error": "Node not found: " + name_or_path}
# Store the node's path for the response before deleting
var node_path = current_scene.get_path_to(node)
var node_type = node.get_class()
# Delete the node
node.queue_free()
return {
"message": "Node deleted: " + name_or_path,
"path": node_path,
"type": node_type
}
#func handle_delete_object(params):
#if not params.has("name"):
#return {"error": "Missing required parameter: name"}
#
#var name = params.name
#var editor_interface = editor_plugin.get_editor_interface()
#var current_scene = editor_interface.get_edited_scene_root()
#
#if current_scene == null:
#return {"error": "No scene is currently open"}
#
## Find node by name
#var node = current_scene.find_child(name, true, false)
#if not node:
#return {"error": "Node not found: " + name}
#
## Delete the node
#node.queue_free()
#return {"message": "Node deleted: " + name}
func handle_find_objects_by_name(params):
if not params.has("name"):
return {"error": "Missing required parameter: name"}
var search_name = params.name
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
var objects = []
var nodes = _find_nodes_by_name(current_scene, search_name)
for node in nodes:
objects.append({
"name": node.name,
"path": _get_node_path(node),
"type": node.get_class()
})
return {"objects": objects}
# Helper function to find nodes recursively
func _find_nodes_by_name(root, search_name):
var result = []
if search_name in root.name:
result.append(root)
for child in root.get_children():
result.append_array(_find_nodes_by_name(child, search_name))
return result
# Helper function to get a node's path relative to the scene root
func _get_node_path(node):
var current_scene = editor_plugin.get_editor_interface().get_edited_scene_root()
if current_scene:
return current_scene.get_path_to(node)
return node.get_path()
func handle_get_object_properties(params):
if not params.has("name"):
return {"error": "Missing required parameter: name"}
var name_or_path = params.name
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
var node
# Check if we're dealing with a path or just a name
if "/" in name_or_path:
# It's a path, try to get the node using get_node
node = current_scene.get_node_or_null(name_or_path)
# If that fails, try with NodePath
if not node:
var node_path = NodePath(name_or_path)
node = current_scene.get_node_or_null(node_path)
# Try finding from root with a leading slash
if not node and not name_or_path.begins_with("/"):
node = current_scene.get_node_or_null("/" + name_or_path)
else:
# It's just a name, use find_child (which searches recursively)
node = current_scene.find_child(name_or_path, true, false)
if not node:
return {"error": "Node not found: " + name_or_path}
# Get properties
var properties = {
"name": node.name,
"type": node.get_class(),
"path": current_scene.get_path_to(node),
"visible": node.visible if "visible" in node else true,
"components": []
}
# Handle transform properties for 3D nodes
if node is Node3D:
properties["transform"] = {
"position": [node.position.x, node.position.y, node.position.z],
"rotation": [node.rotation_degrees.x, node.rotation_degrees.y, node.rotation_degrees.z],
"scale": [node.scale.x, node.scale.y, node.scale.z]
}
elif node is Node2D:
properties["transform"] = {
"position": [node.position.x, node.position.y],
"rotation": node.rotation_degrees,
"scale": [node.scale.x, node.scale.y]
}
# Get node properties and components
if node.get_script():
properties["components"].append({
"type": "Script",
"path": node.get_script().resource_path
})
# Get children information
properties["children"] = []
for child in node.get_children():
properties["children"].append({
"name": child.name,
"type": child.get_class(),
"path": current_scene.get_path_to(child)
})
# Get parent information
var parent = node.get_parent()
if parent and parent != current_scene:
properties["parent"] = {
"name": parent.name,
"type": parent.get_class(),
"path": current_scene.get_path_to(parent)
}
return properties
func handle_set_object_transform(params):
if not params.has("name"):
return {"error": "Missing required parameter: name"}
var name = params.name
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name
var node = current_scene.find_child(name, true, false)
if not node:
return {"error": "Node not found: " + name}
# Check if node is 3D
if not node is Node3D:
return {"error": "Node is not a 3D node: " + name}
# Set transform properties
if params.has("location"):
var loc = params.location
node.position = Vector3(loc[0], loc[1], loc[2])
if params.has("rotation"):
var rot = params.rotation
node.rotation_degrees = Vector3(rot[0], rot[1], rot[2])
if params.has("scale"):
var scale = params.scale
node.scale = Vector3(scale[0], scale[1], scale[2])
return {"message": "Transform updated for node: " + name}
# Asset commands
func handle_get_asset_list(params):
var type = params.get("type", "")
var search_pattern = params.get("search_pattern", "*")
var folder = params.get("folder", "res://")
var assets = []
var dir = DirAccess.open(folder)
if dir == null:
return {"error": "Unable to access directory: " + folder}
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
var full_path = folder.path_join(file_name)
if dir.current_is_dir():
# Handle directories if needed
pass
else:
# Check if it matches the search pattern
if search_pattern == "*" or search_pattern in file_name:
# Check type if specified
var add_file = true
if type != "":
# Determine file type based on extension
var extension = file_name.get_extension().to_lower()
match type.to_lower():
"scene":
add_file = extension == "tscn" or extension == "scn"
"script":
add_file = extension == "gd" or extension == "cs"
"texture":
add_file = extension in ["png", "jpg", "jpeg", "webp"]
"material":
add_file = extension == "material" or extension == "tres"
"prefab", "packedscene":
add_file = extension == "tscn" or extension == "scn"
if add_file:
assets.append({
"name": file_name,
"path": full_path,
"type": _get_resource_type(full_path)
})
file_name = dir.get_next()
dir.list_dir_end()
return {"assets": assets}
# Helper function to determine resource type
func _get_resource_type(path):
var extension = path.get_extension().to_lower()
match extension:
"tscn", "scn":
return "PackedScene"
"gd":
return "GDScript"
"cs":
return "CSharpScript"
"png", "jpg", "jpeg", "webp":
return "Texture"
"material", "tres":
return "Material"
"wav", "mp3", "ogg":
return "AudioStream"
_:
return "Resource"
# Script commands
func handle_view_script(params):
if not params.has("script_path"):
return {"error": "Missing required parameter: script_path"}
var script_path = params.script_path
var require_exists = params.get("require_exists", true)
if not FileAccess.file_exists(script_path):
if require_exists:
return {"error": "Script file does not exist: " + script_path}
else:
return {"exists": false, "message": "Script file does not exist: " + script_path}
var file = FileAccess.open(script_path, FileAccess.READ)
if file == null:
return {"error": "Failed to open script file: " + script_path}
var content = file.get_as_text()
return {"exists": true, "content": content}
func handle_create_script(params):
if not params.has("script_name"):
return {"error": "Missing required parameter: script_name"}
var script_name = params.script_name
var script_type = params.get("script_type", "Node")
var nam = params.get("namespace", "")
var script_folder = params.get("script_folder", "res://scripts")
var overwrite = params.get("overwrite", false)
var content = params.get("content", "")
# Ensure script has .gd extension
if not script_name.ends_with(".gd"):
script_name += ".gd"
# Create full script path
var script_path = script_folder.path_join(script_name)
# Check if directory exists, create if needed
if not DirAccess.dir_exists_absolute(script_folder):
var error = DirAccess.make_dir_recursive_absolute(script_folder)
if error != OK:
return {"error": "Failed to create script directory: " + script_folder}
# Check if script already exists
if FileAccess.file_exists(script_path) and not overwrite:
return {"error": "Script already exists. Use overwrite=true to replace it."}
# Create script content if not provided
if content == "":
content = _generate_script_template(script_type, nam)
# Write script file
var file = FileAccess.open(script_path, FileAccess.WRITE)
if file == null:
return {"error": "Failed to create script file: " + script_path}
file.store_string(content)
return {"message": "Script created successfully: " + script_path}
# Helper function to generate script template
func _generate_script_template(node_type, nam):
var template = ""
# Add class_name if namespace is provided
if nam != "":
template += "class_name " + nam + "\n\n"
# Add extends
template += "extends " + node_type + "\n\n"
# Add basic structure
template += "# Properties\n\n"
template += "# Called when the node enters the scene tree\n"
template += "func _ready():\n"
template += "\tpass\n\n"
template += "# Called every frame\n"
template += "func _process(delta):\n"
template += "\tpass\n"
return template
func handle_update_script(params):
if not params.has("script_path") or not params.has("content"):
return {"error": "Missing required parameters: script_path and content"}
var script_path = params.script_path
var content = params.content
var create_if_missing = params.get("create_if_missing", false)
var create_folder_if_missing = params.get("create_folder_if_missing", false)
# Check if script exists
if not FileAccess.file_exists(script_path):
if not create_if_missing:
return {"error": "Script file does not exist: " + script_path}
# Create directory if needed
var directory = script_path.get_base_dir()
if create_folder_if_missing and not DirAccess.dir_exists_absolute(directory):
var dir_error = DirAccess.make_dir_recursive_absolute(directory)
if dir_error != OK:
return {"error": "Failed to create directory: " + directory}
# Write script content
var file = FileAccess.open(script_path, FileAccess.WRITE)
if file == null:
return {"error": "Failed to open script file for writing: " + script_path}
file.store_string(content)
return {"message": "Script updated successfully: " + script_path}
func handle_list_scripts(params):
var folder_path = params.get("folder_path", "res://")
var scripts = []
var dir = DirAccess.open(folder_path)
if dir == null:
return {"error": "Unable to access directory: " + folder_path}
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if not dir.current_is_dir():
var extension = file_name.get_extension().to_lower()
if extension == "gd" or extension == "cs":
scripts.append(folder_path.path_join(file_name))
file_name = dir.get_next()
dir.list_dir_end()
return {"scripts": scripts}
# Editor control commands
func handle_editor_control(params):
if not params.has("command"):
return {"error": "Missing required parameter: command"}
var command = params.command
var editor_interface = editor_plugin.get_editor_interface()
match command:
"PLAY":
editor_interface.play_main_scene()
return {"message": "Started playing the main scene"}
"STOP":
editor_interface.stop_playing_scene()
return {"message": "Stopped playing the scene"}
"SAVE":
editor_interface.save_scene()
return {"message": "Scene saved"}
"READ_CONSOLE":
# Godot doesn't have a direct API for reading console output
return {"message": "Console reading not implemented in Godot"}
_:
return {"error": "Unknown editor command: " + command}
# Material commands
func handle_set_material(params):
if not params.has("object_name"):
return {"error": "Missing required parameter: object_name"}
var object_name = params.object_name
var material_name = params.get("material_name", "")
var color = params.get("color", [1.0, 1.0, 1.0, 1.0])
var create_if_missing = params.get("create_if_missing", true)
print("handle_set_material called for node: ", object_name, " color: ", color)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node using the improved node lookup
var node = _find_node(current_scene, object_name)
if not node:
return {"error": "Node not found: " + object_name}
print("Node found: ", node.name, " type: ", node.get_class())
# Check if node can have materials
if not (node is MeshInstance3D or node is CSGShape3D):
return {"error": "Node does not support materials: " + object_name}
var material
# Create or load material
if material_name != "":
var material_path = "res://materials/" + material_name + ".material"
if FileAccess.file_exists(material_path):
# Load existing material
material = load(material_path)
elif create_if_missing:
# Create directory if needed
if not DirAccess.dir_exists_absolute("res://materials"):
DirAccess.make_dir_recursive_absolute("res://materials")
# Create new material
material = StandardMaterial3D.new()
# Set color
if color.size() >= 3:
var albedo_color = Color(color[0], color[1], color[2])
if color.size() >= 4:
albedo_color.a = color[3]
material.albedo_color = albedo_color
# Save material
var error = ResourceSaver.save(material, material_path)
if error != OK:
return {"error": "Failed to save material: " + str(error)}
else:
return {"error": "Material not found and create_if_missing is false"}
else:
# Create instance material
material = StandardMaterial3D.new()
# Set color
if color.size() >= 3:
var albedo_color = Color(color[0], color[1], color[2])
if color.size() >= 4:
albedo_color.a = color[3]
material.albedo_color = albedo_color
# Apply material
if node is MeshInstance3D:
node.material_override = material
elif node is CSGShape3D:
node.material = material
if material_name != "":
return {
"material_name": material_name,
"path": "res://materials/" + material_name + ".material",
"message": "Applied shared material to " + object_name
}
else:
return {
"material_name": "instance_material",
"message": "Applied instance material to " + object_name
}
# Asset import
func handle_import_asset(params):
if not params.has("source_path") or not params.has("target_path"):
return {"error": "Missing required parameters: source_path and target_path"}
var source_path = params.source_path
var target_path = params.target_path
var overwrite = params.get("overwrite", false)
# Ensure target_path starts with res://
if not target_path.begins_with("res://"):
target_path = "res://" + target_path
# Check if source file exists
if not FileAccess.file_exists(source_path):
return {"error": "Source file does not exist: " + source_path}
# Check if target file exists
if FileAccess.file_exists(target_path) and not overwrite:
return {"error": "Target file already exists. Use overwrite=true to replace it."}
# Create target directory if needed
var target_dir = target_path.get_base_dir()
if not DirAccess.dir_exists_absolute(target_dir):
var dir_error = DirAccess.make_dir_recursive_absolute(target_dir)
if dir_error != OK:
return {"error": "Failed to create target directory: " + target_dir}
# Copy the file
var source_file = FileAccess.open(source_path, FileAccess.READ)
if source_file == null:
return {"error": "Failed to open source file: " + source_path}
var target_file = FileAccess.open(target_path, FileAccess.WRITE)
if target_file == null:
return {"error": "Failed to create target file: " + target_path}
target_file.store_buffer(source_file.get_buffer(source_file.get_length()))
return {
"success": true,
"message": "Asset imported successfully: " + target_path
}
func handle_create_packed_scene(params):
"""Create a packed scene (prefab) from an existing node."""
if not params.has("object_name") or not params.has("prefab_path"):
return {"error": "Missing required parameters: object_name and prefab_path"}
var object_name = params.object_name
var prefab_path = params.prefab_path
var overwrite = params.get("overwrite", false)
# Ensure prefab_path starts with res://
if not prefab_path.begins_with("res://"):
prefab_path = "res://" + prefab_path
# Ensure it has .tscn extension
if not prefab_path.ends_with(".tscn"):
prefab_path += ".tscn"
# Check if file exists and handle overwrite
if FileAccess.file_exists(prefab_path) and not overwrite:
return {"error": "Packed scene file already exists. Use overwrite=true to replace it."}
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name
var node = current_scene.find_child(object_name, true, false)
if not node:
return {"error": "Node not found: " + object_name}
# Create directory if needed
var directory = prefab_path.get_base_dir()
if not DirAccess.dir_exists_absolute(directory):
var dir_error = DirAccess.make_dir_recursive_absolute(directory)
if dir_error != OK:
return {"error": "Failed to create directory: " + directory}
# Create packed scene
var packed_scene = PackedScene.new()
var result = packed_scene.pack(node)
if result != OK:
return {"error": "Failed to pack scene: " + str(result)}
# Save packed scene
result = ResourceSaver.save(packed_scene, prefab_path)
if result != OK:
return {"error": "Failed to save packed scene: " + str(result)}
return {
"success": true,
"path": prefab_path,
"message": "Packed scene created successfully from " + object_name
}
func handle_instantiate_prefab(params):
"""Instantiate a packed scene (prefab) into the current scene."""
if not params.has("prefab_path"):
return {"error": "Missing required parameter: prefab_path"}
var prefab_path = params.prefab_path
var position_x = params.get("position_x", 0.0)
var position_y = params.get("position_y", 0.0)
var position_z = params.get("position_z", 0.0)
var rotation_x = params.get("rotation_x", 0.0)
var rotation_y = params.get("rotation_y", 0.0)
var rotation_z = params.get("rotation_z", 0.0)
# Ensure prefab_path starts with res://
if not prefab_path.begins_with("res://"):
prefab_path = "res://" + prefab_path
# Check if file exists
if not FileAccess.file_exists(prefab_path):
return {"error": "Packed scene file does not exist: " + prefab_path}
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Load the packed scene
var scene_resource = load(prefab_path)
if not scene_resource is PackedScene:
return {"error": "Failed to load packed scene: " + prefab_path}
# Instantiate the scene
var instance = scene_resource.instantiate()
if not instance:
return {"error": "Failed to instantiate packed scene"}
# Add to the current scene
current_scene.add_child(instance)
instance.owner = current_scene
# Set transform if it's a spatial node
if instance is Node3D:
instance.position = Vector3(position_x, position_y, position_z)
instance.rotation_degrees = Vector3(rotation_x, rotation_y, rotation_z)
return {
"success": true,
"instance_name": instance.name,
"message": "Packed scene instantiated successfully"
}
func handle_set_property(params):
if not params.has("node_name") or not params.has("property_name") or not params.has("value"):
return {"error": "Missing required parameters: node_name, property_name, and value"}
var node_name = params.node_name
var property_name = params.property_name
var value = params.value
var force_type = params.get("force_type", "")
print("handle_set_property called with node_name: ", node_name, " property_name: ", property_name, " value: ", value)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name or path
var node = null
# Check if it's a path (contains /)
if "/" in node_name:
# Try direct path lookup first
node = current_scene.get_node_or_null(node_name)
if not node:
# Try as relative path
if not node_name.begins_with("/"):
node = current_scene.get_node_or_null("/" + node_name)
else:
# Simple name lookup
node = current_scene.find_child(node_name, true, false)
if node:
print("Node found: ", node.name, " of type: ", node.get_class())
else:
print("Node not found: ", node_name)
return {"error": "Node not found: " + node_name}
# Handle special properties
if property_name == "script":
# Check if script file exists
if not FileAccess.file_exists(value):
return {"error": "Script file not found: " + value}
# Try to load the script
var script_resource = load(value)
if not script_resource:
return {"error": "Failed to load script: " + value}
if not script_resource is GDScript:
return {"error": "Resource is not a GDScript: " + value}
# Set the script
node.set_script(script_resource)
return {"message": "Script set on node '" + node_name + "': " + value}
# Handle other special properties
if property_name == "visible":
node.visible = bool(value)
return {"message": "Property set: visible = " + str(bool(value))}
if property_name == "name":
# Check if name is already taken
var existing = current_scene.find_child(str(value), true, false)
if existing != null and existing != node:
return {"error": "Name already in use by another node"}
node.name = str(value)
return {"message": "Node renamed to: " + str(value)}
# Handle property paths (e.g., "position:x")
var parts = property_name.split(":")
if parts.size() == 2:
var base_property = parts[0]
var sub_property = parts[1]
if base_property in node:
var base_value = node.get(base_property)
# Handle different vector types
if base_value is Vector2 or base_value is Vector3 or base_value is Color:
if sub_property in ["x", "y", "z", "r", "g", "b", "a"]:
# Safety check for Vector2 which doesn't have z
if base_value is Vector2 and sub_property == "z":
return {"error": "Vector2 doesn't have a z component"}
# Safety check for components that don't exist
if sub_property == "z" and not (base_value is Vector3):
return {"error": "Property doesn't have a z component"}
if sub_property in ["r", "g", "b", "a"] and not (base_value is Color):
return {"error": "Only Color has r, g, b, a components"}
# Convert value to float for vector components
var float_value
if typeof(value) == TYPE_STRING:
float_value = float(value)
else:
float_value = float(value)
# Set the appropriate component
match sub_property:
"x":
base_value.x = float_value
"y":
base_value.y = float_value
"z":
base_value.z = float_value
"r":
base_value.r = float_value
"g":
base_value.g = float_value
"b":
base_value.b = float_value
"a":
base_value.a = float_value
# Set the modified vector back to the node
node.set(base_property, base_value)
return {"message": "Property set: " + property_name + " = " + str(float_value)}
return {"error": "Unable to set sub-property: " + property_name}
# Handle regular properties
if not property_name in node:
return {"error": "Property not found on node: " + property_name}
# Get the current property value to determine its type
var current_value = node.get(property_name)
print("Current value type: ", typeof(current_value))
var converted_value = value
# If force_type is specified, use that for conversion
if force_type != "":
match force_type:
"bool":
if typeof(value) == TYPE_STRING:
converted_value = (value.to_lower() == "true")
else:
converted_value = bool(value)
"int":
converted_value = int(value)
"float":
converted_value = float(value)
"string":
converted_value = str(value)
"Vector2":
if typeof(value) == TYPE_ARRAY and value.size() >= 2:
converted_value = Vector2(float(value[0]), float(value[1]))
"Vector3":
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
converted_value = Vector3(float(value[0]), float(value[1]), float(value[2]))
"Color":
if typeof(value) == TYPE_ARRAY:
if value.size() >= 4:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
elif value.size() >= 3:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
else:
# Try to convert the value to match the current property type
match typeof(current_value):
TYPE_BOOL:
if typeof(value) == TYPE_STRING:
converted_value = (value.to_lower() == "true")
else:
converted_value = bool(value)
TYPE_INT:
converted_value = int(value)
TYPE_FLOAT:
converted_value = float(value)
TYPE_STRING:
converted_value = str(value)
TYPE_VECTOR2:
if typeof(value) == TYPE_ARRAY and value.size() >= 2:
converted_value = Vector2(float(value[0]), float(value[1]))
TYPE_VECTOR3:
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
converted_value = Vector3(float(value[0]), float(value[1]), float(value[2]))
TYPE_COLOR:
if typeof(value) == TYPE_ARRAY:
if value.size() >= 4:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
elif value.size() >= 3:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
print("Converted value: ", converted_value, " of type: ", typeof(converted_value))
# Try to set the property
var previous_value = node.get(property_name)
# Use set to safely call the setter
var error = null
node.set(property_name, converted_value)
# Check if the property actually changed
var new_value = node.get(property_name)
# Handle type-safe comparison - don't compare different types directly
var values_different = false
if typeof(new_value) == typeof(converted_value):
values_different = (new_value != converted_value)
else:
# Different types means they're different values
values_different = true
if new_value == previous_value and values_different:
return {"warning": "Property might be read-only or conversion failed: " + property_name, "previous_value": previous_value, "attempted_value": converted_value}
return {"message": "Property set: " + property_name + " = " + str(converted_value)}
#func handle_set_property(params):
#if not params.has("node_name") or not params.has("property_name") or not params.has("value"):
#return {"error": "Missing required parameters: node_name, property_name, and value"}
#
#var node_name = params.node_name
#var property_name = params.property_name
#var value = params.value
#var force_type = params.get("force_type", "")
#
#var editor_interface = editor_plugin.get_editor_interface()
#var current_scene = editor_interface.get_edited_scene_root()
#
#if current_scene == null:
#return {"error": "No scene is currently open"}
#
## Find node by name
#var node = current_scene.find_child(node_name, true, false)
#if not node:
#return {"error": "Node not found: " + node_name}
#
## Handle special properties
#if property_name == "script":
## Check if script file exists
#if not FileAccess.file_exists(value):
#return {"error": "Script file not found: " + value}
#
## Try to load the script
#var script_resource = load(value)
#if not script_resource:
#return {"error": "Failed to load script: " + value}
#
#if not script_resource is GDScript:
#return {"error": "Resource is not a GDScript: " + value}
#
## Set the script
#node.set_script(script_resource)
#return {"message": "Script set on node '" + node_name + "': " + value}
#
## Handle other special properties
#if property_name == "visible":
#node.visible = bool(value)
#return {"message": "Property set: visible = " + str(bool(value))}
#
#if property_name == "name":
## Check if name is already taken
#var existing = current_scene.find_child(str(value), true, false)
#if existing != null and existing != node:
#return {"error": "Name already in use by another node"}
#node.name = str(value)
#return {"message": "Node renamed to: " + str(value)}
#
## Handle property paths (e.g., "position:x")
#var parts = property_name.split(":")
#if parts.size() == 2:
#var base_property = parts[0]
#var sub_property = parts[1]
#
#if base_property in node:
#var base_value = node.get(base_property)
#
## Handle different vector types
#if base_value is Vector2 or base_value is Vector3 or base_value is Color:
#if sub_property in ["x", "y", "z", "r", "g", "b", "a"]:
## Safety check for Vector2 which doesn't have z
#if base_value is Vector2 and sub_property == "z":
#return {"error": "Vector2 doesn't have a z component"}
#
## Safety check for components that don't exist
#if sub_property == "z" and not (base_value is Vector3):
#return {"error": "Property doesn't have a z component"}
#
#if sub_property in ["r", "g", "b", "a"] and not (base_value is Color):
#return {"error": "Only Color has r, g, b, a components"}
#
## Convert value to float for vector components
#var float_value
#if typeof(value) == TYPE_STRING:
#float_value = float(value)
#else:
#float_value = float(value)
#
## Set the appropriate component
#match sub_property:
#"x": base_value.x = float_value
#"y": base_value.y = float_value
#"z": base_value.z = float_value
#"r": base_value.r = float_value
#"g": base_value.g = float_value
#"b": base_value.b = float_value
#"a": base_value.a = float_value
#
## Set the modified vector back to the node
#node.set(base_property, base_value)
#return {"message": "Property set: " + property_name + " = " + str(float_value)}
#
#return {"error": "Unable to set sub-property: " + property_name}
#
## Handle regular properties
#if not property_name in node:
#return {"error": "Property not found on node: " + property_name}
#
## Get the current property value to determine its type
#var current_value = node.get(property_name)
#var converted_value = value
#
## If force_type is specified, use that for conversion
#if force_type != "":
#match force_type:
#"bool":
#if typeof(value) == TYPE_STRING:
#converted_value = (value.to_lower() == "true")
#else:
#converted_value = bool(value)
#"int":
#converted_value = int(value)
#"float":
#converted_value = float(value)
#"string":
#converted_value = str(value)
#"Vector2":
#if typeof(value) == TYPE_ARRAY and value.size() >= 2:
#converted_value = Vector2(float(value[0]), float(value[1]))
#"Vector3":
#if typeof(value) == TYPE_ARRAY and value.size() >= 3:
#converted_value = Vector3(float(value[0]), float(value[1]), float(value[2]))
#"Color":
#if typeof(value) == TYPE_ARRAY:
#if value.size() >= 4:
#converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
#elif value.size() >= 3:
#converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
#else:
## Try to convert the value to match the current property type
#match typeof(current_value):
#TYPE_BOOL:
#if typeof(value) == TYPE_STRING:
#converted_value = (value.to_lower() == "true")
#else:
#converted_value = bool(value)
#TYPE_INT:
#converted_value = int(value)
#TYPE_FLOAT:
#converted_value = float(value)
#TYPE_STRING:
#converted_value = str(value)
#TYPE_VECTOR2:
#if typeof(value) == TYPE_ARRAY and value.size() >= 2:
#converted_value = Vector2(float(value[0]), float(value[1]))
#TYPE_VECTOR3:
#if typeof(value) == TYPE_ARRAY and value.size() >= 3:
#converted_value = Vector3(float(value[0]), float(value[1]), float(value[2]))
#TYPE_COLOR:
#if typeof(value) == TYPE_ARRAY:
#if value.size() >= 4:
#converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
#elif value.size() >= 3:
#converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
#
## Try to set the property
#var previous_value = node.get(property_name)
#
## Use callv to safely call the setter
#node.set(property_name, converted_value)
#
## Check if the property actually changed
#var new_value = node.get(property_name)
#if new_value == previous_value and new_value != converted_value:
#return {"warning": "Property might be read-only or conversion failed: " + property_name, "previous_value": previous_value, "attempted_value": converted_value}
#
#return {"message": "Property set: " + property_name + " = " + str(converted_value)}
func handle_set_parent(params):
if not params.has("child_name") or not params.has("parent_name"):
return {"error": "Missing required parameters: child_name and parent_name"}
var child_name = params.child_name
var parent_name = params.parent_name
var keep_global_transform = params.get("keep_global_transform", true)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find child node (search the entire scene)
var child_node = current_scene.find_child(child_name, true, false)
if not child_node:
return {"error": "Child node not found: " + child_name}
# Find parent node (search the entire scene)
var parent_node
if parent_name == "root":
parent_node = current_scene
else:
parent_node = current_scene.find_child(parent_name, true, false)
if not parent_node:
return {"error": "Parent node not found: " + parent_name}
# Store the global transform if needed
var global_transform = null
if child_node is Node3D and keep_global_transform:
global_transform = child_node.global_transform
elif child_node is Node2D and keep_global_transform:
global_transform = child_node.global_transform
# Get the original parent
var original_parent = child_node.get_parent()
if original_parent:
# Remove from original parent
original_parent.remove_child(child_node)
# Add to new parent
parent_node.add_child(child_node)
# Ensure ownership is set correctly
child_node.owner = current_scene
# Set all children's owner recursively
_set_owner_recursive(child_node, current_scene)
# Restore global transform if needed
if child_node is Node3D and global_transform and keep_global_transform:
child_node.global_transform = global_transform
elif child_node is Node2D and global_transform and keep_global_transform:
child_node.global_transform = global_transform
return {"message": "Set parent of '" + child_name + "' to '" + parent_name + "'"}
# Helper function to set owner recursively
func _set_owner_recursive(node, owner):
for child in node.get_children():
child.owner = owner
_set_owner_recursive(child, owner)
func handle_create_child_object(params):
"""Create a new object as a child of an existing node."""
if not params.has("parent_name"):
return {"error": "Missing required parameter: parent_name"}
var parent_name = params.parent_name
var type = params.get("type", "EMPTY")
var name = params.get("name", "")
var location = params.get("location", [0, 0, 0])
var rotation = params.get("rotation", [0, 0, 0])
var scale = params.get("scale", [1, 1, 1])
var replace_if_exists = params.get("replace_if_exists", false)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find parent node with proper path handling
var parent_node = null
if parent_name == "root":
parent_node = current_scene
else:
# Check if it's a path (contains /)
if "/" in parent_name:
# Try direct path lookup first
parent_node = current_scene.get_node_or_null(parent_name)
# If that fails, try with leading slash
if not parent_node and not parent_name.begins_with("/"):
parent_node = current_scene.get_node_or_null("/" + parent_name)
# If still not found, try parsing the path components
if not parent_node:
var parts = parent_name.split("/")
var current = current_scene
for part in parts:
if part == "":
continue
var found = false
for child in current.get_children():
if child.name == part:
current = child
found = true
break
if not found:
# Try searching recursively for this part
var found_node = current.find_child(part, true, false)
if found_node:
current = found_node
else:
# Part not found, path is invalid
return {"error": "Could not find part '" + part + "' in path: " + parent_name}
parent_node = current
else:
# Simple name lookup for non-path names
parent_node = current_scene.find_child(parent_name, true, false)
if not parent_node:
return {"error": "Parent node not found: " + parent_name}
# Check if name already exists and handle replacement
if name != "":
var existing_node = current_scene.find_child(name, true, false)
if existing_node and not replace_if_exists:
return {"error": "Node with name '" + name + "' already exists. Use replace_if_exists=true to replace it."}
elif existing_node and replace_if_exists:
existing_node.queue_free()
# Create the node based on type
var node
var upper_type = type.to_upper()
# Handle common 3D node types
match upper_type:
"NODE", "EMPTY":
node = Node3D.new()
"NODE3D":
node = Node3D.new()
"SPATIAL": # For compatibility with Godot 3.x terminology
node = Node3D.new()
"MESH", "MESHINSTANCE3D":
node = MeshInstance3D.new()
"CUBE", "BOX":
node = MeshInstance3D.new()
node.mesh = BoxMesh.new()
"SPHERE":
node = MeshInstance3D.new()
node.mesh = SphereMesh.new()
"CYLINDER":
node = MeshInstance3D.new()
node.mesh = CylinderMesh.new()
"PLANE":
node = MeshInstance3D.new()
node.mesh = PlaneMesh.new()
"CAMERA", "CAMERA3D":
node = Camera3D.new()
"LIGHT", "DIRECTIONALLIGHT", "DIRECTIONALLIGHT3D":
node = DirectionalLight3D.new()
"SPOTLIGHT", "SPOTLIGHT3D":
node = SpotLight3D.new()
"OMNILIGHT", "OMNILIGHT3D":
node = OmniLight3D.new()
"RIGIDBODY", "RIGIDBODY3D":
node = RigidBody3D.new()
"STATICBODY", "STATICBODY3D":
node = StaticBody3D.new()
"CHARACTERBODY", "CHARACTERBODY3D":
node = CharacterBody3D.new()
"AREA", "AREA3D":
node = Area3D.new()
"COLLISION", "COLLISIONSHAPE3D":
node = CollisionShape3D.new()
# Add a default sphere shape
node.shape = SphereShape3D.new()
# Handle common 2D node types
"NODE2D":
node = Node2D.new()
"SPRITE", "SPRITE2D":
node = Sprite2D.new()
"CAMERA2D":
node = Camera2D.new()
"AREA2D":
node = Area2D.new()
"COLLISION2D", "COLLISIONSHAPE2D":
node = CollisionShape2D.new()
# Add a default circle shape
node.shape = CircleShape2D.new()
"RIGIDBODY2D":
node = RigidBody2D.new()
"STATICBODY2D":
node = StaticBody2D.new()
"CHARACTERBODY2D":
node = CharacterBody2D.new()
# Handle UI node types
"CONTROL":
node = Control.new()
"PANEL":
node = Panel.new()
"BUTTON":
node = Button.new()
"LABEL":
node = Label.new()
"LINEEDIT":
node = LineEdit.new()
"TEXTEDIT":
node = TextEdit.new()
"CONTAINER":
node = Container.new()
"VBOX", "VBOXCONTAINER":
node = VBoxContainer.new()
"HBOX", "HBOXCONTAINER":
node = HBoxContainer.new()
_:
# Try to create the node directly by class name using ClassDB
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
node = ClassDB.instantiate(type)
else:
return {"error": "Unsupported object type: " + type}
# Set node name if provided
if name != "":
node.name = name
# Add to parent node directly
parent_node.add_child(node)
node.owner = current_scene
# Recursively set ownership for all children
_set_owner_recursive(node, current_scene)
# Set transform for Node3D or Node2D objects
if node is Node3D:
node.position = Vector3(location[0], location[1], location[2])
node.rotation_degrees = Vector3(rotation[0], rotation[1], rotation[2])
node.scale = Vector3(scale[0], scale[1], scale[2])
elif node is Node2D:
node.position = Vector2(location[0], location[1])
node.rotation_degrees = rotation[0]
node.scale = Vector2(scale[0], scale[1])
# In the JSON output, include the full path to the parent to help with debugging
var parent_path = current_scene.get_path_to(parent_node)
return {
"name": node.name,
"type": node.get_class(),
"path": current_scene.get_path_to(node),
"parent": parent_node.name,
"parent_path": parent_path
}
#func handle_create_child_object(params):
#"""Create a new object as a child of an existing node."""
#if not params.has("parent_name"):
#return {"error": "Missing required parameter: parent_name"}
#
#var parent_name = params.parent_name
#var type = params.get("type", "EMPTY")
#var name = params.get("name", "")
#var location = params.get("location", [0, 0, 0])
#var rotation = params.get("rotation", [0, 0, 0])
#var scale = params.get("scale", [1, 1, 1])
#var replace_if_exists = params.get("replace_if_exists", false)
#
#var editor_interface = editor_plugin.get_editor_interface()
#var current_scene = editor_interface.get_edited_scene_root()
#
#if current_scene == null:
#return {"error": "No scene is currently open"}
#
## Find parent node
#var parent_node
#if parent_name == "root":
#parent_node = current_scene
#else:
#parent_node = current_scene.find_child(parent_name, true, false)
#
#if not parent_node:
#return {"error": "Parent node not found: " + parent_name}
#
## Check if name already exists and handle replacement
#if name != "":
#var existing_node = current_scene.find_child(name, true, false)
#if existing_node and not replace_if_exists:
#return {"error": "Node with name '" + name + "' already exists. Use replace_if_exists=true to replace it."}
#elif existing_node and replace_if_exists:
#existing_node.queue_free()
#
## Create the node based on type (same logic as handle_create_object)
#var node
#var upper_type = type.to_upper()
#
## Handle common 3D node types
#match upper_type:
#"NODE", "EMPTY":
#node = Node3D.new()
## (rest of the type matching code from handle_create_object)
## ...
#_:
## Try to create the node directly by class name using ClassDB
#if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
#node = ClassDB.instantiate(type)
#else:
#return {"error": "Unsupported object type: " + type}
#
## Set node name if provided
#if name != "":
#node.name = name
#
## Add to parent node directly
#parent_node.add_child(node)
#node.owner = current_scene
#
## Set transform for Node3D or Node2D objects
#if node is Node3D:
#node.position = Vector3(location[0], location[1], location[2])
#node.rotation_degrees = Vector3(rotation[0], rotation[1], rotation[2])
#node.scale = Vector3(scale[0], scale[1], scale[2])
#elif node is Node2D:
#node.position = Vector2(location[0], location[1])
#node.rotation_degrees = rotation[0]
#node.scale = Vector2(scale[0], scale[1])
#
#return {
#"name": node.name,
#"type": node.get_class(),
#"path": current_scene.get_path_to(node),
#"parent": parent_name
#}
func handle_set_mesh(params):
"""Create and set a mesh on a MeshInstance3D node."""
if not params.has("node_name") or not params.has("mesh_type"):
return {"error": "Missing required parameters: node_name and mesh_type"}
var node_name = params.node_name
var mesh_type = params.mesh_type
print("handle_set_mesh called for node: ", node_name, " mesh_type: ", mesh_type)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node using the improved node lookup
var node = _find_node(current_scene, node_name)
if not node:
return {"error": "Node not found: " + node_name}
print("Node found: ", node.name, " type: ", node.get_class())
# Check if the node can have a mesh property
if not node is MeshInstance3D:
return {"error": "Node is not a MeshInstance3D: " + node_name}
# Create mesh based on type
var mesh
match mesh_type.to_upper():
"CAPSULEMESH":
mesh = CapsuleMesh.new()
if params.has("radius"):
mesh.radius = float(params.radius)
if params.has("height"):
mesh.height = float(params.height)
"BOXMESH":
mesh = BoxMesh.new()
if params.has("size"):
var size = params.size
mesh.size = Vector3(float(size[0]), float(size[1]), float(size[2]))
"SPHEREMESH":
mesh = SphereMesh.new()
if params.has("radius"):
mesh.radius = float(params.radius)
"CYLINDERMESH":
mesh = CylinderMesh.new()
if params.has("radius"):
mesh.radius = float(params.radius)
if params.has("height"):
mesh.height = float(params.height)
"PLANEMESH":
mesh = PlaneMesh.new()
if params.has("size"):
var size = params.size
mesh.size = Vector2(float(size[0]), float(size[1]))
_:
return {"error": "Unsupported mesh type: " + mesh_type}
# Set the mesh
node.mesh = mesh
return {"message": "Set " + mesh_type + " on " + node_name}
func _find_node(root, name_or_path):
# Check if it's a path (contains /)
if "/" in name_or_path:
# Try direct path lookup first
var node = root.get_node_or_null(name_or_path)
if node:
return node
# Try as relative path
if not name_or_path.begins_with("/"):
node = root.get_node_or_null("/" + name_or_path)
if node:
return node
# Try all combinations of path separators
var parts = name_or_path.split("/")
var current = root
for part in parts:
# Look for the next part in current node's children
var found = false
for child in current.get_children():
if child.name == part:
current = child
found = true
break
if not found:
# Try searching recursively
var found_node = current.find_child(part, true, false)
if found_node:
current = found_node
else:
return null
return current
else:
# Simple name lookup
return root.find_child(name_or_path, true, false)
func handle_set_collision_shape(params):
"""Create and set a collision shape on a CollisionShape3D or CollisionShape2D node."""
if not params.has("node_name") or not params.has("shape_type"):
return {"error": "Missing required parameters: node_name and shape_type"}
var node_name = params.node_name
var shape_type = params.shape_type
var shape_params = params.get("shape_params", {})
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name or path
var node = _find_node(current_scene, node_name)
if not node:
return {"error": "Node not found: " + node_name}
# Check if the node can have a shape property
if not (node is CollisionShape3D or node is CollisionShape2D):
return {"error": "Node is not a CollisionShape: " + node_name}
# Create shape based on type
var shape
match shape_type.to_upper():
"CAPSULESHAPE3D":
shape = CapsuleShape3D.new()
if shape_params.has("radius"):
shape.radius = float(shape_params.radius)
if shape_params.has("height"):
shape.height = float(shape_params.height)
"BOXSHAPE3D":
shape = BoxShape3D.new()
if shape_params.has("size"):
var size = shape_params.size
shape.size = Vector3(float(size[0]), float(size[1]), float(size[2]))
"SPHERESHAPE3D":
shape = SphereShape3D.new()
if shape_params.has("radius"):
shape.radius = float(shape_params.radius)
"CYLINDERSHAPE3D":
shape = CylinderShape3D.new()
if shape_params.has("radius"):
shape.radius = float(shape_params.radius)
if shape_params.has("height"):
shape.height = float(shape_params.height)
"WORLDBOUNDARYSHAPE3D":
shape = WorldBoundaryShape3D.new()
# 2D Shapes
"CIRCLESHAPE2D":
shape = CircleShape2D.new()
if shape_params.has("radius"):
shape.radius = float(shape_params.radius)
"RECTANGLESHAPE2D":
shape = RectangleShape2D.new()
if shape_params.has("size"):
var size = shape_params.size
shape.size = Vector2(float(size[0]), float(size[1]))
"CAPSULESHAPE2D":
shape = CapsuleShape2D.new()
if shape_params.has("radius"):
shape.radius = float(shape_params.radius)
if shape_params.has("height"):
shape.height = float(shape_params.height)
_:
return {"error": "Unsupported shape type: " + shape_type}
# Set the shape
node.shape = shape
return {"message": "Set " + shape_type + " on " + node_name}
#func handle_set_nested_property(params):
#"""Set a nested property like environment/sky/sky_material on a node."""
#if not params.has("node_name") or not params.has("property_name") or not params.has("value"):
#return {"error": "Missing required parameters: node_name, property_name, and value"}
#
#var node_name = params.node_name
#var property_path = params.property_name
#var value = params.value
#var value_type = params.get("value_type", "")
#
#var editor_interface = editor_plugin.get_editor_interface()
#var current_scene = editor_interface.get_edited_scene_root()
#
#if current_scene == null:
#return {"error": "No scene is currently open"}
#
## Find node by name or path
#var node = null
#
## Check if it's a path (contains /)
#if "/" in node_name:
## Try direct path lookup first
#node = current_scene.get_node_or_null(node_name)
#
## If that fails, try with leading slash
#if not node and not node_name.begins_with("/"):
#node = current_scene.get_node_or_null("/" + node_name)
#
## Try other lookup methods if needed...
#else:
## Simple name lookup for non-path names
#node = current_scene.find_child(node_name, true, false)
#
#if not node:
#return {"error": "Node not found: " + node_name}
#
## Split the property path
#var property_parts = property_path.split("/")
#
## Handle special cases for common node types
#if node is WorldEnvironment:
## Make sure the environment exists
#if not node.environment:
#node.environment = Environment.new()
#
## Handle environment properties
#if property_parts.size() >= 1 and property_parts[0] == "environment":
#var env = node.environment
#
## Handle special case for sky material
#if property_parts.size() >= 3 and property_parts[1] == "sky" and property_parts[2] == "sky_material":
## Make sure sky exists
#if not env.sky:
#env.sky = Sky.new()
#
#if property_parts.size() == 3:
## Create the material based on type
#var material = null
#if value == "ProceduralSkyMaterial":
#material = ProceduralSkyMaterial.new()
#elif value == "PanoramaSkyMaterial":
#material = PanoramaSkyMaterial.new()
#elif value == "PhysicalSkyMaterial":
#material = PhysicalSkyMaterial.new()
#else:
#return {"error": "Unknown sky material type: " + str(value)}
#
## Set the sky material
#env.sky.sky_material = material
#return {"message": "Set sky material to " + str(value)}
#
## Handle sky material properties (e.g., environment/sky/sky_material/sky_top_color)
#elif property_parts.size() >= 4 and env.sky.sky_material:
#var material = env.sky.sky_material
#var mat_prop = property_parts[3]
#
## Verify property exists on the material
#if not mat_prop in material:
#return {"error": "Property not found on sky material: " + mat_prop}
#
## Handle color properties
#if typeof(material.get(mat_prop)) == TYPE_COLOR:
#if typeof(value) == TYPE_ARRAY and value.size() >= 3:
#if value.size() >= 4:
#material.set(mat_prop, Color(value[0], value[1], value[2], value[3]))
#else:
#material.set(mat_prop, Color(value[0], value[1], value[2]))
#return {"message": "Set sky material property " + mat_prop + " to " + str(value)}
#else:
## Set other property types
#material.set(mat_prop, value)
#return {"message": "Set sky material property " + mat_prop + " to " + str(value)}
#
## Handle direct environment properties
#elif property_parts.size() == 2:
#var prop = property_parts[1]
#
## Map common property names to actual Godot property names
#match prop:
#"background_color":
#prop = "background_color"
#"ambient_light_color":
#prop = "ambient_light_color"
#"fog_color":
#prop = "fog_color"
#"background_mode":
#prop = "background_mode"
## Add more mappings as needed
#
## Verify property exists
#if not prop in env:
#var available_props = []
#for p in ["background_color", "ambient_light_color", "fog_color", "background_mode",
#"fog_enabled", "fog_density", "glow_enabled", "glow_intensity",
#"adjustment_enabled", "tonemap_mode"]:
#if p in env:
#available_props.append(p)
#return {"error": "Property not found on environment: " + prop +
#". Available properties include: " + str(available_props)}
#
## Convert value based on property type
#var converted_value = value
#var current_value = env.get(prop)
#
## Handle different types
#if typeof(current_value) == TYPE_COLOR:
#if typeof(value) == TYPE_ARRAY and value.size() >= 3:
#if value.size() >= 4:
#converted_value = Color(value[0], value[1], value[2], value[3])
#else:
#converted_value = Color(value[0], value[1], value[2])
#elif typeof(current_value) == TYPE_BOOL:
#if typeof(value) == TYPE_STRING:
#converted_value = (value.to_lower() == "true")
#else:
#converted_value = bool(value)
#elif typeof(current_value) == TYPE_INT:
#converted_value = int(value)
#elif typeof(current_value) == TYPE_FLOAT:
#converted_value = float(value)
#
## Set the property
#env.set(prop, converted_value)
#return {"message": "Set environment property " + prop + " to " + str(converted_value)}
#
## Handle environment sub-objects (e.g., environment/fog/enabled)
#elif property_parts.size() == 3:
#var sub_obj = property_parts[1]
#var sub_prop = property_parts[2]
#
## Map common property paths
#if sub_obj == "fog" and sub_prop == "enabled":
#env.fog_enabled = bool(value)
#return {"message": "Set fog_enabled to " + str(bool(value))}
#elif sub_obj == "glow" and sub_prop == "enabled":
#env.glow_enabled = bool(value)
#return {"message": "Set glow_enabled to " + str(bool(value))}
#elif sub_obj == "ambient_light" and sub_prop == "color":
#if typeof(value) == TYPE_ARRAY and value.size() >= 3:
#env.ambient_light_color = Color(value[0], value[1], value[2])
#return {"message": "Set ambient_light_color to " + str(value)}
#
## Generic property mapping attempt
#var full_prop = sub_obj + "_" + sub_prop
#if full_prop in env:
#env.set(full_prop, value)
#return {"message": "Set " + full_prop + " to " + str(value)}
#
#return {"error": "Unsupported environment property path: " + property_path}
#
## Handle other node types here
## ...
#
#return {"error": "Unsupported nested property path: " + property_path}
func handle_set_nested_property(params):
"""Set a nested property like environment/sky/sky_material on a node."""
if not params.has("node_name") or not params.has("property_name") or not params.has("value"):
return {"error": "Missing required parameters: node_name, property_name, and value"}
var node_name = params.node_name
var property_path = params.property_name
var value = params.value
var value_type = params.get("value_type", "")
print("Setting nested property: ", property_path, " to ", value, " on ", node_name)
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Find node by name or path
var node = null
# Check if it's a path (contains /)
if "/" in node_name:
# Try direct path lookup first
node = current_scene.get_node_or_null(node_name)
# If that fails, try with leading slash
if not node and not node_name.begins_with("/"):
node = current_scene.get_node_or_null("/" + node_name)
# Try parsing the path manually if needed
if not node:
# Try other path resolution methods...
pass
else:
# Simple name lookup for non-path names
node = current_scene.find_child(node_name, true, false)
if not node:
return {"error": "Node not found: " + node_name}
# Split the property path
var property_parts = property_path.split("/")
# Handle WorldEnvironment properties
if node is WorldEnvironment:
return _handle_worldenvironment_properties(node, property_parts, value)
# Add other node type handlers here
# Default case for simple properties
return {"error": "Unsupported node type for nested properties: " + node.get_class()}
func _handle_worldenvironment_properties(node, property_parts, value):
"""Handle nested properties for WorldEnvironment nodes."""
# Make sure the environment exists
if not node.environment:
node.environment = Environment.new()
# Get environment resource for easier access
var env = node.environment
# First level should be "environment"
if property_parts.size() < 1 or property_parts[0] != "environment":
return {"error": "WorldEnvironment properties must start with 'environment/'"}
# Handle sky material properties (4-part paths)
if property_parts.size() == 4 and property_parts[1] == "sky" and property_parts[2] == "sky_material":
# Make sure sky exists
if not env.sky:
env.sky = Sky.new()
# Make sure sky material exists
if not env.sky.sky_material:
env.sky.sky_material = ProceduralSkyMaterial.new()
var material = env.sky.sky_material
var property_name = property_parts[3]
# Debug info
print("Trying to set sky material property: ", property_name)
print("Material class: ", material.get_class())
# Direct property handling for cloud properties
if property_name == "use_clouds" or property_name == "clouds_enabled":
# Enable/disable clouds directly
material.use_clouds = bool(value)
return {"message": "Set use_clouds to " + str(bool(value))}
elif property_name == "cloud_color":
# Set cloud color directly
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
material.cloud_color = Color(float(value[0]), float(value[1]), float(value[2]))
return {"message": "Set cloud_color to " + str(material.cloud_color)}
elif property_name == "cloud_coverage":
# Set cloud coverage directly
material.cloud_coverage = float(value)
return {"message": "Set cloud_coverage to " + str(float(value))}
elif property_name == "cloud_size":
# Set cloud size directly
material.cloud_size = float(value)
return {"message": "Set cloud_size to " + str(float(value))}
# Handle other properties with the property map
var property_map = {
# Sky colors
"sky_top_color": "sky_top_color",
"sky_horizon_color": "sky_horizon_color",
"sky_curve": "sky_curve",
"ground_horizon_color": "ground_horizon_color",
"ground_bottom_color": "ground_bottom_color",
"ground_curve": "ground_curve",
# Sun properties
"sun_angle_max": "sun_angle_max",
"sun_curve": "sun_curve"
}
# Check if property name needs to be mapped
if property_name in property_map:
property_name = property_map[property_name]
# Try to set the property with property-specific type conversion
var current_value = material.get(property_name)
var converted_value = value
# Handle different property types
match typeof(current_value):
TYPE_BOOL:
converted_value = bool(value)
TYPE_INT:
converted_value = int(value)
TYPE_FLOAT:
converted_value = float(value)
TYPE_COLOR:
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
if value.size() >= 4:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
else:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
# Set the property
material.set(property_name, converted_value)
return {"message": "Set sky material property " + property_name + " to " + str(converted_value)}
return {"error": "Unknown sky material property: " + property_name + ". Available cloud properties: use_clouds, cloud_color, cloud_coverage, cloud_size"}
# Handle sky material type (3-part paths)
elif property_parts.size() == 3 and property_parts[1] == "sky" and property_parts[2] == "sky_material":
# Make sure sky exists
if not env.sky:
env.sky = Sky.new()
# Create the material based on type
var material = null
if value == "ProceduralSkyMaterial":
material = ProceduralSkyMaterial.new()
elif value == "PanoramaSkyMaterial":
material = PanoramaSkyMaterial.new()
elif value == "PhysicalSkyMaterial":
material = PhysicalSkyMaterial.new()
else:
return {"error": "Unknown sky material type: " + str(value)}
# Set the sky material
env.sky.sky_material = material
return {"message": "Set sky material to " + str(value)}
# Handle direct environment properties (2-part paths)
elif property_parts.size() == 2:
var property_name = property_parts[1]
# Map common property name variations to actual property names
var property_map = {
"background": "background_mode",
"ambient_light_color": "ambient_light_color",
"background_color": "background_color",
"fog_enabled": "fog_enabled",
"glow_enabled": "glow_enabled",
"glow_intensity": "glow_intensity",
"fog_density": "fog_density",
"fog_color": "fog_color",
"volumetric_fog_enabled": "volumetric_fog_enabled",
"volumetric_fog_density": "volumetric_fog_density",
"volumetric_fog_albedo": "volumetric_fog_albedo"
}
# Check if we need to map the property name
if property_name in property_map:
property_name = property_map[property_name]
# Verify property exists
if not property_name in env:
return {"error": "Property not found on environment: " + property_name}
# Convert value based on property type
var converted_value = value
var current_value = env.get(property_name)
# Handle different property types
match typeof(current_value):
TYPE_BOOL:
converted_value = bool(value)
TYPE_INT:
converted_value = int(value)
TYPE_FLOAT:
converted_value = float(value)
TYPE_COLOR:
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
if value.size() >= 4:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]), float(value[3]))
else:
converted_value = Color(float(value[0]), float(value[1]), float(value[2]))
# Set the property
env.set(property_name, converted_value)
return {"message": "Set environment property " + property_name + " to " + str(converted_value)}
# Handle fog properties (3-part paths)
elif property_parts.size() == 3 and property_parts[1] == "fog":
var prop = property_parts[2]
if prop == "enabled":
env.fog_enabled = bool(value)
return {"message": "Set fog_enabled to " + str(bool(value))}
elif prop == "density":
env.fog_density = float(value)
return {"message": "Set fog_density to " + str(float(value))}
elif prop == "color":
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
env.fog_color = Color(float(value[0]), float(value[1]), float(value[2]))
return {"message": "Set fog_color to " + str(env.fog_color)}
else:
# Try a direct combined property name
var combined_prop = "fog_" + prop
if combined_prop in env:
env.set(combined_prop, value)
return {"message": "Set " + combined_prop + " to " + str(value)}
return {"error": "Unknown fog property: " + prop}
# Handle volumetric fog properties (3-part paths)
elif property_parts.size() == 3 and property_parts[1] == "volumetric_fog":
var prop = property_parts[2]
if prop == "enabled":
env.volumetric_fog_enabled = bool(value)
return {"message": "Set volumetric_fog_enabled to " + str(bool(value))}
elif prop == "density":
env.volumetric_fog_density = float(value)
return {"message": "Set volumetric_fog_density to " + str(float(value))}
elif prop == "albedo":
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
env.volumetric_fog_albedo = Color(float(value[0]), float(value[1]), float(value[2]))
return {"message": "Set volumetric_fog_albedo to " + str(env.volumetric_fog_albedo)}
else:
# Try a direct combined property name
var combined_prop = "volumetric_fog_" + prop
if combined_prop in env:
env.set(combined_prop, value)
return {"message": "Set " + combined_prop + " to " + str(value)}
return {"error": "Unknown volumetric fog property: " + prop}
# Other environment properties can be added here
return {"error": "Unsupported property path: " + property_parts.join("/")}
# Add the following function:
func handle_delete_script(params):
if not params.has("script_path"):
return {"error": "Missing required parameter: script_path"}
var script_path = params.script_path
# Check if file exists
if not FileAccess.file_exists(script_path):
return {"error": "Script file does not exist: " + script_path}
# Delete the file
var error = DirAccess.remove_absolute(script_path)
if error != OK:
return {"error": "Failed to delete script file: " + script_path + " (Error code: " + str(error) + ")"}
return {
"message": "Script deleted successfully: " + script_path
}
# Add the following function near the handle_delete_script function:
func handle_delete_file(params):
if not params.has("file_path"):
return {"error": "Missing required parameter: file_path"}
var file_path = params.file_path
# Check if file exists
if not FileAccess.file_exists(file_path):
return {"error": "File does not exist: " + file_path}
# Delete the file
var error = DirAccess.remove_absolute(file_path)
if error != OK:
return {"error": "Failed to delete file: " + file_path + " (Error code: " + str(error) + ")"}
return {
"message": "File deleted successfully: " + file_path
}
func handle_show_message(params):
if not params.has("message"):
return {"error": "Missing required parameter: message"}
var message = params.message
# Show a message dialog
editor_plugin.get_editor_interface().show_message_notification(message)
return {"message": "Message shown successfully"}
func handle_reimport_asset(params):
if not params.has("asset_path"):
return {"error": "Missing required parameter: asset_path"}
var asset_path = params.asset_path
# Force a filesystem scan to detect the new file
var editor_filesystem = editor_plugin.get_editor_interface().get_resource_filesystem()
if editor_filesystem:
editor_filesystem.scan()
# Also scan the specific directory
var dir_path = asset_path.get_base_dir()
editor_filesystem.scan_sources()
return {
"message": "Triggered filesystem scan for: " + asset_path
}
else:
return {"error": "Could not access editor filesystem"}
func handle_import_glb_scene(params):
if not params.has("glb_path"):
return {"error": "Missing required parameter: glb_path"}
var glb_path = params.glb_path
var name = params.get("name", "")
var position = params.get("position", [0, 0, 0])
var rotation = params.get("rotation", [0, 0, 0])
var scale = params.get("scale", [1, 1, 1])
var editor_interface = editor_plugin.get_editor_interface()
var current_scene = editor_interface.get_edited_scene_root()
if current_scene == null:
return {"error": "No scene is currently open"}
# Check if the GLB file exists
if not FileAccess.file_exists(glb_path):
return {"error": "GLB file not found: " + glb_path}
# Try to load the GLB as a PackedScene
var glb_scene = load(glb_path)
if not glb_scene:
return {"error": "Failed to load GLB file: " + glb_path}
# Check if it's a PackedScene
if glb_scene is PackedScene:
# Instantiate the packed scene
var instance = glb_scene.instantiate()
if not instance:
return {"error": "Failed to instantiate GLB scene"}
# Set the name
if name != "":
instance.name = name
else:
# Use filename without extension
var filename = glb_path.get_file().get_basename()
instance.name = filename
# Add to current scene
current_scene.add_child(instance)
instance.owner = current_scene
# Set ownership recursively for all children
_set_owner_recursive(instance, current_scene)
# Set transform if it's a 3D node
if instance is Node3D:
instance.position = Vector3(position[0], position[1], position[2])
instance.rotation_degrees = Vector3(rotation[0], rotation[1], rotation[2])
instance.scale = Vector3(scale[0], scale[1], scale[2])
return {
"success": true,
"instance_name": instance.name,
"message": "GLB scene imported successfully as: " + instance.name
}
else:
# If it's not a PackedScene, try to create a MeshInstance3D
var node_name = name if name != "" else glb_path.get_file().get_basename()
# Create a MeshInstance3D
var mesh_instance = MeshInstance3D.new()
mesh_instance.name = node_name
# Try to set the mesh
if glb_scene is Mesh:
mesh_instance.mesh = glb_scene
else:
# Try loading it differently
return {"error": "GLB file is not a PackedScene or Mesh resource"}
# Add to scene
current_scene.add_child(mesh_instance)
mesh_instance.owner = current_scene
# Set transform
mesh_instance.position = Vector3(position[0], position[1], position[2])
mesh_instance.rotation_degrees = Vector3(rotation[0], rotation[1], rotation[2])
mesh_instance.scale = Vector3(scale[0], scale[1], scale[2])
return {
"success": true,
"instance_name": mesh_instance.name,
"message": "GLB imported as MeshInstance3D: " + mesh_instance.name
}