first commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
## CSG authoring — create CSG shapes (box, sphere, cylinder, torus, prism)
|
||||
## and set their boolean operation (union / intersection / subtraction) so
|
||||
## agents can carve geometry (holes, caves, tunnels) directly in the editor.
|
||||
##
|
||||
## All ops target nodes in the currently edited scene by scene-relative
|
||||
## path. All write ops are undoable via EditorUndoRedoManager. Sibling CSG
|
||||
## shapes under the same parent combine automatically; use a CSGCombiner3D
|
||||
## parent when you need explicit grouping.
|
||||
|
||||
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
||||
|
||||
const SHAPES := {
|
||||
"box": "CSGBox3D",
|
||||
"sphere": "CSGSphere3D",
|
||||
"cylinder": "CSGCylinder3D",
|
||||
"torus": "CSGTorus3D",
|
||||
"polygon": "CSGPolygon3D",
|
||||
}
|
||||
|
||||
const OPERATIONS := {
|
||||
"union": CSGShape3D.OPERATION_UNION,
|
||||
"intersection": CSGShape3D.OPERATION_INTERSECTION,
|
||||
"subtraction": CSGShape3D.OPERATION_SUBTRACTION,
|
||||
}
|
||||
|
||||
var _undo_redo: EditorUndoRedoManager
|
||||
|
||||
|
||||
func _init(undo_redo: EditorUndoRedoManager) -> void:
|
||||
_undo_redo = undo_redo
|
||||
|
||||
|
||||
## Create a CSG shape under a Node3D parent.
|
||||
## params: {parent_path, name="", shape="box", operation="union"}
|
||||
## Returns: {path, name, shape, operation, undoable}
|
||||
func create(params: Dictionary) -> Dictionary:
|
||||
var parent_path: String = params.get("parent_path", "")
|
||||
var shape: String = params.get("shape", "box")
|
||||
var operation: String = params.get("operation", "union")
|
||||
var scene_file: String = params.get("scene_file", "")
|
||||
|
||||
var shape_class: String = SHAPES.get(shape, "")
|
||||
if shape_class.is_empty():
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Unknown shape: %s. Valid shapes: %s" % [shape, ", ".join(SHAPES.keys())])
|
||||
if not OPERATIONS.has(operation):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Unknown operation: %s. Valid operations: %s" % [operation, ", ".join(OPERATIONS.keys())])
|
||||
|
||||
var scene_check := McpScenePath.require_edited_scene(scene_file)
|
||||
if scene_check.has("error"):
|
||||
return scene_check
|
||||
var scene_root: Node = scene_check.node
|
||||
|
||||
var parent: Node = scene_root
|
||||
if not parent_path.is_empty():
|
||||
parent = McpScenePath.resolve(parent_path, scene_root)
|
||||
if parent == null:
|
||||
return ErrorCodes.make(ErrorCodes.NODE_NOT_FOUND,
|
||||
McpScenePath.format_parent_error(parent_path, scene_root))
|
||||
if not parent is Node3D:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"CSG parent must be a Node3D (got %s)" % parent.get_class())
|
||||
|
||||
var node: CSGShape3D = ClassDB.instantiate(shape_class)
|
||||
if node == null:
|
||||
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate %s" % shape_class)
|
||||
var node_name: String = params.get("name", "")
|
||||
if node_name.is_empty():
|
||||
node_name = shape_class
|
||||
node.name = node_name
|
||||
node.operation = OPERATIONS[operation]
|
||||
|
||||
_undo_redo.create_action("MCP: Create %s" % node.name)
|
||||
_undo_redo.add_do_method(parent, "add_child", node, true)
|
||||
_undo_redo.add_do_method(node, "set_owner", scene_root)
|
||||
_undo_redo.add_do_reference(node)
|
||||
_undo_redo.add_undo_method(parent, "remove_child", node)
|
||||
_undo_redo.commit_action()
|
||||
|
||||
return {"data": {
|
||||
"path": McpScenePath.from_node(node, scene_root),
|
||||
"name": node.name,
|
||||
"shape": shape,
|
||||
"operation": operation,
|
||||
"undoable": true,
|
||||
}}
|
||||
|
||||
|
||||
## Set the boolean operation of a CSG shape.
|
||||
## params: {path, operation}
|
||||
## Returns: {operation, undoable}
|
||||
func set_operation(params: Dictionary) -> Dictionary:
|
||||
var operation: String = params.get("operation", "")
|
||||
if not OPERATIONS.has(operation):
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Unknown operation: %s. Valid operations: %s" % [operation, ", ".join(OPERATIONS.keys())])
|
||||
var resolved := McpNodeValidator.resolve_or_error(
|
||||
params.get("path", ""), "path", params.get("scene_file", ""))
|
||||
if resolved.has("error"):
|
||||
return resolved
|
||||
var node: Node = resolved.node
|
||||
if not node is CSGShape3D:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Node is not a CSGShape3D: %s" % params.get("path", ""))
|
||||
var shape: CSGShape3D = node
|
||||
var prev: int = shape.operation
|
||||
var next: int = OPERATIONS[operation]
|
||||
## Target the node in both callbacks so the action lands in the scene
|
||||
## undo history (first-target routing); `set` exists on every Object.
|
||||
_undo_redo.create_action("MCP: CSG set_operation")
|
||||
_undo_redo.add_do_method(shape, "set", "operation", next)
|
||||
_undo_redo.add_undo_method(shape, "set", "operation", prev)
|
||||
_undo_redo.commit_action()
|
||||
return {"data": {"operation": operation, "undoable": true}}
|
||||
Reference in New Issue
Block a user