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, extra_properties: Dictionary = {}) -> 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 props.merge(extra_properties) 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()