first commit
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
## GridMap authoring — set, fill, clear, and read 3D cells plus mesh-library
|
||||
## items directly in the editor scene with full undo/redo support.
|
||||
##
|
||||
## All ops target GridMap nodes in the currently edited scene by
|
||||
## scene-relative path (e.g. "/Main/Terrain"). All write ops are undoable
|
||||
## via EditorUndoRedoManager.
|
||||
|
||||
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
|
||||
const MAX_FILL_CELLS := 4096
|
||||
|
||||
var _undo_redo: EditorUndoRedoManager
|
||||
|
||||
|
||||
func _init(undo_redo: EditorUndoRedoManager) -> void:
|
||||
_undo_redo = undo_redo
|
||||
|
||||
|
||||
## Set a single cell item. item = -1 erases the cell.
|
||||
## params: {path, item, map_x, map_y, map_z, orientation=0}
|
||||
## Returns: {map_x, map_y, map_z, item, orientation, undoable}
|
||||
func set_item(params: Dictionary) -> Dictionary:
|
||||
var gm := _resolve_gridmap(params)
|
||||
if gm.has("error"): return gm
|
||||
var node: GridMap = gm.node
|
||||
var pos := Vector3i(int(params.get("map_x", 0)), int(params.get("map_y", 0)), int(params.get("map_z", 0)))
|
||||
var item := int(params.get("item", 0))
|
||||
var orientation := int(params.get("orientation", 0))
|
||||
if orientation < 0 or orientation > 24:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
|
||||
var prev := _capture_cell_state(node, pos)
|
||||
_undo_redo.create_action("MCP: GridMap set_item")
|
||||
_undo_redo.add_do_method(node, "set_cell_item", pos, item, orientation)
|
||||
_undo_redo.add_undo_method(self, "_restore_cell_state", node, pos, prev)
|
||||
_undo_redo.commit_action()
|
||||
return {"data": {"map_x": pos.x, "map_y": pos.y, "map_z": pos.z,
|
||||
"item": item, "orientation": orientation, "undoable": true}}
|
||||
|
||||
|
||||
## Fill a box region with one item in a single undo action.
|
||||
## params: {path, item, rect_x, rect_y, rect_z, rect_w, rect_h, rect_d, orientation=0}
|
||||
## Returns: {cells_filled, rect: {x, y, z, w, h, d}}
|
||||
func fill(params: Dictionary) -> Dictionary:
|
||||
var gm := _resolve_gridmap(params)
|
||||
if gm.has("error"): return gm
|
||||
var node: GridMap = gm.node
|
||||
var item := int(params.get("item", 0))
|
||||
var orientation := int(params.get("orientation", 0))
|
||||
if orientation < 0 or orientation > 24:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"orientation must be in 0..24 (GridMap baked rotations), got %d" % orientation)
|
||||
var rx := int(params.get("rect_x", 0)); var ry := int(params.get("rect_y", 0)); var rz := int(params.get("rect_z", 0))
|
||||
var rw := int(params.get("rect_w", 1)); var rh := int(params.get("rect_h", 1)); var rd := int(params.get("rect_d", 1))
|
||||
if rw <= 0 or rh <= 0 or rd <= 0:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"rect_w, rect_h and rect_d must be > 0 (got %d x %d x %d)" % [rw, rh, rd])
|
||||
var cell_count := rw * rh * rd
|
||||
if cell_count > MAX_FILL_CELLS:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"Region too large: %d cells exceeds max %d" % [cell_count, MAX_FILL_CELLS])
|
||||
var snapshot: Array[Dictionary] = []
|
||||
for x in range(rx, rx + rw):
|
||||
for y in range(ry, ry + rh):
|
||||
for z in range(rz, rz + rd):
|
||||
var pos := Vector3i(x, y, z)
|
||||
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
|
||||
_undo_redo.create_action("MCP: GridMap fill %dx%dx%d" % [rw, rh, rd])
|
||||
## First callback targets the node so the action lands in the scene undo
|
||||
## history (first-target routing); _apply_fill batches the rest of the
|
||||
## region into a single history entry instead of one per cell.
|
||||
_undo_redo.add_do_method(node, "set_cell_item", snapshot[0].pos, item, orientation)
|
||||
_undo_redo.add_do_method(self, "_apply_fill", node, snapshot, item, orientation)
|
||||
_undo_redo.add_undo_method(self, "_restore_rect_snapshot", node, snapshot)
|
||||
_undo_redo.commit_action()
|
||||
return {"data": {"cells_filled": snapshot.size(),
|
||||
"rect": {"x": rx, "y": ry, "z": rz, "w": rw, "h": rh, "d": rd}, "undoable": true}}
|
||||
|
||||
|
||||
## Remove all cells from the GridMap.
|
||||
## params: {path}
|
||||
## Returns: {cleared: true}
|
||||
func clear_layer(params: Dictionary) -> Dictionary:
|
||||
var gm := _resolve_gridmap(params)
|
||||
if gm.has("error"): return gm
|
||||
var node: GridMap = gm.node
|
||||
var used := node.get_used_cells()
|
||||
if used.size() > MAX_FILL_CELLS:
|
||||
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
|
||||
"GridMap has %d cells, exceeds max %d for undoable clear"
|
||||
% [used.size(), MAX_FILL_CELLS])
|
||||
var snapshot := _capture_used_cells_snapshot(node)
|
||||
_undo_redo.create_action("MCP: GridMap clear")
|
||||
_undo_redo.add_do_method(node, "clear")
|
||||
_undo_redo.add_undo_method(self, "_restore_cells_snapshot", node, snapshot)
|
||||
_undo_redo.commit_action()
|
||||
return {"data": {"cleared": true, "undoable": true}}
|
||||
|
||||
|
||||
## Return all used cell coordinates.
|
||||
## params: {path}
|
||||
## Returns: {cells: [{x, y, z}, ...], count: int}
|
||||
func get_used_cells(params: Dictionary) -> Dictionary:
|
||||
var gm := _resolve_gridmap(params)
|
||||
if gm.has("error"): return gm
|
||||
var node: GridMap = gm.node
|
||||
var result: Array = []
|
||||
for c in node.get_used_cells():
|
||||
result.append({"x": c.x, "y": c.y, "z": c.z})
|
||||
return {"data": {"cells": result, "count": result.size()}}
|
||||
|
||||
|
||||
## List the items available in the GridMap's MeshLibrary, so agents can
|
||||
## discover item ids and names before placing cells (the 3D analogue of
|
||||
## tileset atlas inspection).
|
||||
## params: {path}
|
||||
## Returns: {library: res:// path or "", items: [{item, name, mesh}...], count}
|
||||
func list_library_items(params: Dictionary) -> Dictionary:
|
||||
var gm := _resolve_gridmap(params)
|
||||
if gm.has("error"): return gm
|
||||
var node: GridMap = gm.node
|
||||
var library: MeshLibrary = node.mesh_library
|
||||
if library == null:
|
||||
return {"data": {"library": "", "items": [], "count": 0}}
|
||||
var items: Array = []
|
||||
var ids := library.get_item_list()
|
||||
ids.sort()
|
||||
for item in ids:
|
||||
var mesh: Mesh = library.get_item_mesh(item)
|
||||
var mesh_path := mesh.resource_path if mesh != null else ""
|
||||
items.append({
|
||||
"item": item,
|
||||
"name": library.get_item_name(item),
|
||||
"mesh": mesh_path,
|
||||
})
|
||||
return {"data": {"library": library.resource_path, "items": items, "count": items.size()}}
|
||||
|
||||
|
||||
## Resolve a GridMap node from params["path"] in the currently edited
|
||||
## scene. Returns {"node": GridMap} on success, or an error dict.
|
||||
func _resolve_gridmap(params: Dictionary) -> Dictionary:
|
||||
var path: String = params.get("path", "")
|
||||
var scene_file: String = params.get("scene_file", "")
|
||||
var resolved := McpNodeValidator.resolve_or_error(path, "path", scene_file)
|
||||
if resolved.has("error"):
|
||||
return resolved
|
||||
var node: Node = resolved.node
|
||||
if not node is GridMap:
|
||||
return ErrorCodes.make(ErrorCodes.WRONG_TYPE,
|
||||
"Node is not a GridMap: %s" % path)
|
||||
return {"node": node}
|
||||
|
||||
|
||||
func _capture_cell_state(node: GridMap, pos: Vector3i) -> Dictionary:
|
||||
var item := node.get_cell_item(pos)
|
||||
if item == -1:
|
||||
return {"has_item": false}
|
||||
return {"has_item": true, "item": item, "orientation": node.get_cell_item_orientation(pos)}
|
||||
|
||||
|
||||
func _capture_used_cells_snapshot(node: GridMap) -> Array[Dictionary]:
|
||||
var snapshot: Array[Dictionary] = []
|
||||
for pos in node.get_used_cells():
|
||||
snapshot.append({"pos": pos, "state": _capture_cell_state(node, pos)})
|
||||
return snapshot
|
||||
|
||||
|
||||
func _restore_cells_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
|
||||
node.clear()
|
||||
for entry in snapshot:
|
||||
_restore_cell_state(node, entry.pos, entry.state)
|
||||
|
||||
|
||||
## Batched do-method for fill: one undo-history entry applies the whole
|
||||
## region instead of one entry per cell.
|
||||
func _apply_fill(node: GridMap, snapshot: Array[Dictionary], item: int, orientation: int) -> void:
|
||||
for entry in snapshot:
|
||||
node.set_cell_item(entry.pos, item, orientation)
|
||||
|
||||
|
||||
func _restore_rect_snapshot(node: GridMap, snapshot: Array[Dictionary]) -> void:
|
||||
for entry in snapshot:
|
||||
_restore_cell_state(node, entry.pos, entry.state)
|
||||
|
||||
|
||||
func _restore_cell_state(node: GridMap, pos: Vector3i, state: Dictionary) -> void:
|
||||
if not state.get("has_item", false):
|
||||
node.set_cell_item(pos, -1)
|
||||
return
|
||||
node.set_cell_item(pos, int(state.get("item", 0)), int(state.get("orientation", 0)))
|
||||
Reference in New Issue
Block a user