@tool extends RefCounted ## 3d.* command handlers — create 3D primitives, lights, cameras, ## and environment setups in the edited scene. const Modules := preload("res://addons/godot_mcp_toolkit/core/modules.gd") const Coerce = Modules.Coerce const Helpers = Modules.CommandHelpers const _VALID_PRIMITIVES := ["box", "sphere", "cylinder", "capsule", "plane", "prism"] const _VALID_LIGHT_TYPES := ["directional", "omni", "spot"] const _VALID_PROJECTIONS := ["perspective", "orthogonal"] const _VALID_TONEMAPS := ["linear", "reinhardt", "filmic", "aces"] static func register(registry: MCPToolkitCommandRegistry, _server: Node) -> void: registry.add("3d.create_primitive", func(parameters: Dictionary) -> Dictionary: return _cmd_create_primitive(parameters) , MCPToolkitCommandOptions.new()) registry.add("3d.setup_environment", func(parameters: Dictionary) -> Dictionary: return _cmd_setup_environment(parameters) , MCPToolkitCommandOptions.new()) registry.add("3d.create_light", func(parameters: Dictionary) -> Dictionary: return _cmd_create_light(parameters) , MCPToolkitCommandOptions.new()) registry.add("3d.create_camera", func(parameters: Dictionary) -> Dictionary: return _cmd_create_camera(parameters) , MCPToolkitCommandOptions.new()) # -- Helpers ------------------------------------------------------------------ static func _vec3(d: Variant) -> Vector3: if typeof(d) != TYPE_DICTIONARY: return Vector3.ZERO return Vector3(float(d.get("x", 0.0)), float(d.get("y", 0.0)), float(d.get("z", 0.0))) static func _color(d: Variant) -> Color: return Coerce.color_from_dict(d) static func _path_in_scene(root: Node, node: Node) -> String: return str(root.get_path_to(node)) static func _resolve_parent(parameters: Dictionary) -> Variant: var root := Helpers.get_edited_root() if root == null: return MCPToolkitError.fail("NO_SCENE", "no edited scene") var parent_path := str(parameters.get("parent_path", ".")) parent_path = Helpers.normalize_editor_path(parent_path) var parent := Helpers.resolve_scene_node(parent_path) if parent == null: return MCPToolkitError.fail("NOT_FOUND", "parent not found: %s" % parent_path, MCPToolkitError.HINT_NODE_PATH) return parent static func _add_node_undoable(parent: Node, node: Node, root: Node) -> void: parent.add_child(node) node.set_owner(root) MCPToolkitUndoRedoAction.begin("create %s" % node.name, parent) \ .do_method(parent.add_child.bind(node)) \ .do_method(node.set_owner.bind(root)) \ .do_reference(node) \ .undo_method(parent.remove_child.bind(node)) \ .commit_recorded() # -- Commands ----------------------------------------------------------------- static func _cmd_create_primitive(parameters: Dictionary) -> Dictionary: var err = MCPToolkitError.require(parameters, ["parent_path", "primitive"]) if err != null: return err var primitive := str(parameters.get("primitive", "")) if primitive not in _VALID_PRIMITIVES: return MCPToolkitError.fail("INVALID_PARAMS", "invalid primitive '%s'; must be one of: %s" % [primitive, ", ".join(_VALID_PRIMITIVES)]) var parent_result = _resolve_parent(parameters) if parent_result is Dictionary: return parent_result var parent: Node = parent_result var root := Helpers.get_edited_root() var mesh_instance := MeshInstance3D.new() mesh_instance.name = str(parameters.get("name", "MeshInstance3D")) # Create appropriate mesh type. var mesh: Mesh match primitive: "box": var box := BoxMesh.new() if parameters.has("size"): box.size = _vec3(parameters["size"]) mesh = box "sphere": var sphere := SphereMesh.new() if parameters.has("size"): var s = parameters["size"] if typeof(s) == TYPE_DICTIONARY: if s.has("x"): sphere.radius = float(s["x"]) / 2.0 if s.has("y"): sphere.height = float(s["y"]) mesh = sphere "cylinder": var cyl := CylinderMesh.new() if parameters.has("size"): var s = parameters["size"] if typeof(s) == TYPE_DICTIONARY: if s.has("x"): cyl.top_radius = float(s["x"]) / 2.0 cyl.bottom_radius = float(s["x"]) / 2.0 if s.has("y"): cyl.height = float(s["y"]) mesh = cyl "capsule": var cap := CapsuleMesh.new() if parameters.has("size"): var s = parameters["size"] if typeof(s) == TYPE_DICTIONARY: if s.has("x"): cap.radius = float(s["x"]) / 2.0 if s.has("y"): cap.height = float(s["y"]) mesh = cap "plane": var plane := PlaneMesh.new() if parameters.has("size"): var s = parameters["size"] if typeof(s) == TYPE_DICTIONARY: plane.size = Vector2( float(s.get("x", 1.0)), float(s.get("z", 1.0))) mesh = plane "prism": var prism := PrismMesh.new() if parameters.has("size"): prism.size = _vec3(parameters["size"]) mesh = prism mesh_instance.mesh = mesh # Material. if parameters.has("material"): var mat_data = parameters["material"] if typeof(mat_data) == TYPE_DICTIONARY: var mat := StandardMaterial3D.new() if mat_data.has("albedo_color"): mat.albedo_color = _color(mat_data["albedo_color"]) if mat_data.has("metallic"): mat.metallic = float(mat_data["metallic"]) if mat_data.has("roughness"): mat.roughness = float(mat_data["roughness"]) mesh_instance.material_override = mat # Position. if parameters.has("position"): mesh_instance.position = _vec3(parameters["position"]) _add_node_undoable(parent, mesh_instance, root) return MCPToolkitSuccess.ok({"status": "created", "path": _path_in_scene(root, mesh_instance)}) static func _cmd_setup_environment(parameters: Dictionary) -> Dictionary: var err = MCPToolkitError.require(parameters, ["parent_path"]) if err != null: return err var parent_result = _resolve_parent(parameters) if parent_result is Dictionary: return parent_result var parent: Node = parent_result var root := Helpers.get_edited_root() var env := Environment.new() # Sky. var sky_data = parameters.get("sky", null) var sky_material: ProceduralSkyMaterial if sky_data != null and typeof(sky_data) == TYPE_DICTIONARY: sky_material = ProceduralSkyMaterial.new() if sky_data.has("sky_top_color"): sky_material.sky_top_color = _color(sky_data["sky_top_color"]) if sky_data.has("sky_horizon_color"): sky_material.sky_horizon_color = _color(sky_data["sky_horizon_color"]) if sky_data.has("ground_bottom_color"): sky_material.ground_bottom_color = _color(sky_data["ground_bottom_color"]) else: sky_material = ProceduralSkyMaterial.new() var sky := Sky.new() sky.sky_material = sky_material env.sky = sky env.background_mode = Environment.BG_SKY # Ambient light. var ambient_data = parameters.get("ambient_light", null) if ambient_data != null and typeof(ambient_data) == TYPE_DICTIONARY: env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR if ambient_data.has("color"): env.ambient_light_color = _color(ambient_data["color"]) if ambient_data.has("energy"): env.ambient_light_energy = float(ambient_data["energy"]) # Tonemap. var tonemap_str := str(parameters.get("tonemap", "")) if not tonemap_str.is_empty(): match tonemap_str: "linear": env.tonemap_mode = Environment.TONE_MAPPER_LINEAR "reinhardt": env.tonemap_mode = Environment.TONE_MAPPER_REINHARDT "filmic": env.tonemap_mode = Environment.TONE_MAPPER_FILMIC "aces": env.tonemap_mode = Environment.TONE_MAPPER_ACES # Fog. var fog_data = parameters.get("fog", null) if fog_data != null and typeof(fog_data) == TYPE_DICTIONARY: env.fog_enabled = bool(fog_data.get("enabled", true)) if fog_data.has("color"): env.fog_light_color = _color(fog_data["color"]) if fog_data.has("density"): env.fog_density = float(fog_data["density"]) var world_env := WorldEnvironment.new() world_env.name = str(parameters.get("name", "WorldEnvironment")) world_env.environment = env _add_node_undoable(parent, world_env, root) return MCPToolkitSuccess.ok({"status": "created", "path": _path_in_scene(root, world_env)}) static func _cmd_create_light(parameters: Dictionary) -> Dictionary: var err = MCPToolkitError.require(parameters, ["parent_path", "light_type"]) if err != null: return err var light_type := str(parameters.get("light_type", "")) if light_type not in _VALID_LIGHT_TYPES: return MCPToolkitError.fail("INVALID_PARAMS", "invalid light_type '%s'; must be one of: %s" % [light_type, ", ".join(_VALID_LIGHT_TYPES)]) var parent_result = _resolve_parent(parameters) if parent_result is Dictionary: return parent_result var parent: Node = parent_result var root := Helpers.get_edited_root() var light: Light3D match light_type: "directional": light = DirectionalLight3D.new() light.name = str(parameters.get("name", "DirectionalLight3D")) "omni": light = OmniLight3D.new() light.name = str(parameters.get("name", "OmniLight3D")) "spot": light = SpotLight3D.new() light.name = str(parameters.get("name", "SpotLight3D")) if parameters.has("color"): light.light_color = _color(parameters["color"]) if parameters.has("energy"): light.light_energy = float(parameters["energy"]) if parameters.has("shadow"): light.shadow_enabled = bool(parameters["shadow"]) if parameters.has("position"): light.position = _vec3(parameters["position"]) if parameters.has("rotation"): var rot = parameters["rotation"] if typeof(rot) == TYPE_DICTIONARY: light.rotation_degrees = _vec3(rot) _add_node_undoable(parent, light, root) return MCPToolkitSuccess.ok({"status": "created", "path": _path_in_scene(root, light)}) static func _cmd_create_camera(parameters: Dictionary) -> Dictionary: var err = MCPToolkitError.require(parameters, ["parent_path"]) if err != null: return err var parent_result = _resolve_parent(parameters) if parent_result is Dictionary: return parent_result var parent: Node = parent_result var root := Helpers.get_edited_root() var camera := Camera3D.new() camera.name = str(parameters.get("name", "Camera3D")) var projection_str := str(parameters.get("projection", "")) if not projection_str.is_empty(): match projection_str: "perspective": camera.projection = Camera3D.PROJECTION_PERSPECTIVE "orthogonal": camera.projection = Camera3D.PROJECTION_ORTHOGONAL if parameters.has("fov"): camera.fov = float(parameters["fov"]) if parameters.has("size"): camera.size = float(parameters["size"]) if parameters.has("position"): camera.position = _vec3(parameters["position"]) if parameters.has("rotation"): var rot = parameters["rotation"] if typeof(rot) == TYPE_DICTIONARY: camera.rotation_degrees = _vec3(rot) if parameters.has("current"): camera.current = bool(parameters["current"]) _add_node_undoable(parent, camera, root) return MCPToolkitSuccess.ok({"status": "created", "path": _path_in_scene(root, camera)})