189 lines
6.8 KiB
GDScript
189 lines
6.8 KiB
GDScript
class_name EditorCore
|
|
extends RefCounted
|
|
## Shared, data-native editor core (docs/architecture/editor.md §7.1).
|
|
##
|
|
## Both the in-game editor UI (`scripts/editor/ui/in_game_editor.gd`) and,
|
|
## per the doc's §6.2/§7.3, any future thin dev-plugin surface are meant to
|
|
## sit on top of this single object. It never touches `Control` nodes,
|
|
## HTTP, or the live scene tree -- it only knows about the chunk model,
|
|
## undo/redo, selection, and tools. It owns:
|
|
##
|
|
## - `model` (ChunkEditorModel) the chunk being edited
|
|
## - `undo_redo` (EditorUndoRedoStack) command-pattern history
|
|
## - `selection` (SelectionManager) selected object id
|
|
## - `tools` (ToolRegistry) brush / object_place / select /
|
|
## erase / transform
|
|
## - `catalog` (EditorObjectCatalog, optional) palette source consumed
|
|
## by `object_place`
|
|
##
|
|
## Every mutating public method here (`paint_tile`, `erase_tile`,
|
|
## `place_object`, `delete_object`, `move_object`, `rotate_object`,
|
|
## `scale_object`, `select_object`) is a thin wrapper around `use_tool()` --
|
|
## it goes through `ToolRegistry`, never straight at `ChunkEditorModel`.
|
|
## This is the seam this task's verification bar cares about: UI code and
|
|
## tests mutate the chunk exactly the same way, so a test that calls
|
|
## `core.move_object(...)` is exercising the same code path a player's
|
|
## drag-in-the-viewport would.
|
|
##
|
|
## Persisting the edited chunk to the backend is explicitly **out of
|
|
## scope** here (a sibling task owns the batch-PUT/conflict-handling save
|
|
## path per world_persistence_impl.md) -- `model.encode_binary()` is as far
|
|
## as this class goes.
|
|
|
|
var model: ChunkEditorModel
|
|
var undo_redo: EditorUndoRedoStack
|
|
var selection: SelectionManager
|
|
var tools: ToolRegistry
|
|
var catalog: EditorObjectCatalog = null
|
|
|
|
const PlacementStackManagerClass = preload("res://scripts/editor/placement_stack_manager.gd")
|
|
|
|
|
|
func _init(p_model: ChunkEditorModel, p_catalog: EditorObjectCatalog = null) -> void:
|
|
model = p_model
|
|
catalog = p_catalog
|
|
undo_redo = EditorUndoRedoStack.new()
|
|
selection = SelectionManager.new()
|
|
tools = ToolRegistry.new()
|
|
tools.register_tool(BrushTool.new())
|
|
tools.register_tool(ObjectPlaceTool.new())
|
|
tools.register_tool(SelectTool.new())
|
|
tools.register_tool(EraseTool.new())
|
|
tools.register_tool(TransformTool.new())
|
|
tools.set_active_tool("select")
|
|
|
|
|
|
## Route an action through the named tool. This is the single choke point
|
|
## every mutation (and every convenience wrapper below) goes through.
|
|
func use_tool(tool_id: String, args: Dictionary) -> Variant:
|
|
var tool: EditorTool = tools.get_tool(tool_id)
|
|
if tool == null:
|
|
push_error("EditorCore: unknown tool '%s'" % tool_id)
|
|
return null
|
|
return tool.apply(self, args)
|
|
|
|
|
|
# ───────── convenience wrappers (all route through use_tool/ToolRegistry) ─────────
|
|
|
|
func paint_tile(layer_idx: int, x: int, y: int, tile_id: int, z: int = 0) -> bool:
|
|
return bool(use_tool("brush", {"layer": layer_idx, "x": x, "y": y, "z": z, "tile_id": tile_id}))
|
|
|
|
|
|
func erase_tile(layer_idx: int, x: int, y: int, z: int = 0) -> bool:
|
|
return bool(use_tool("erase", {"layer": layer_idx, "x": x, "y": y, "z": z}))
|
|
|
|
|
|
func place_object(catalog_id: String, position: Vector3,
|
|
rotation: Vector4 = Vector4(0, 0, 0, 1), scale: Vector3 = Vector3.ONE,
|
|
properties: Dictionary = {}) -> String:
|
|
var args: Dictionary = {
|
|
"catalog_id": catalog_id,
|
|
"position": position,
|
|
"rotation": rotation,
|
|
"scale": scale,
|
|
}
|
|
if not properties.is_empty():
|
|
args["properties"] = properties
|
|
var result: Variant = use_tool("object_place", args)
|
|
return result if result is String else ""
|
|
|
|
|
|
func place_object_stacked(catalog_id: String, target_object_id: String = "",
|
|
hit_point: Vector3 = Vector3.ZERO, surface_normal: Vector3 = Vector3(0, 0, 1),
|
|
elevation_offset: float = 0.0, align_to_normal: bool = false) -> String:
|
|
var target_obj: ChunkDataDecoder.WorldObjectData = null
|
|
if target_object_id != "":
|
|
target_obj = model.find_object(target_object_id)
|
|
var stack_info: Dictionary = PlacementStackManagerClass.calculate_stack_transform(
|
|
catalog_id, target_obj, hit_point, surface_normal, elevation_offset, align_to_normal
|
|
)
|
|
var props: Dictionary = {}
|
|
if stack_info["parent_id"] != "":
|
|
props["parent_id"] = stack_info["parent_id"]
|
|
props["stacked_on"] = stack_info["parent_id"]
|
|
if elevation_offset != 0.0:
|
|
props["elevation_offset"] = elevation_offset
|
|
|
|
return place_object(catalog_id, stack_info["position"], stack_info["rotation"], Vector3.ONE, props)
|
|
|
|
|
|
func drop_object_to_surface(object_id: String) -> bool:
|
|
var obj := model.find_object(object_id)
|
|
if obj == null:
|
|
return false
|
|
var old_pos := obj.position
|
|
var old_props := obj.properties.duplicate(true)
|
|
if not PlacementStackManagerClass.drop_to_surface(model, object_id):
|
|
return false
|
|
var new_pos := obj.position
|
|
var new_props := obj.properties.duplicate(true)
|
|
|
|
undo_redo.begin("Drop '%s' to surface" % object_id)
|
|
undo_redo.add_do(func():
|
|
var target := model.find_object(object_id)
|
|
if target != null:
|
|
target.position = new_pos
|
|
target.properties = new_props
|
|
)
|
|
undo_redo.add_undo(func():
|
|
var target := model.find_object(object_id)
|
|
if target != null:
|
|
target.position = old_pos
|
|
target.properties = old_props
|
|
)
|
|
undo_redo.commit()
|
|
return true
|
|
|
|
|
|
func adjust_object_elevation(object_id: String, delta_z: float) -> bool:
|
|
var obj := model.find_object(object_id)
|
|
if obj == null:
|
|
return false
|
|
var target_pos := obj.position + Vector3(0, 0, delta_z)
|
|
target_pos.z = maxf(0.0, target_pos.z)
|
|
return move_object(object_id, target_pos)
|
|
|
|
|
|
func get_stacked_children(parent_id: String) -> Array[String]:
|
|
return PlacementStackManagerClass.get_all_children(model, parent_id)
|
|
|
|
|
|
func delete_object(object_id: String) -> bool:
|
|
return bool(use_tool("erase", {"object_id": object_id}))
|
|
|
|
|
|
func move_object(object_id: String, position: Vector3) -> bool:
|
|
return bool(use_tool("transform", {"object_id": object_id, "op": "move", "value": position}))
|
|
|
|
|
|
func rotate_object(object_id: String, rotation: Vector4) -> bool:
|
|
return bool(use_tool("transform", {"object_id": object_id, "op": "rotate", "value": rotation}))
|
|
|
|
|
|
func scale_object(object_id: String, scale: Vector3) -> bool:
|
|
return bool(use_tool("transform", {"object_id": object_id, "op": "scale", "value": scale}))
|
|
|
|
|
|
func set_object_property(object_id: String, key: String, value: Variant) -> bool:
|
|
return bool(use_tool("transform", {"object_id": object_id, "op": "property", "key": key, "value": value}))
|
|
|
|
|
|
func set_object_behavior(object_id: String, behavior_dict: Dictionary) -> bool:
|
|
return set_object_property(object_id, "script_behavior", behavior_dict)
|
|
|
|
|
|
func select_object(object_id: String) -> bool:
|
|
return bool(use_tool("select", {"object_id": object_id}))
|
|
|
|
|
|
func clear_selection() -> void:
|
|
use_tool("select", {})
|
|
|
|
|
|
func undo() -> bool:
|
|
return undo_redo.undo()
|
|
|
|
|
|
func redo() -> bool:
|
|
return undo_redo.redo()
|