feat: Implement Canvas Automation Service for Enhanced GUI Control
- Introduced a new gRPC service for canvas automation, enabling remote control of canvas operations for AI agents, GUI testing, and collaborative editing. - Added proto definitions for canvas automation, including tile operations, selection management, and view control functionalities. - Implemented the CanvasAutomationServiceImpl class to handle various canvas operations, ensuring a robust interface for automation tasks. - Updated CMake configuration to include new proto files and service implementations, enhancing the build system for the canvas automation features.
This commit is contained in:
@@ -83,11 +83,13 @@ if(YAZE_WITH_GRPC)
|
||||
${CMAKE_SOURCE_DIR}/third_party/json/include)
|
||||
target_compile_definitions(yaze_core_lib PRIVATE YAZE_WITH_JSON)
|
||||
|
||||
# Add proto definitions for test harness and ROM service
|
||||
# Add proto definitions for test harness, ROM service, and canvas automation
|
||||
target_add_protobuf(yaze_core_lib
|
||||
${PROJECT_SOURCE_DIR}/src/app/core/proto/imgui_test_harness.proto)
|
||||
${PROJECT_SOURCE_DIR}/src/protos/imgui_test_harness.proto)
|
||||
target_add_protobuf(yaze_core_lib
|
||||
${PROJECT_SOURCE_DIR}/src/protos/rom_service.proto)
|
||||
target_add_protobuf(yaze_core_lib
|
||||
${PROJECT_SOURCE_DIR}/src/protos/canvas_automation.proto)
|
||||
|
||||
# Add test harness implementation
|
||||
target_sources(yaze_core_lib PRIVATE
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package yaze.test;
|
||||
|
||||
// ImGuiTestHarness service for remote GUI testing
|
||||
// Allows z3ed CLI to interact with YAZE's GUI for automated testing
|
||||
service ImGuiTestHarness {
|
||||
// Health check - verifies the service is running
|
||||
rpc Ping(PingRequest) returns (PingResponse);
|
||||
|
||||
// Click a button or interactive element
|
||||
rpc Click(ClickRequest) returns (ClickResponse);
|
||||
|
||||
// Type text into an input field
|
||||
rpc Type(TypeRequest) returns (TypeResponse);
|
||||
|
||||
// Wait for a condition to be true
|
||||
rpc Wait(WaitRequest) returns (WaitResponse);
|
||||
|
||||
// Assert that a condition is true
|
||||
rpc Assert(AssertRequest) returns (AssertResponse);
|
||||
|
||||
// Capture a screenshot
|
||||
rpc Screenshot(ScreenshotRequest) returns (ScreenshotResponse);
|
||||
|
||||
// Test introspection APIs (IT-05)
|
||||
rpc GetTestStatus(GetTestStatusRequest) returns (GetTestStatusResponse);
|
||||
rpc ListTests(ListTestsRequest) returns (ListTestsResponse);
|
||||
rpc GetTestResults(GetTestResultsRequest) returns (GetTestResultsResponse);
|
||||
|
||||
// Widget discovery API (IT-06)
|
||||
rpc DiscoverWidgets(DiscoverWidgetsRequest) returns (DiscoverWidgetsResponse);
|
||||
|
||||
// Test recording & replay (IT-07)
|
||||
rpc StartRecording(StartRecordingRequest) returns (StartRecordingResponse);
|
||||
rpc StopRecording(StopRecordingRequest) returns (StopRecordingResponse);
|
||||
rpc ReplayTest(ReplayTestRequest) returns (ReplayTestResponse);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ping - Health Check
|
||||
// ============================================================================
|
||||
|
||||
message PingRequest {
|
||||
string message = 1; // Message to echo back
|
||||
}
|
||||
|
||||
message PingResponse {
|
||||
string message = 1; // Echoed message with "Pong: " prefix
|
||||
int64 timestamp_ms = 2; // Server timestamp in milliseconds
|
||||
string yaze_version = 3; // YAZE version string (e.g., "0.3.2")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Click - Interact with GUI elements
|
||||
// ============================================================================
|
||||
|
||||
message ClickRequest {
|
||||
string target = 1; // Target element (e.g., "button:Open ROM")
|
||||
ClickType type = 2; // Type of click
|
||||
|
||||
enum ClickType {
|
||||
CLICK_TYPE_UNSPECIFIED = 0; // Default/unspecified click type
|
||||
CLICK_TYPE_LEFT = 1; // Single left click
|
||||
CLICK_TYPE_RIGHT = 2; // Single right click
|
||||
CLICK_TYPE_DOUBLE = 3; // Double click
|
||||
CLICK_TYPE_MIDDLE = 4; // Middle mouse button
|
||||
}
|
||||
}
|
||||
|
||||
message ClickResponse {
|
||||
bool success = 1; // Whether the click succeeded
|
||||
string message = 2; // Human-readable result message
|
||||
int32 execution_time_ms = 3; // Time taken to execute (for debugging)
|
||||
string test_id = 4; // Unique test identifier for introspection
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type - Send keyboard input
|
||||
// ============================================================================
|
||||
|
||||
message TypeRequest {
|
||||
string target = 1; // Target input field (e.g., "textbox:File Path")
|
||||
string text = 2; // Text to type
|
||||
bool clear_first = 3; // Clear existing text before typing
|
||||
}
|
||||
|
||||
message TypeResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
int32 execution_time_ms = 3;
|
||||
string test_id = 4;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wait - Poll for conditions
|
||||
// ============================================================================
|
||||
|
||||
message WaitRequest {
|
||||
string condition = 1; // Condition to wait for (e.g., "window:Overworld")
|
||||
int32 timeout_ms = 2; // Maximum time to wait (default 5000ms)
|
||||
int32 poll_interval_ms = 3; // How often to check (default 100ms)
|
||||
}
|
||||
|
||||
message WaitResponse {
|
||||
bool success = 1; // Whether condition was met before timeout
|
||||
string message = 2;
|
||||
int32 elapsed_ms = 3; // Time taken before condition met (or timeout)
|
||||
string test_id = 4; // Unique test identifier for introspection
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Assert - Validate GUI state
|
||||
// ============================================================================
|
||||
|
||||
message AssertRequest {
|
||||
string condition = 1; // Condition to assert (e.g., "visible:button:Save")
|
||||
string failure_message = 2; // Custom message if assertion fails
|
||||
}
|
||||
|
||||
message AssertResponse {
|
||||
bool success = 1; // Whether assertion passed
|
||||
string message = 2; // Diagnostic message
|
||||
string actual_value = 3; // Actual value found (for debugging)
|
||||
string expected_value = 4; // Expected value (for debugging)
|
||||
string test_id = 5; // Unique test identifier for introspection
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Screenshot - Capture window state
|
||||
// ============================================================================
|
||||
|
||||
message ScreenshotRequest {
|
||||
string window_title = 1; // Window to capture (empty = main window)
|
||||
string output_path = 2; // Where to save screenshot
|
||||
ImageFormat format = 3; // Image format
|
||||
|
||||
enum ImageFormat {
|
||||
IMAGE_FORMAT_UNSPECIFIED = 0;
|
||||
IMAGE_FORMAT_PNG = 1;
|
||||
IMAGE_FORMAT_JPEG = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ScreenshotResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string file_path = 3; // Absolute path to saved screenshot
|
||||
int64 file_size_bytes = 4;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GetTestStatus - Query test execution state
|
||||
// ============================================================================
|
||||
|
||||
message GetTestStatusRequest {
|
||||
string test_id = 1; // Test ID from Click/Type/Wait/Assert response
|
||||
}
|
||||
|
||||
message GetTestStatusResponse {
|
||||
enum Status {
|
||||
STATUS_UNSPECIFIED = 0; // Test ID not found or unspecified
|
||||
STATUS_QUEUED = 1; // Waiting to execute
|
||||
STATUS_RUNNING = 2; // Currently executing
|
||||
STATUS_PASSED = 3; // Completed successfully
|
||||
STATUS_FAILED = 4; // Assertion failed or error
|
||||
STATUS_TIMEOUT = 5; // Exceeded timeout
|
||||
}
|
||||
|
||||
Status status = 1;
|
||||
int64 queued_at_ms = 2; // When test was queued
|
||||
int64 started_at_ms = 3; // When test started (0 if not started)
|
||||
int64 completed_at_ms = 4; // When test completed (0 if not complete)
|
||||
int32 execution_time_ms = 5; // Total execution time
|
||||
string error_message = 6; // Error details if FAILED/TIMEOUT
|
||||
repeated string assertion_failures = 7; // Failed assertion details
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ListTests - Enumerate available tests
|
||||
// ============================================================================
|
||||
|
||||
message ListTestsRequest {
|
||||
string category_filter = 1; // Optional: "grpc", "unit", "integration", "e2e"
|
||||
int32 page_size = 2; // Number of results per page (default 100)
|
||||
string page_token = 3; // Pagination token from previous response
|
||||
}
|
||||
|
||||
message ListTestsResponse {
|
||||
repeated TestInfo tests = 1;
|
||||
string next_page_token = 2; // Token for next page (empty if no more)
|
||||
int32 total_count = 3; // Total number of matching tests
|
||||
}
|
||||
|
||||
message TestInfo {
|
||||
string test_id = 1; // Unique test identifier
|
||||
string name = 2; // Human-readable test name
|
||||
string category = 3; // Category: grpc, unit, integration, e2e
|
||||
int64 last_run_timestamp_ms = 4; // When test last executed
|
||||
int32 total_runs = 5; // Total number of executions
|
||||
int32 pass_count = 6; // Number of successful runs
|
||||
int32 fail_count = 7; // Number of failed runs
|
||||
int32 average_duration_ms = 8; // Average execution time
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GetTestResults - Retrieve detailed results
|
||||
// ============================================================================
|
||||
|
||||
message GetTestResultsRequest {
|
||||
string test_id = 1;
|
||||
bool include_logs = 2; // Include full execution logs
|
||||
}
|
||||
|
||||
message GetTestResultsResponse {
|
||||
bool success = 1; // Overall test result
|
||||
string test_name = 2;
|
||||
string category = 3;
|
||||
int64 executed_at_ms = 4;
|
||||
int32 duration_ms = 5;
|
||||
repeated AssertionResult assertions = 6;
|
||||
repeated string logs = 7; // If include_logs=true
|
||||
map<string, int32> metrics = 8; // e.g., "frame_count": 123
|
||||
|
||||
// IT-08b: Failure diagnostics
|
||||
string screenshot_path = 9; // Path to failure screenshot (if captured)
|
||||
int64 screenshot_size_bytes = 10; // Size of screenshot file
|
||||
string failure_context = 11; // Execution context at failure time
|
||||
string widget_state = 12; // Widget state dump (IT-08c - future)
|
||||
}
|
||||
|
||||
message AssertionResult {
|
||||
string description = 1;
|
||||
bool passed = 2;
|
||||
string expected_value = 3;
|
||||
string actual_value = 4;
|
||||
string error_message = 5;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DiscoverWidgets - Enumerate discoverable GUI widgets
|
||||
// ============================================================================
|
||||
|
||||
message DiscoverWidgetsRequest {
|
||||
// Optional: Limit to a window name (case-insensitive)
|
||||
string window_filter = 1;
|
||||
// Optional: Limit to widget type
|
||||
WidgetType type_filter = 2;
|
||||
// Optional: Require widget path to start with prefix
|
||||
string path_prefix = 3;
|
||||
// Include widgets that are currently not visible
|
||||
bool include_invisible = 4;
|
||||
// Include widgets that are currently disabled
|
||||
bool include_disabled = 5;
|
||||
}
|
||||
|
||||
enum WidgetType {
|
||||
WIDGET_TYPE_UNSPECIFIED = 0;
|
||||
WIDGET_TYPE_ALL = 1;
|
||||
WIDGET_TYPE_BUTTON = 2;
|
||||
WIDGET_TYPE_INPUT = 3;
|
||||
WIDGET_TYPE_MENU = 4;
|
||||
WIDGET_TYPE_TAB = 5;
|
||||
WIDGET_TYPE_CHECKBOX = 6;
|
||||
WIDGET_TYPE_SLIDER = 7;
|
||||
WIDGET_TYPE_CANVAS = 8;
|
||||
WIDGET_TYPE_SELECTABLE = 9;
|
||||
WIDGET_TYPE_OTHER = 10;
|
||||
}
|
||||
|
||||
message WidgetBounds {
|
||||
float min_x = 1;
|
||||
float min_y = 2;
|
||||
float max_x = 3;
|
||||
float max_y = 4;
|
||||
}
|
||||
|
||||
message DiscoveredWidget {
|
||||
// Full hierarchical path (e.g. Overworld/Toolset/button:Pan)
|
||||
string path = 1;
|
||||
// Human-readable label (e.g. Pan)
|
||||
string label = 2;
|
||||
// Widget type string (button, input, ...)
|
||||
string type = 3;
|
||||
// Description provided by registry (if any)
|
||||
string description = 4;
|
||||
// Suggested action for automation (e.g. "Click button:Pan")
|
||||
string suggested_action = 5;
|
||||
bool visible = 6; // Currently visible in UI
|
||||
bool enabled = 7; // Currently enabled for interaction
|
||||
WidgetBounds bounds = 8; // Bounding rectangle in screen coordinates
|
||||
uint32 widget_id = 9; // ImGui ID (debugging / direct access)
|
||||
int64 last_seen_frame = 10; // Frame number when widget was last observed
|
||||
int64 last_seen_at_ms = 11; // Wall-clock timestamp of last observation
|
||||
bool stale = 12; // True if widget not seen in the current frame
|
||||
}
|
||||
|
||||
message DiscoveredWindow {
|
||||
// Window name (first segment of path)
|
||||
string name = 1;
|
||||
// Whether the window is currently visible
|
||||
bool visible = 2;
|
||||
// Widgets contained in this window
|
||||
repeated DiscoveredWidget widgets = 3;
|
||||
}
|
||||
|
||||
message DiscoverWidgetsResponse {
|
||||
repeated DiscoveredWindow windows = 1;
|
||||
int32 total_widgets = 2; // Total number of widgets returned
|
||||
int64 generated_at_ms = 3; // Snapshot timestamp (Unix ms)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Recording & Replay (IT-07)
|
||||
// ============================================================================
|
||||
|
||||
message StartRecordingRequest {
|
||||
string output_path = 1; // Where to store the JSON script
|
||||
string session_name = 2; // Optional friendly name for the recording
|
||||
string description = 3; // Optional description stored alongside metadata
|
||||
}
|
||||
|
||||
message StartRecordingResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string recording_id = 3; // Identifier required when stopping
|
||||
int64 started_at_ms = 4;
|
||||
}
|
||||
|
||||
message StopRecordingRequest {
|
||||
string recording_id = 1; // Recording session to stop
|
||||
bool discard = 2; // If true, delete steps instead of writing file
|
||||
}
|
||||
|
||||
message StopRecordingResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string output_path = 3; // Final location of saved script (if any)
|
||||
int32 step_count = 4; // Total steps captured during session
|
||||
int64 duration_ms = 5; // Duration of the recording session
|
||||
}
|
||||
|
||||
message ReplayTestRequest {
|
||||
string script_path = 1; // Path to JSON script
|
||||
bool ci_mode = 2; // Suppress interactive prompts
|
||||
map<string, string> parameter_overrides = 3; // Optional parameter overrides
|
||||
}
|
||||
|
||||
message ReplayTestResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string replay_session_id = 3;
|
||||
int32 steps_executed = 4;
|
||||
repeated AssertionResult assertions = 5; // Aggregated assertion outcomes
|
||||
repeated string logs = 6; // Replay log entries
|
||||
}
|
||||
338
src/app/core/service/canvas_automation_service.cc
Normal file
338
src/app/core/service/canvas_automation_service.cc
Normal file
@@ -0,0 +1,338 @@
|
||||
#include "app/core/service/canvas_automation_service.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include "src/protos/canvas_automation.pb.h"
|
||||
#include "app/editor/overworld/overworld_editor.h"
|
||||
#include "app/gui/canvas/canvas_automation_api.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
void CanvasAutomationServiceImpl::RegisterCanvas(const std::string& canvas_id,
|
||||
gui::Canvas* canvas) {
|
||||
canvases_[canvas_id] = canvas;
|
||||
}
|
||||
|
||||
void CanvasAutomationServiceImpl::RegisterOverworldEditor(
|
||||
const std::string& canvas_id, editor::OverworldEditor* editor) {
|
||||
overworld_editors_[canvas_id] = editor;
|
||||
}
|
||||
|
||||
gui::Canvas* CanvasAutomationServiceImpl::GetCanvas(const std::string& canvas_id) {
|
||||
auto it = canvases_.find(canvas_id);
|
||||
if (it != canvases_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
editor::OverworldEditor* CanvasAutomationServiceImpl::GetOverworldEditor(
|
||||
const std::string& canvas_id) {
|
||||
auto it = overworld_editors_.find(canvas_id);
|
||||
if (it != overworld_editors_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tile Operations
|
||||
// ============================================================================
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::SetTile(
|
||||
const proto::SetTileRequest* request, proto::SetTileResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
bool success = api->SetTileAt(request->x(), request->y(), request->tile_id());
|
||||
|
||||
response->set_success(success);
|
||||
if (!success) {
|
||||
response->set_error("Failed to set tile - out of bounds or callback failed");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::GetTile(
|
||||
const proto::GetTileRequest* request, proto::GetTileResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
int tile_id = api->GetTileAt(request->x(), request->y());
|
||||
|
||||
if (tile_id >= 0) {
|
||||
response->set_tile_id(tile_id);
|
||||
response->set_success(true);
|
||||
} else {
|
||||
response->set_success(false);
|
||||
response->set_error("Tile not found - out of bounds or no callback set");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::SetTiles(
|
||||
const proto::SetTilesRequest* request, proto::SetTilesResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
|
||||
std::vector<std::tuple<int, int, int>> tiles;
|
||||
for (const auto& tile : request->tiles()) {
|
||||
tiles.push_back({tile.x(), tile.y(), tile.tile_id()});
|
||||
}
|
||||
|
||||
bool success = api->SetTiles(tiles);
|
||||
response->set_success(success);
|
||||
response->set_tiles_painted(tiles.size());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Selection Operations
|
||||
// ============================================================================
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::SelectTile(
|
||||
const proto::SelectTileRequest* request, proto::SelectTileResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
api->SelectTile(request->x(), request->y());
|
||||
response->set_success(true);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::SelectTileRect(
|
||||
const proto::SelectTileRectRequest* request,
|
||||
proto::SelectTileRectResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
const auto& rect = request->rect();
|
||||
api->SelectTileRect(rect.x1(), rect.y1(), rect.x2(), rect.y2());
|
||||
|
||||
auto selection = api->GetSelection();
|
||||
response->set_success(true);
|
||||
response->set_tiles_selected(selection.selected_tiles.size());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::GetSelection(
|
||||
const proto::GetSelectionRequest* request,
|
||||
proto::GetSelectionResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
auto selection = api->GetSelection();
|
||||
|
||||
response->set_has_selection(selection.has_selection);
|
||||
|
||||
for (const auto& tile : selection.selected_tiles) {
|
||||
auto* coord = response->add_selected_tiles();
|
||||
coord->set_x(static_cast<int>(tile.x));
|
||||
coord->set_y(static_cast<int>(tile.y));
|
||||
}
|
||||
|
||||
auto* start = response->mutable_selection_start();
|
||||
start->set_x(static_cast<int>(selection.selection_start.x));
|
||||
start->set_y(static_cast<int>(selection.selection_start.y));
|
||||
|
||||
auto* end = response->mutable_selection_end();
|
||||
end->set_x(static_cast<int>(selection.selection_end.x));
|
||||
end->set_y(static_cast<int>(selection.selection_end.y));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::ClearSelection(
|
||||
const proto::ClearSelectionRequest* request,
|
||||
proto::ClearSelectionResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
api->ClearSelection();
|
||||
response->set_success(true);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View Operations
|
||||
// ============================================================================
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::ScrollToTile(
|
||||
const proto::ScrollToTileRequest* request,
|
||||
proto::ScrollToTileResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
api->ScrollToTile(request->x(), request->y(), request->center());
|
||||
response->set_success(true);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::CenterOn(
|
||||
const proto::CenterOnRequest* request, proto::CenterOnResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
api->CenterOn(request->x(), request->y());
|
||||
response->set_success(true);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::SetZoom(
|
||||
const proto::SetZoomRequest* request, proto::SetZoomResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
response->set_success(false);
|
||||
response->set_error("Canvas not found: " + request->canvas_id());
|
||||
return absl::NotFoundError(response->error());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
api->SetZoom(request->zoom());
|
||||
|
||||
float actual_zoom = api->GetZoom();
|
||||
response->set_success(true);
|
||||
response->set_actual_zoom(actual_zoom);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::GetZoom(
|
||||
const proto::GetZoomRequest* request, proto::GetZoomResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
response->set_zoom(api->GetZoom());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Operations
|
||||
// ============================================================================
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::GetDimensions(
|
||||
const proto::GetDimensionsRequest* request,
|
||||
proto::GetDimensionsResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
auto dims = api->GetDimensions();
|
||||
|
||||
auto* proto_dims = response->mutable_dimensions();
|
||||
proto_dims->set_width_tiles(dims.width_tiles);
|
||||
proto_dims->set_height_tiles(dims.height_tiles);
|
||||
proto_dims->set_tile_size(dims.tile_size);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::GetVisibleRegion(
|
||||
const proto::GetVisibleRegionRequest* request,
|
||||
proto::GetVisibleRegionResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
auto region = api->GetVisibleRegion();
|
||||
|
||||
auto* proto_region = response->mutable_region();
|
||||
proto_region->set_min_x(region.min_x);
|
||||
proto_region->set_min_y(region.min_y);
|
||||
proto_region->set_max_x(region.max_x);
|
||||
proto_region->set_max_y(region.max_y);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CanvasAutomationServiceImpl::IsTileVisible(
|
||||
const proto::IsTileVisibleRequest* request,
|
||||
proto::IsTileVisibleResponse* response) {
|
||||
|
||||
auto* canvas = GetCanvas(request->canvas_id());
|
||||
if (!canvas) {
|
||||
return absl::NotFoundError("Canvas not found: " + request->canvas_id());
|
||||
}
|
||||
|
||||
auto* api = canvas->GetAutomationAPI();
|
||||
response->set_is_visible(api->IsTileVisible(request->x(), request->y()));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
|
||||
133
src/app/core/service/canvas_automation_service.h
Normal file
133
src/app/core/service/canvas_automation_service.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#ifndef YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_
|
||||
#define YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gui/canvas.h"
|
||||
|
||||
// Forward declarations
|
||||
namespace grpc {
|
||||
class ServerContext;
|
||||
}
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
class OverworldEditor;
|
||||
}
|
||||
|
||||
namespace proto {
|
||||
// Forward declare proto types
|
||||
class SetTileRequest;
|
||||
class SetTileResponse;
|
||||
class GetTileRequest;
|
||||
class GetTileResponse;
|
||||
class SetTilesRequest;
|
||||
class SetTilesResponse;
|
||||
class SelectTileRequest;
|
||||
class SelectTileResponse;
|
||||
class SelectTileRectRequest;
|
||||
class SelectTileRectResponse;
|
||||
class GetSelectionRequest;
|
||||
class GetSelectionResponse;
|
||||
class ClearSelectionRequest;
|
||||
class ClearSelectionResponse;
|
||||
class ScrollToTileRequest;
|
||||
class ScrollToTileResponse;
|
||||
class CenterOnRequest;
|
||||
class CenterOnResponse;
|
||||
class SetZoomRequest;
|
||||
class SetZoomResponse;
|
||||
class GetZoomRequest;
|
||||
class GetZoomResponse;
|
||||
class GetDimensionsRequest;
|
||||
class GetDimensionsResponse;
|
||||
class GetVisibleRegionRequest;
|
||||
class GetVisibleRegionResponse;
|
||||
class IsTileVisibleRequest;
|
||||
class IsTileVisibleResponse;
|
||||
} // namespace proto
|
||||
|
||||
/**
|
||||
* @brief Implementation of CanvasAutomation gRPC service
|
||||
*
|
||||
* Provides remote access to canvas automation API for:
|
||||
* - AI agent tool calls
|
||||
* - Remote GUI testing
|
||||
* - Collaborative editing workflows
|
||||
* - CLI automation scripts
|
||||
*/
|
||||
class CanvasAutomationServiceImpl {
|
||||
public:
|
||||
CanvasAutomationServiceImpl() = default;
|
||||
|
||||
// Register a canvas for automation
|
||||
void RegisterCanvas(const std::string& canvas_id, gui::Canvas* canvas);
|
||||
|
||||
// Register an overworld editor (for tile get/set callbacks)
|
||||
void RegisterOverworldEditor(const std::string& canvas_id,
|
||||
editor::OverworldEditor* editor);
|
||||
|
||||
// RPC method implementations
|
||||
absl::Status SetTile(const proto::SetTileRequest* request,
|
||||
proto::SetTileResponse* response);
|
||||
|
||||
absl::Status GetTile(const proto::GetTileRequest* request,
|
||||
proto::GetTileResponse* response);
|
||||
|
||||
absl::Status SetTiles(const proto::SetTilesRequest* request,
|
||||
proto::SetTilesResponse* response);
|
||||
|
||||
absl::Status SelectTile(const proto::SelectTileRequest* request,
|
||||
proto::SelectTileResponse* response);
|
||||
|
||||
absl::Status SelectTileRect(const proto::SelectTileRectRequest* request,
|
||||
proto::SelectTileRectResponse* response);
|
||||
|
||||
absl::Status GetSelection(const proto::GetSelectionRequest* request,
|
||||
proto::GetSelectionResponse* response);
|
||||
|
||||
absl::Status ClearSelection(const proto::ClearSelectionRequest* request,
|
||||
proto::ClearSelectionResponse* response);
|
||||
|
||||
absl::Status ScrollToTile(const proto::ScrollToTileRequest* request,
|
||||
proto::ScrollToTileResponse* response);
|
||||
|
||||
absl::Status CenterOn(const proto::CenterOnRequest* request,
|
||||
proto::CenterOnResponse* response);
|
||||
|
||||
absl::Status SetZoom(const proto::SetZoomRequest* request,
|
||||
proto::SetZoomResponse* response);
|
||||
|
||||
absl::Status GetZoom(const proto::GetZoomRequest* request,
|
||||
proto::GetZoomResponse* response);
|
||||
|
||||
absl::Status GetDimensions(const proto::GetDimensionsRequest* request,
|
||||
proto::GetDimensionsResponse* response);
|
||||
|
||||
absl::Status GetVisibleRegion(const proto::GetVisibleRegionRequest* request,
|
||||
proto::GetVisibleRegionResponse* response);
|
||||
|
||||
absl::Status IsTileVisible(const proto::IsTileVisibleRequest* request,
|
||||
proto::IsTileVisibleResponse* response);
|
||||
|
||||
private:
|
||||
gui::Canvas* GetCanvas(const std::string& canvas_id);
|
||||
editor::OverworldEditor* GetOverworldEditor(const std::string& canvas_id);
|
||||
|
||||
// Canvas registry
|
||||
std::unordered_map<std::string, gui::Canvas*> canvases_;
|
||||
|
||||
// Editor registry (for tile callbacks)
|
||||
std::unordered_map<std::string, editor::OverworldEditor*> overworld_editors_;
|
||||
};
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
#endif // YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_
|
||||
|
||||
Reference in New Issue
Block a user