yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
pixel_editor_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <memory>
6#include <queue>
7
8#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
12#include "app/gui/core/style.h"
17#include "imgui/imgui.h"
18
19namespace yaze {
20namespace editor {
21
23 // Canvas is initialized via member initializer list
24}
25
26void PixelEditorPanel::Draw(bool* p_open) {
27 // EditorPanel interface - delegate to Update()
28 Update().IgnoreError();
29}
30
32 // Top toolbar
34 ImGui::SameLine();
36
37 ImGui::Separator();
38
39 constexpr float kColorPickerWidth = 200.0f;
40 constexpr float kStatusBarHeight = 24.0f;
41
42 // Main content area with canvas and side panels
43 ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -kStatusBarHeight),
44 false);
45
46 // Color picker on the left
47 ImGui::BeginChild("##ColorPickerSide", ImVec2(kColorPickerWidth, 0), true);
49 ImGui::Separator();
51 ImGui::EndChild();
52
53 ImGui::SameLine();
54
55 // Main canvas
56 ImGui::BeginChild("##CanvasArea", ImVec2(0, 0), true,
57 ImGuiWindowFlags_HorizontalScrollbar);
58 DrawCanvas();
59 ImGui::EndChild();
60
61 ImGui::EndChild();
62
63 // Status bar
65
66 return absl::OkStatus();
67}
68
70 // Tool selection buttons
71 auto tool_button = [this](PixelTool tool, const char* icon,
72 const char* tooltip) {
73 bool is_selected = state_->current_tool == tool;
74 std::optional<gui::StyleColorGuard> sel_guard;
75 if (is_selected) {
76 sel_guard.emplace(ImGuiCol_Button, gui::GetPrimaryVec4());
77 }
78 if (ImGui::Button(icon)) {
79 state_->SetTool(tool);
80 }
81 sel_guard.reset();
82 if (ImGui::IsItemHovered()) {
83 ImGui::SetTooltip("%s", tooltip);
84 }
85 ImGui::SameLine();
86 };
87
88 tool_button(PixelTool::kSelect, ICON_MD_SELECT_ALL, "Select (V)");
89 tool_button(PixelTool::kPencil, ICON_MD_DRAW, "Pencil (B)");
90 tool_button(PixelTool::kBrush, ICON_MD_BRUSH, "Brush (B)");
91 tool_button(PixelTool::kEraser, ICON_MD_AUTO_FIX_HIGH, "Eraser (E)");
92 tool_button(PixelTool::kFill, ICON_MD_FORMAT_COLOR_FILL, "Fill (G)");
93 tool_button(PixelTool::kLine, ICON_MD_HORIZONTAL_RULE, "Line");
94 tool_button(PixelTool::kRectangle, ICON_MD_CROP_SQUARE, "Rectangle");
95 tool_button(PixelTool::kEyedropper, ICON_MD_COLORIZE, "Eyedropper (I)");
96
97 ImGui::SameLine();
98 ImGui::Text("|");
99 ImGui::SameLine();
100
101 // Brush size for pencil/brush/eraser
105 ImGui::SetNextItemWidth(80);
106 int brush = state_->brush_size;
107 if (ImGui::SliderInt("##BrushSize", &brush, 1, 8, "%d px")) {
108 state_->brush_size = static_cast<uint8_t>(brush);
109 }
110 HOVER_HINT("Brush size");
111 ImGui::SameLine();
112 }
113
114 // Undo/Redo buttons
115 ImGui::Text("|");
116 ImGui::SameLine();
117
118 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanUndo());
119 if (gui::ToolbarIconButton(ICON_MD_UNDO, "Undo (Ctrl+Z)") && undo_manager_) {
120 undo_manager_->Undo().IgnoreError();
121 }
122 ImGui::EndDisabled();
123
124 ImGui::SameLine();
125
126 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanRedo());
127 if (gui::ToolbarIconButton(ICON_MD_REDO, "Redo (Ctrl+Y)") && undo_manager_) {
128 undo_manager_->Redo().IgnoreError();
129 }
130 ImGui::EndDisabled();
131}
132
134 // Zoom controls
135 if (gui::ToolbarIconButton(ICON_MD_ZOOM_OUT, "Zoom out (-)")) {
136 state_->ZoomOut();
137 }
138 ImGui::SameLine();
139
140 ImGui::SetNextItemWidth(100);
141 float zoom = state_->zoom_level;
142 if (ImGui::SliderFloat("##Zoom", &zoom, 1.0f, 16.0f, "%.0fx")) {
143 state_->SetZoom(zoom);
144 }
145 ImGui::SameLine();
146
147 if (gui::ToolbarIconButton(ICON_MD_ZOOM_IN, "Zoom in (+)")) {
148 state_->ZoomIn();
149 }
150
151 ImGui::SameLine();
152 ImGui::Text("|");
153 ImGui::SameLine();
154
155 // View overlay toggles
156 ImGui::Checkbox(ICON_MD_GRID_ON, &state_->show_grid);
157 HOVER_HINT("Toggle grid (Ctrl+G)");
158 ImGui::SameLine();
159
160 ImGui::Checkbox(ICON_MD_ADD, &state_->show_cursor_crosshair);
161 HOVER_HINT("Toggle cursor crosshair");
162 ImGui::SameLine();
163
164 ImGui::Checkbox(ICON_MD_BRUSH, &state_->show_brush_preview);
165 HOVER_HINT("Toggle brush preview");
166 ImGui::SameLine();
167
169 HOVER_HINT("Toggle transparency grid");
170}
171
173 if (state_->open_sheets.empty()) {
174 ImGui::TextDisabled("No sheet selected. Select a sheet from the browser.");
175 return;
176 }
177
178 // Tab bar for open sheets
179 if (gui::BeginThemedTabBar("##SheetTabs",
180 ImGuiTabBarFlags_AutoSelectNewTabs |
181 ImGuiTabBarFlags_Reorderable |
182 ImGuiTabBarFlags_TabListPopupButton)) {
183 std::vector<uint16_t> sheets_to_close;
184
185 for (uint16_t sheet_id : state_->open_sheets) {
186 bool open = true;
187 std::string tab_label = absl::StrFormat("%02X", sheet_id);
188 if (state_->modified_sheets.count(sheet_id) > 0) {
189 tab_label += "*";
190 }
191
192 if (ImGui::BeginTabItem(tab_label.c_str(), &open)) {
193 state_->current_sheet_id = sheet_id;
194
195 // Get the current sheet bitmap
196 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(
198
199 if (!sheet.is_active()) {
200 ImGui::TextDisabled("Sheet %02X is not active", sheet_id);
201 ImGui::EndTabItem();
202 continue;
203 }
204
205 // Calculate canvas size based on zoom
206 float canvas_width = sheet.width() * state_->zoom_level;
207 float canvas_height = sheet.height() * state_->zoom_level;
208
209 // Draw canvas background
210 canvas_.DrawBackground(ImVec2(canvas_width, canvas_height));
211
212 // Draw transparency checkerboard background if enabled
214 DrawTransparencyGrid(canvas_width, canvas_height);
215 }
216
217 // Draw the sheet texture
218 if (sheet.texture()) {
219 canvas_.draw_list()->AddImage(
220 (ImTextureID)(intptr_t)sheet.texture(), canvas_.zero_point(),
221 ImVec2(canvas_.zero_point().x + canvas_width,
222 canvas_.zero_point().y + canvas_height));
223 }
224
225 // Draw grid if enabled
226 if (state_->show_grid) {
228 }
229
230 // Draw transient tile highlight (e.g., from "Edit Graphics" jump)
231 DrawTileHighlight(sheet);
232
233 // Draw selection rectangle if active
235 ImVec2 sel_min =
237 ImVec2 sel_max =
240 canvas_.draw_list()->AddRect(
241 sel_min, sel_max, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
242
243 // Marching ants effect (simplified)
244 canvas_.draw_list()->AddRect(sel_min, sel_max, IM_COL32(0, 0, 0, 128),
245 0.0f, 0, 1.0f);
246 }
247
248 // Draw tool preview (line/rectangle)
250 ImVec2 start = PixelToScreen(static_cast<int>(tool_start_pixel_.x),
251 static_cast<int>(tool_start_pixel_.y));
252 ImVec2 end = PixelToScreen(static_cast<int>(preview_end_.x),
253 static_cast<int>(preview_end_.y));
254
256 canvas_.draw_list()->AddLine(start, end, IM_COL32(255, 255, 0, 200),
257 2.0f);
259 canvas_.draw_list()->AddRect(start, end, IM_COL32(255, 255, 0, 200),
260 0.0f, 0, 2.0f);
261 }
262 }
263
264 // Draw cursor crosshair overlay if enabled and cursor in canvas
267 }
268
269 // Draw brush preview if using brush/eraser tool
274 }
275
277
278 // Handle mouse input
280
281 // Show pixel info tooltip if enabled
284 }
285
286 ImGui::EndTabItem();
287 }
288
289 if (!open) {
290 sheets_to_close.push_back(sheet_id);
291 }
292 }
293
294 // Close tabs that were requested
295 for (uint16_t sheet_id : sheets_to_close) {
296 state_->CloseSheet(sheet_id);
297 }
298
300 }
301}
302
304 float canvas_height) {
305 const float cell_size = 8.0f; // Checkerboard cell size
306 const ImU32 color1 = IM_COL32(180, 180, 180, 255);
307 const ImU32 color2 = IM_COL32(220, 220, 220, 255);
308
309 ImVec2 origin = canvas_.zero_point();
310 int cols = static_cast<int>(canvas_width / cell_size) + 1;
311 int rows = static_cast<int>(canvas_height / cell_size) + 1;
312
313 for (int row = 0; row < rows; row++) {
314 for (int col = 0; col < cols; col++) {
315 bool is_light = (row + col) % 2 == 0;
316 ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
317 ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
318 std::min(p_min.y + cell_size, origin.y + canvas_height));
319 canvas_.draw_list()->AddRectFilled(p_min, p_max,
320 is_light ? color1 : color2);
321 }
322 }
323}
324
326 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
327 float pixel_size = state_->zoom_level;
328
329 // Vertical line through cursor pixel
330 ImVec2 v_start(cursor_screen.x + pixel_size / 2, canvas_.zero_point().y);
331 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
333 canvas_.draw_list()->AddLine(v_start, v_end, IM_COL32(255, 100, 100, 100),
334 1.0f);
335
336 // Horizontal line through cursor pixel
337 ImVec2 h_start(canvas_.zero_point().x, cursor_screen.y + pixel_size / 2);
338 ImVec2 h_end(canvas_.zero_point().x + canvas_.canvas_size().x,
339 cursor_screen.y + pixel_size / 2);
340 canvas_.draw_list()->AddLine(h_start, h_end, IM_COL32(255, 100, 100, 100),
341 1.0f);
342
343 // Highlight current pixel with a bright outline
344 ImVec2 pixel_min = cursor_screen;
345 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
346 canvas_.draw_list()->AddRect(pixel_min, pixel_max,
347 IM_COL32(255, 255, 255, 200), 0.0f, 0, 2.0f);
348}
349
351 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
352 float pixel_size = state_->zoom_level;
353 int brush = state_->brush_size;
354 int half = brush / 2;
355
356 // Draw preview of brush area
357 ImVec2 brush_min(cursor_screen.x - half * pixel_size,
358 cursor_screen.y - half * pixel_size);
359 ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
360 cursor_screen.y + (brush - half) * pixel_size);
361
362 // Fill with semi-transparent color preview
363 ImU32 preview_color = (state_->current_tool == PixelTool::kEraser)
364 ? IM_COL32(255, 0, 0, 50)
365 : IM_COL32(0, 255, 0, 50);
366 canvas_.draw_list()->AddRectFilled(brush_min, brush_max, preview_color);
367
368 // Outline
369 ImU32 outline_color = (state_->current_tool == PixelTool::kEraser)
370 ? IM_COL32(255, 100, 100, 200)
371 : IM_COL32(100, 255, 100, 200);
372 canvas_.draw_list()->AddRect(brush_min, brush_max, outline_color, 0.0f, 0,
373 1.0f);
374}
375
377 if (cursor_x_ < 0 || cursor_x_ >= sheet.width() || cursor_y_ < 0 ||
378 cursor_y_ >= sheet.height()) {
379 return;
380 }
381
382 uint8_t color_index = sheet.GetPixel(cursor_x_, cursor_y_);
383 auto palette = sheet.palette();
384
385 ImGui::BeginTooltip();
386 ImGui::Text("Pos: %d, %d", cursor_x_, cursor_y_);
387 ImGui::Text("Tile: %d, %d", cursor_x_ / 8, cursor_y_ / 8);
388 ImGui::Text("Index: %d", color_index);
389
390 if (color_index < palette.size()) {
391 ImGui::Text("SNES: $%04X", palette[color_index].snes());
392 ImVec4 color(palette[color_index].rgb().x / 255.0f,
393 palette[color_index].rgb().y / 255.0f,
394 palette[color_index].rgb().z / 255.0f, 1.0f);
395 ImGui::ColorButton("##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
396 ImVec2(24, 24));
397 if (color_index == 0) {
398 ImGui::SameLine();
399 ImGui::TextDisabled("(Transparent)");
400 }
401 }
402 ImGui::EndTooltip();
403}
404
407 return;
408 }
410 return;
411 }
412
413 const double now = ImGui::GetTime();
414 const double elapsed = now - state_->tile_highlight.start_time;
415 if (elapsed > state_->tile_highlight.duration) {
417 return;
418 }
419
420 const int tiles_per_row = sheet.width() / 8;
421 if (tiles_per_row <= 0) {
422 return;
423 }
424 const uint16_t tile_index = state_->tile_highlight.tile_index;
425 const int tile_x = static_cast<int>(tile_index % tiles_per_row);
426 const int tile_y = static_cast<int>(tile_index / tiles_per_row);
427
428 const float pulse =
429 0.5f + 0.5f * std::sin(static_cast<float>(elapsed) * 6.0f);
430 const float alpha = 0.25f + (pulse * 0.35f);
431
432 ImVec2 min = PixelToScreen(tile_x * 8, tile_y * 8);
433 ImVec2 max = PixelToScreen(tile_x * 8 + 8, tile_y * 8 + 8);
434
435 ImVec4 sel = gui::GetSelectedColor();
436 sel.w = alpha;
437 const ImU32 fill_color = ImGui::GetColorU32(sel);
438 ImVec4 sel_outline = gui::GetSelectedColor();
439 sel_outline.w = 0.9f;
440 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
441 canvas_.draw_list()->AddRectFilled(min, max, fill_color);
442 canvas_.draw_list()->AddRect(min, max, outline_color, 0.0f, 0, 2.0f);
443
444 if (ImGui::IsMouseHoveringRect(min, max)) {
445 ImGui::BeginTooltip();
446 ImGui::Text("Focus tile: %d (sheet %02X)", tile_index,
448 if (!state_->tile_highlight.label.empty()) {
449 ImGui::Text("%s", state_->tile_highlight.label.c_str());
450 }
451 ImGui::EndTooltip();
452 }
453}
454
456 ImGui::Text("Colors");
457
458 if (state_->open_sheets.empty()) {
459 ImGui::TextDisabled("No sheet");
460 return;
461 }
462
463 auto& sheet =
465 auto palette = sheet.palette();
466
467 // Draw palette colors in 4x4 grid (16 colors)
468 for (int i = 0; i < static_cast<int>(palette.size()) && i < 16; i++) {
469 if (i > 0 && i % 4 == 0) {
470 // New row
471 } else if (i > 0) {
472 ImGui::SameLine();
473 }
474
475 ImVec4 color(palette[i].rgb().x / 255.0f, palette[i].rgb().y / 255.0f,
476 palette[i].rgb().z / 255.0f, 1.0f);
477
478 bool is_selected = state_->current_color_index == i;
479 std::optional<gui::StyleVarGuard> border_var;
480 std::optional<gui::StyleColorGuard> border_color;
481 if (is_selected) {
482 border_var.emplace(ImGuiStyleVar_FrameBorderSize, 2.0f);
483 border_color.emplace(ImGuiCol_Border, gui::GetWarningColor());
484 }
485
486 std::string id = absl::StrFormat("##Color%d", i);
487 if (ImGui::ColorButton(
488 id.c_str(), color,
489 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
490 ImVec2(24, 24))) {
491 state_->current_color_index = static_cast<uint8_t>(i);
492 state_->current_color = color;
493 }
494
495 if (is_selected) {
496 border_color.reset();
497 border_var.reset();
498 }
499
500 if (ImGui::IsItemHovered()) {
501 ImGui::BeginTooltip();
502 ImGui::Text("Index: %d", i);
503 ImGui::Text("SNES: $%04X", palette[i].snes());
504 ImGui::Text("RGB: %d, %d, %d", static_cast<int>(palette[i].rgb().x),
505 static_cast<int>(palette[i].rgb().y),
506 static_cast<int>(palette[i].rgb().z));
507 if (i == 0) {
508 ImGui::Text("(Transparent)");
509 }
510 ImGui::EndTooltip();
511 }
512 }
513
514 ImGui::Separator();
515
516 // Current color preview
517 ImGui::Text("Current:");
518 ImGui::ColorButton("##CurrentColor", state_->current_color,
519 ImGuiColorEditFlags_NoTooltip, ImVec2(40, 40));
520 ImGui::SameLine();
521 ImGui::Text("Index: %d", state_->current_color_index);
522}
523
525 ImGui::Text("Navigator");
526
527 if (state_->open_sheets.empty()) {
528 ImGui::TextDisabled("No sheet");
529 return;
530 }
531
532 auto& sheet =
534 if (!sheet.texture())
535 return;
536
537 // Draw mini version of the sheet
538 float mini_scale = 0.5f;
539 float mini_width = sheet.width() * mini_scale;
540 float mini_height = sheet.height() * mini_scale;
541
542 ImVec2 pos = ImGui::GetCursorScreenPos();
543
544 ImGui::GetWindowDrawList()->AddImage(
545 (ImTextureID)(intptr_t)sheet.texture(), pos,
546 ImVec2(pos.x + mini_width, pos.y + mini_height));
547
548 // Draw viewport rectangle
549 // TODO: Calculate actual viewport bounds based on scroll position
550
551 ImGui::Dummy(ImVec2(mini_width, mini_height));
552}
553
555 ImGui::Separator();
556
557 // Tool name
558 ImGui::Text("%s", state_->GetToolName());
559 ImGui::SameLine();
560
561 // Cursor position
562 if (cursor_in_canvas_) {
563 ImGui::Text("Pos: %d, %d", cursor_x_, cursor_y_);
564 ImGui::SameLine();
565
566 // Tile coordinates
567 int tile_x = cursor_x_ / 8;
568 int tile_y = cursor_y_ / 8;
569 ImGui::Text("Tile: %d, %d", tile_x, tile_y);
570 ImGui::SameLine();
571 }
572
573 // Sheet info
574 ImGui::Text("Sheet: %02X", state_->current_sheet_id);
575 ImGui::SameLine();
576
579 ImGui::TextColored(gui::GetWarningColor(), "Focus: %d",
581 ImGui::SameLine();
582 }
583
584 // Modified indicator
586 ImGui::TextColored(gui::GetModifiedColor(), "(Modified)");
587 }
588
589 // Zoom level
590 ImGui::SameLine();
591 ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80);
592 ImGui::Text("Zoom: %.0fx", state_->zoom_level);
593}
594
596 if (!ImGui::IsItemHovered()) {
597 cursor_in_canvas_ = false;
598 return;
599 }
600
601 cursor_in_canvas_ = true;
602 ImVec2 mouse_pos = ImGui::GetMousePos();
603 ImVec2 pixel_pos = ScreenToPixel(mouse_pos);
604
605 cursor_x_ = static_cast<int>(pixel_pos.x);
606 cursor_y_ = static_cast<int>(pixel_pos.y);
607
608 auto& sheet =
610
611 // Clamp to sheet bounds
612 cursor_x_ = std::clamp(cursor_x_, 0, sheet.width() - 1);
613 cursor_y_ = std::clamp(cursor_y_, 0, sheet.height() - 1);
614
615 // Mouse button handling
616 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
617 is_drawing_ = true;
619 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
621
622 // Save undo state before starting to draw
624
625 // Handle tools that need start position
626 switch (state_->current_tool) {
629 break;
632 break;
635 break;
636 case PixelTool::kFill:
638 break;
641 break;
644 break;
645 case PixelTool::kLine:
647 show_tool_preview_ = true;
648 break;
649 default:
650 break;
651 }
652 }
653
654 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && is_drawing_) {
656 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
657
658 switch (state_->current_tool) {
661 break;
664 break;
667 break;
670 break;
671 default:
672 break;
673 }
674
676 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
677 }
678
679 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && is_drawing_) {
680 is_drawing_ = false;
681
682 switch (state_->current_tool) {
683 case PixelTool::kLine:
684 DrawLine(static_cast<int>(tool_start_pixel_.x),
685 static_cast<int>(tool_start_pixel_.y), cursor_x_, cursor_y_);
686 break;
688 DrawRectangle(static_cast<int>(tool_start_pixel_.x),
689 static_cast<int>(tool_start_pixel_.y), cursor_x_,
690 cursor_y_, false);
691 break;
693 EndSelection();
694 break;
695 default:
696 break;
697 }
698
699 // Finalize undo action after the edit stroke completes
701
702 show_tool_preview_ = false;
703 }
704}
705
707 auto& sheet =
709
710 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
711 sheet.WriteToPixel(x, y, state_->current_color_index);
714 }
715}
716
718 auto& sheet =
720 int size = state_->brush_size;
721 int half = size / 2;
722
723 for (int dy = -half; dy < size - half; dy++) {
724 for (int dx = -half; dx < size - half; dx++) {
725 int px = x + dx;
726 int py = y + dy;
727 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
728 sheet.WriteToPixel(px, py, state_->current_color_index);
729 }
730 }
731 }
732
735}
736
738 auto& sheet =
740 int size = state_->brush_size;
741 int half = size / 2;
742
743 for (int dy = -half; dy < size - half; dy++) {
744 for (int dx = -half; dx < size - half; dx++) {
745 int px = x + dx;
746 int py = y + dy;
747 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
748 sheet.WriteToPixel(px, py, 0); // Index 0 = transparent
749 }
750 }
751 }
752
755}
756
757void PixelEditorPanel::ApplyFill(int x, int y) {
758 auto& sheet =
760
761 if (x < 0 || x >= sheet.width() || y < 0 || y >= sheet.height())
762 return;
763
764 uint8_t target_color = sheet.GetPixel(x, y);
765 uint8_t fill_color = state_->current_color_index;
766
767 if (target_color == fill_color)
768 return; // Nothing to fill
769
770 // BFS flood fill
771 std::queue<std::pair<int, int>> queue;
772 std::vector<bool> visited(sheet.width() * sheet.height(), false);
773
774 queue.push({x, y});
775 visited[y * sheet.width() + x] = true;
776
777 while (!queue.empty()) {
778 auto [cx, cy] = queue.front();
779 queue.pop();
780
781 sheet.WriteToPixel(cx, cy, fill_color);
782
783 // Check 4-connected neighbors
784 const int dx[] = {0, 0, -1, 1};
785 const int dy[] = {-1, 1, 0, 0};
786
787 for (int i = 0; i < 4; i++) {
788 int nx = cx + dx[i];
789 int ny = cy + dy[i];
790
791 if (nx >= 0 && nx < sheet.width() && ny >= 0 && ny < sheet.height()) {
792 int idx = ny * sheet.width() + nx;
793 if (!visited[idx] && sheet.GetPixel(nx, ny) == target_color) {
794 visited[idx] = true;
795 queue.push({nx, ny});
796 }
797 }
798 }
799 }
800
803}
804
807
808 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
809 state_->current_color_index = sheet.GetPixel(x, y);
810
811 // Update current color display
812 auto palette = sheet.palette();
813 if (state_->current_color_index < palette.size()) {
814 auto& color = palette[state_->current_color_index];
816 ImVec4(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
817 color.rgb().z / 255.0f, 1.0f);
818 }
819 }
820}
821
822void PixelEditorPanel::DrawLine(int x1, int y1, int x2, int y2) {
823 auto& sheet =
825
826 // Bresenham's line algorithm
827 int dx = std::abs(x2 - x1);
828 int dy = std::abs(y2 - y1);
829 int sx = x1 < x2 ? 1 : -1;
830 int sy = y1 < y2 ? 1 : -1;
831 int err = dx - dy;
832
833 while (true) {
834 if (x1 >= 0 && x1 < sheet.width() && y1 >= 0 && y1 < sheet.height()) {
835 sheet.WriteToPixel(x1, y1, state_->current_color_index);
836 }
837
838 if (x1 == x2 && y1 == y2)
839 break;
840
841 int e2 = 2 * err;
842 if (e2 > -dy) {
843 err -= dy;
844 x1 += sx;
845 }
846 if (e2 < dx) {
847 err += dx;
848 y1 += sy;
849 }
850 }
851
854}
855
856void PixelEditorPanel::DrawRectangle(int x1, int y1, int x2, int y2,
857 bool filled) {
858 auto& sheet =
860
861 int min_x = std::min(x1, x2);
862 int max_x = std::max(x1, x2);
863 int min_y = std::min(y1, y2);
864 int max_y = std::max(y1, y2);
865
866 if (filled) {
867 for (int y = min_y; y <= max_y; y++) {
868 for (int x = min_x; x <= max_x; x++) {
869 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
870 sheet.WriteToPixel(x, y, state_->current_color_index);
871 }
872 }
873 }
874 } else {
875 // Top and bottom edges
876 for (int x = min_x; x <= max_x; x++) {
877 if (x >= 0 && x < sheet.width()) {
878 if (min_y >= 0 && min_y < sheet.height())
879 sheet.WriteToPixel(x, min_y, state_->current_color_index);
880 if (max_y >= 0 && max_y < sheet.height())
881 sheet.WriteToPixel(x, max_y, state_->current_color_index);
882 }
883 }
884 // Left and right edges
885 for (int y = min_y; y <= max_y; y++) {
886 if (y >= 0 && y < sheet.height()) {
887 if (min_x >= 0 && min_x < sheet.width())
888 sheet.WriteToPixel(min_x, y, state_->current_color_index);
889 if (max_x >= 0 && max_x < sheet.width())
890 sheet.WriteToPixel(max_x, y, state_->current_color_index);
891 }
892 }
893 }
894
897}
898
900 state_->selection.x = x;
901 state_->selection.y = y;
904 state_->selection.is_active = true;
905 state_->is_selecting = true;
906}
907
909 int start_x = static_cast<int>(tool_start_pixel_.x);
910 int start_y = static_cast<int>(tool_start_pixel_.y);
911
912 state_->selection.x = std::min(start_x, x);
913 state_->selection.y = std::min(start_y, y);
914 state_->selection.width = std::abs(x - start_x) + 1;
915 state_->selection.height = std::abs(y - start_y) + 1;
916}
917
919 state_->is_selecting = false;
920
921 // Copy pixel data for the selection
922 if (state_->selection.width > 0 && state_->selection.height > 0) {
926
927 for (int y = 0; y < state_->selection.height; y++) {
928 for (int x = 0; x < state_->selection.width; x++) {
929 int src_x = state_->selection.x + x;
930 int src_y = state_->selection.y + y;
931 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
932 src_y < sheet.height()) {
934 sheet.GetPixel(src_x, src_y);
935 }
936 }
937 }
938
939 state_->selection.palette = sheet.palette();
940 }
941}
942
944 // Selection data is already in state_->selection
945}
946
948 if (state_->selection.pixel_data.empty())
949 return;
950
951 auto& sheet =
953
955
956 for (int dy = 0; dy < state_->selection.height; dy++) {
957 for (int dx = 0; dx < state_->selection.width; dx++) {
958 int dest_x = x + dx;
959 int dest_y = y + dy;
960 if (dest_x >= 0 && dest_x < sheet.width() && dest_y >= 0 &&
961 dest_y < sheet.height()) {
962 uint8_t pixel =
964 sheet.WriteToPixel(dest_x, dest_y, pixel);
965 }
966 }
967 }
968
972}
973
975 if (state_->selection.pixel_data.empty())
976 return;
977
978 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
979 for (int y = 0; y < state_->selection.height; y++) {
980 for (int x = 0; x < state_->selection.width; x++) {
981 int src_idx = y * state_->selection.width + x;
982 int dst_idx =
983 y * state_->selection.width + (state_->selection.width - 1 - x);
984 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
985 }
986 }
987 state_->selection.pixel_data = std::move(flipped);
988}
989
991 if (state_->selection.pixel_data.empty())
992 return;
993
994 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
995 for (int y = 0; y < state_->selection.height; y++) {
996 for (int x = 0; x < state_->selection.width; x++) {
997 int src_idx = y * state_->selection.width + x;
998 int dst_idx =
999 (state_->selection.height - 1 - y) * state_->selection.width + x;
1000 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
1001 }
1002 }
1003 state_->selection.pixel_data = std::move(flipped);
1004}
1005
1014
1017 has_pending_undo_ = false;
1018 return;
1019 }
1020
1022 auto after_data = sheet.vector();
1023
1024 // Only push if the data actually changed
1025 if (after_data != pending_undo_before_data_) {
1026 auto description =
1027 absl::StrFormat("Edit pixels on sheet %02X", pending_undo_sheet_id_);
1028 undo_manager_->Push(std::make_unique<GraphicsPixelEditAction>(
1030 std::move(after_data), std::move(description)));
1031 }
1032
1033 has_pending_undo_ = false;
1035}
1036
1037ImVec2 PixelEditorPanel::ScreenToPixel(ImVec2 screen_pos) {
1038 float px = (screen_pos.x - canvas_.zero_point().x) / state_->zoom_level;
1039 float py = (screen_pos.y - canvas_.zero_point().y) / state_->zoom_level;
1040 return ImVec2(px, py);
1041}
1042
1044 return ImVec2(canvas_.zero_point().x + x * state_->zoom_level,
1045 canvas_.zero_point().y + y * state_->zoom_level);
1046}
1047
1048} // namespace editor
1049} // namespace yaze
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
void SetZoom(float zoom)
Set zoom level with clamping.
const char * GetToolName() const
Get tool name for status display.
void CloseSheet(uint16_t sheet_id)
Close a sheet tab.
void SetTool(PixelTool tool)
Set the current editing tool.
void EndSelection()
Finalize the selection.
void DrawLine(int x1, int y1, int x2, int y2)
Draw line from start to end.
void DrawColorPicker()
Draw the color palette picker.
void ApplyEraser(int x, int y)
Apply eraser tool at position.
void DrawPixelInfoTooltip(const gfx::Bitmap &sheet)
Draw tooltip with pixel information.
void DrawCursorCrosshair()
Draw crosshair at cursor position.
void DrawMiniMap()
Draw the mini navigation map.
void Draw(bool *p_open) override
Draw the pixel editor UI (EditorPanel interface)
void HandleCanvasInput()
Handle canvas mouse input for current tool.
ImVec2 ScreenToPixel(ImVec2 screen_pos)
Convert screen coordinates to pixel coordinates.
void DrawBrushPreview()
Draw brush size preview circle.
void ApplyEyedropper(int x, int y)
Apply eyedropper tool at position.
void CopySelection()
Copy selection to clipboard.
void UpdateSelection(int x, int y)
Update selection during drag.
void FlipSelectionVertical()
Flip selection vertically.
void ApplyBrush(int x, int y)
Apply brush tool at position.
void Initialize()
Initialize the panel.
void FlipSelectionHorizontal()
Flip selection horizontally.
void DrawViewControls()
Draw zoom and view controls.
void ApplyPencil(int x, int y)
Apply pencil tool at position.
void DrawToolbar()
Draw the toolbar with tool selection.
absl::Status Update()
Legacy Update method for backward compatibility.
ImVec2 PixelToScreen(int x, int y)
Convert pixel coordinates to screen coordinates.
void BeginSelection(int x, int y)
Start a new selection.
void DrawStatusBar()
Draw the status bar with cursor position.
void DrawTransparencyGrid(float canvas_width, float canvas_height)
Draw checkerboard pattern for transparent pixels.
void DrawTileHighlight(const gfx::Bitmap &sheet)
Draw a transient highlight for a target tile.
void ApplyFill(int x, int y)
Apply flood fill starting at position.
void PasteSelection(int x, int y)
Paste clipboard at position.
void DrawRectangle(int x1, int y1, int x2, int y2, bool filled)
Draw rectangle from start to end.
void FinalizeUndoAction()
Finalize the current undo action by capturing the after-snapshot and pushing a GraphicsPixelEditActio...
void DrawCanvas()
Draw the main editing canvas.
std::vector< uint8_t > pending_undo_before_data_
void SaveUndoState()
Save current state for undo (captures before-snapshot)
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
Definition arena.h:178
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
void NotifySheetModified(int sheet_index)
Notify Arena that a graphics sheet has been modified.
Definition arena.cc:393
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const SnesPalette & palette() const
Definition bitmap.h:368
int height() const
Definition bitmap.h:374
int width() const
Definition bitmap.h:373
uint8_t GetPixel(int x, int y) const
Get the palette index at the given x,y coordinates.
Definition bitmap.h:274
auto draw_list() const
Definition canvas.h:442
auto canvas_size() const
Definition canvas.h:451
auto zero_point() const
Definition canvas.h:443
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:590
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1480
#define ICON_MD_COLORIZE
Definition icons.h:441
#define ICON_MD_TEXTURE
Definition icons.h:1965
#define ICON_MD_DRAW
Definition icons.h:625
#define ICON_MD_BRUSH
Definition icons.h:325
#define ICON_MD_ZOOM_OUT
Definition icons.h:2196
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_FORMAT_COLOR_FILL
Definition icons.h:830
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_CROP_SQUARE
Definition icons.h:500
#define ICON_MD_HORIZONTAL_RULE
Definition icons.h:960
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_UNDO
Definition icons.h:2039
#define HOVER_HINT(string)
Definition macro.h:24
PixelTool
Pixel editing tool types for the graphics editor.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
ImVec4 GetSelectedColor()
Definition ui_helpers.cc:98
void EndThemedTabBar()
ImVec4 GetPrimaryVec4()
ImVec4 GetWarningColor()
Definition ui_helpers.cc:53
ImVec4 GetModifiedColor()
bool ToolbarIconButton(const char *icon, const char *tooltip, bool is_active)
Convenience wrapper for toolbar-sized icon buttons.
std::vector< uint8_t > pixel_data