2026-10-09 12:49:07 +02:00

324 lines
11 KiB
GDScript

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