yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_group_editor.cc
Go to the documentation of this file.
1#include "gfx_group_editor.h"
2#include "util/i18n/tr.h"
3
4#include "absl/status/status.h"
5#include "absl/strings/str_cat.h"
6#include "absl/strings/str_format.h"
12#include "app/gui/core/color.h"
13#include "app/gui/core/icons.h"
14#include "app/gui/core/input.h"
17#include "imgui/imgui.h"
18#include "rom/rom.h"
19
20namespace yaze {
21namespace editor {
22
23using ImGui::BeginChild;
24using ImGui::BeginCombo;
25using ImGui::BeginGroup;
26using ImGui::BeginTabItem;
27using ImGui::BeginTable;
28using ImGui::EndChild;
29using ImGui::EndCombo;
30using ImGui::EndGroup;
31using ImGui::EndTabItem;
32using ImGui::EndTable;
33using ImGui::GetContentRegionAvail;
34using ImGui::GetStyle;
35using ImGui::IsItemClicked;
36using ImGui::PopID;
37using ImGui::PushID;
38using ImGui::SameLine;
39using ImGui::Selectable;
40using ImGui::Separator;
41using ImGui::SetNextItemWidth;
42using ImGui::SliderFloat;
43using ImGui::TableHeadersRow;
44using ImGui::TableNextColumn;
45using ImGui::TableNextRow;
46using ImGui::TableSetupColumn;
47using ImGui::Text;
48
49using gfx::kPaletteGroupNames;
51
52namespace {
53
54// Constants for sheet display
55constexpr int kSheetDisplayWidth = 256; // 2x scale from 128px sheets
56constexpr int kSheetDisplayHeight = 64; // 2x scale from 32px sheets
57constexpr float kDefaultScale = 2.0f;
58constexpr int kTileSize = 16; // 8px tiles at 2x scale
59
60// Draw a single sheet with proper scaling and unique ID
61void DrawScaledSheet(gui::Canvas& canvas, gfx::Bitmap& sheet, int unique_id,
62 float scale = kDefaultScale) {
63 PushID(unique_id);
64
65 // Each sheet viewer is read-only; declare it so future canvas surfaces
66 // (cursors, hover hints, context-menu items) can default appropriately
67 // without the caller threading the policy through every frame.
69
70 // Calculate scaled dimensions
71 int display_width = static_cast<int>(gfx::kTilesheetWidth * scale);
72 int display_height = static_cast<int>(gfx::kTilesheetHeight * scale);
73
74 // Draw canvas background
75 canvas.DrawBackground(ImVec2(display_width + 1, display_height + 1));
76 canvas.DrawContextMenu();
77
78 // Draw bitmap with proper scale
79 canvas.DrawBitmap(sheet, 2, scale);
80
81 // Draw grid at scaled tile size
82 canvas.DrawGrid(static_cast<int>(8 * scale));
83 canvas.DrawOverlay();
84
85 PopID();
86}
87
88} // namespace
89
90absl::Status GfxGroupEditor::Update() {
91 if (!host_surface_hint_.empty()) {
92 ImGui::TextDisabled("%s", host_surface_hint_.c_str());
93 Separator();
94 }
95
96 // Palette controls at top for all tabs
98 Separator();
99
100 if (gui::BeginThemedTabBar("##GfxGroupEditorTabs")) {
101 if (BeginTabItem(tr("Blocksets"))) {
102 gui::InputHexByte("Selected Blockset", &Ws().selected_blockset,
103 static_cast<uint8_t>(0x24));
105 false, "blockset", "0x" + std::to_string(Ws().selected_blockset),
106 "Blockset " + std::to_string(Ws().selected_blockset));
108 EndTabItem();
109 }
110
111 if (BeginTabItem(tr("Roomsets"))) {
112 gui::InputHexByte("Selected Roomset", &Ws().selected_roomset,
113 static_cast<uint8_t>(81));
115 false, "roomset", "0x" + std::to_string(Ws().selected_roomset),
116 "Roomset " + std::to_string(Ws().selected_roomset));
118 EndTabItem();
119 }
120
121 if (BeginTabItem(tr("Spritesets"))) {
122 gui::InputHexByte("Selected Spriteset", &Ws().selected_spriteset,
123 static_cast<uint8_t>(143));
125 false, "spriteset", "0x" + std::to_string(Ws().selected_spriteset),
126 "Spriteset " + std::to_string(Ws().selected_spriteset));
128 EndTabItem();
129 }
130
132 }
133
134 return absl::OkStatus();
135}
136
138 if (!game_data()) {
139 Text(tr("No game data loaded"));
140 return;
141 }
142
143 PushID("BlocksetViewer");
144 auto& ws = Ws();
145
146 if (BeginTable("##BlocksetTable", sheet_only ? 1 : 2,
147 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
148 ImVec2(0, 0))) {
149 if (!sheet_only) {
150 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch,
151 GetContentRegionAvail().x);
152 }
153
154 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
155 kSheetDisplayWidth + 16);
156 TableHeadersRow();
157 TableNextRow();
158
159 if (!sheet_only) {
160 TableNextColumn();
161 BeginGroup();
162 for (int idx = 0; idx < 8; idx++) {
163 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
165 ("Slot " + std::to_string(idx)).c_str(),
166 &game_data()->main_blockset_ids[ws.selected_blockset][idx]);
167 }
168 EndGroup();
169 }
170
171 TableNextColumn();
172 BeginGroup();
173 for (int idx = 0; idx < 8; idx++) {
174 int sheet_id = game_data()->main_blockset_ids[ws.selected_blockset][idx];
175 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
176
177 // Make sure the sheet has a GPU texture before the canvas tries to draw
178 // it; otherwise canvas_rendering silently returns and the slot is blank.
180
181 if (ws.override_palette && current_palette_) {
182 sheet.SetPalette(*current_palette_);
185 sheet, gfx::RoleForBlocksetSlot(idx),
186 game_data()->palette_groups)) {
188 }
189
190 // Unique ID combining blockset, slot, and sheet
191 int unique_id = (ws.selected_blockset << 16) | (idx << 8) | sheet_id;
192 DrawScaledSheet(blockset_canvases_[idx], sheet, unique_id, ws.view_scale);
193 }
194 EndGroup();
195 EndTable();
196 }
197
198 PopID();
199}
200
202 if (!game_data()) {
203 Text(tr("No game data loaded"));
204 return;
205 }
206
207 PushID("RoomsetViewer");
208 auto& ws = Ws();
209 Text(tr("Roomsets overwrite slots 4-7 of the main blockset"));
210
211 if (BeginTable("##RoomsTable", 3,
212 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
213 ImVec2(0, 0))) {
214 TableSetupColumn("List", ImGuiTableColumnFlags_WidthFixed, 120);
215 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch,
216 GetContentRegionAvail().x);
217 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
218 kSheetDisplayWidth + 16);
219 TableHeadersRow();
220 TableNextRow();
221
222 // Roomset list column
223 TableNextColumn();
224 if (BeginChild("##RoomsetListChild", ImVec2(0, 300))) {
225 for (int idx = 0; idx < 0x51; idx++) {
226 PushID(idx);
227 std::string roomset_label = absl::StrFormat("0x%02X", idx);
228 bool is_selected = (ws.selected_roomset == static_cast<uint8_t>(idx));
229 if (Selectable(roomset_label.c_str(), is_selected)) {
230 ws.selected_roomset = static_cast<uint8_t>(idx);
231 }
232 PopID();
233 }
234 }
235 EndChild();
236
237 // Inputs column
238 TableNextColumn();
239 BeginGroup();
240 Text(tr("Sheet IDs (overwrites slots 4-7):"));
241 for (int idx = 0; idx < 4; idx++) {
242 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
244 ("Slot " + std::to_string(idx + 4)).c_str(),
245 &game_data()->room_blockset_ids[ws.selected_roomset][idx]);
246 }
247 EndGroup();
248
249 // Sheets column
250 TableNextColumn();
251 BeginGroup();
252 for (int idx = 0; idx < 4; idx++) {
253 int sheet_id = game_data()->room_blockset_ids[ws.selected_roomset][idx];
254 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
255
257
258 if (ws.override_palette && current_palette_) {
259 sheet.SetPalette(*current_palette_);
262 sheet, gfx::RoleForRoomsetSlot(idx),
263 game_data()->palette_groups)) {
265 }
266
267 // Unique ID combining roomset, slot, and sheet
268 int unique_id =
269 (0x1000) | (ws.selected_roomset << 8) | (idx << 4) | sheet_id;
270 DrawScaledSheet(roomset_canvases_[idx], sheet, unique_id, ws.view_scale);
271 }
272 EndGroup();
273 EndTable();
274 }
275
276 PopID();
277}
278
280 if (!game_data()) {
281 Text(tr("No game data loaded"));
282 return;
283 }
284
285 PushID("SpritesetViewer");
286 auto& ws = Ws();
287
288 if (BeginTable("##SpritesTable", sheet_only ? 1 : 2,
289 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
290 ImVec2(0, 0))) {
291 if (!sheet_only) {
292 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch,
293 GetContentRegionAvail().x);
294 }
295 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed,
296 kSheetDisplayWidth + 16);
297 TableHeadersRow();
298 TableNextRow();
299
300 if (!sheet_only) {
301 TableNextColumn();
302 BeginGroup();
303 Text(tr("Sprite sheet IDs (base 115+):"));
304 for (int idx = 0; idx < 4; idx++) {
305 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
307 ("Slot " + std::to_string(idx)).c_str(),
308 &game_data()->spriteset_ids[ws.selected_spriteset][idx]);
309 }
310 EndGroup();
311 }
312
313 TableNextColumn();
314 BeginGroup();
315 for (int idx = 0; idx < 4; idx++) {
316 int sheet_offset = game_data()->spriteset_ids[ws.selected_spriteset][idx];
317 int sheet_id = 115 + sheet_offset;
318 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id);
319
321
322 if (ws.override_palette && current_palette_) {
323 sheet.SetPalette(*current_palette_);
326 sheet, gfx::RoleForSpritesetSlot(idx),
327 game_data()->palette_groups)) {
329 }
330
331 // Unique ID combining spriteset, slot, and sheet
332 int unique_id =
333 (0x2000) | (ws.selected_spriteset << 8) | (idx << 4) | sheet_offset;
334 DrawScaledSheet(spriteset_canvases_[idx], sheet, unique_id,
335 ws.view_scale);
336 }
337 EndGroup();
338 EndTable();
339 }
340
341 PopID();
342}
343
344namespace {
346 if (palette.empty()) {
347 return;
348 }
349 for (size_t color_idx = 0; color_idx < palette.size(); color_idx++) {
350 PushID(static_cast<int>(color_idx));
351 if ((color_idx % 8) != 0) {
352 SameLine(0.0f, GetStyle().ItemSpacing.y);
353 }
354
355 // Small icon of the color in the palette
356 gui::SnesColorButton(absl::StrCat("Palette", color_idx), palette[color_idx],
357 ImGuiColorEditFlags_NoAlpha |
358 ImGuiColorEditFlags_NoPicker |
359 ImGuiColorEditFlags_NoTooltip);
360
361 PopID();
362 }
363}
364} // namespace
365
367 if (!game_data()) {
368 return;
369 }
370
371 auto& ws = Ws();
372
373 // View scale control
374 Text(ICON_MD_ZOOM_IN " View");
375 SameLine();
376 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
377 SliderFloat("##ViewScale", &ws.view_scale, 1.0f, 4.0f, "%.1fx");
378 SameLine();
379
380 // Palette category selector
381 Text(ICON_MD_PALETTE " Palette");
382 SameLine();
383 SetNextItemWidth(gui::LayoutHelpers::GetComboWidth());
384
385 // Use the category names array for display
386 static constexpr int kNumPaletteCategories = 14;
387 if (BeginCombo(
388 "##PaletteCategory",
389 gfx::kPaletteCategoryNames[ws.selected_palette_category].data())) {
390 for (int cat = 0; cat < kNumPaletteCategories; cat++) {
391 auto category = static_cast<PaletteCategory>(cat);
392 bool is_selected = (ws.selected_palette_category == category);
393 if (Selectable(gfx::kPaletteCategoryNames[category].data(),
394 is_selected)) {
395 ws.selected_palette_category = category;
396 ws.selected_palette_index = 0;
398 }
399 if (is_selected) {
400 ImGui::SetItemDefaultFocus();
401 }
402 }
403 EndCombo();
404 }
405
406 SameLine();
407 SetNextItemWidth(gui::LayoutHelpers::GetHexInputWidth());
408 if (gui::InputHexByte("##PaletteIndex", &ws.selected_palette_index)) {
410 }
411
412 SameLine();
413 ImGui::Checkbox(tr("Override palette"), &ws.override_palette);
414 if (ImGui::IsItemHovered()) {
415 ImGui::SetTooltip(
416 tr("When off, each sheet renders against its slot's role-default "
417 "palette (overworld main/aux/animated, sprites, hud, dungeon).\n"
418 "When on, the palette picked above is forced onto every sheet."));
419 }
420
421 // Show current palette preview
423 SameLine();
424 DrawPaletteFromPaletteGroup(*current_palette_);
425 }
426}
427
429 if (!game_data()) {
430 current_palette_ = nullptr;
431 return;
432 }
433
434 const auto& ws = Ws();
435 auto& groups = game_data()->palette_groups;
436 switch (ws.selected_palette_category) {
437 case PaletteCategory::kSword:
439 groups.swords.mutable_palette(ws.selected_palette_index);
440 break;
441 case PaletteCategory::kShield:
443 groups.shields.mutable_palette(ws.selected_palette_index);
444 break;
445 case PaletteCategory::kClothes:
447 groups.armors.mutable_palette(ws.selected_palette_index);
448 break;
449 case PaletteCategory::kWorldColors:
451 groups.overworld_main.mutable_palette(ws.selected_palette_index);
452 break;
453 case PaletteCategory::kAreaColors:
455 groups.overworld_aux.mutable_palette(ws.selected_palette_index);
456 break;
457 case PaletteCategory::kGlobalSprites:
459 groups.global_sprites.mutable_palette(ws.selected_palette_index);
460 break;
461 case PaletteCategory::kSpritesAux1:
463 groups.sprites_aux1.mutable_palette(ws.selected_palette_index);
464 break;
465 case PaletteCategory::kSpritesAux2:
467 groups.sprites_aux2.mutable_palette(ws.selected_palette_index);
468 break;
469 case PaletteCategory::kSpritesAux3:
471 groups.sprites_aux3.mutable_palette(ws.selected_palette_index);
472 break;
473 case PaletteCategory::kDungeons:
475 groups.dungeon_main.mutable_palette(ws.selected_palette_index);
476 break;
477 case PaletteCategory::kWorldMap:
478 case PaletteCategory::kDungeonMap:
480 groups.overworld_mini_map.mutable_palette(ws.selected_palette_index);
481 break;
482 case PaletteCategory::kTriforce:
483 case PaletteCategory::kCrystal:
485 groups.object_3d.mutable_palette(ws.selected_palette_index);
486 break;
487 default:
488 current_palette_ = nullptr;
489 break;
490 }
491}
492
493} // namespace editor
494} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:162
std::array< gui::Canvas, 8 > blockset_canvases_
zelda3::GameData * game_data() const
std::array< gui::Canvas, 4 > roomset_canvases_
GfxGroupWorkspaceState & Ws()
gfx::SnesPalette * current_palette_
void DrawSpritesetViewer(bool sheet_only=false)
std::array< gui::Canvas, 4 > spriteset_canvases_
void DrawBlocksetViewer(bool sheet_only=false)
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
Definition arena.h:178
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
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1162
void DrawContextMenu()
Definition canvas.cc:692
CanvasConfig & GetConfig()
Definition canvas.h:229
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:602
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1484
static float GetHexInputWidth()
static float GetSliderWidth()
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_PALETTE
Definition icons.h:1370
void DrawScaledSheet(gui::Canvas &canvas, gfx::Bitmap &sheet, int unique_id, float scale=kDefaultScale)
void EnsureSheetTextureQueued(gfx::Bitmap &sheet)
bool ApplyRoleDefaultPalette(gfx::Bitmap &sheet, gfx::SheetRole role, gfx::PaletteGroupMap &palette_groups)
PaletteCategory
Categories for organizing palette groups in the UI.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
SheetRole RoleForRoomsetSlot(int slot_index)
SheetRole RoleForBlocksetSlot(int slot_index)
SheetRole RoleForSpritesetSlot(int slot_index)
IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor &color, ImGuiColorEditFlags flags, const ImVec2 &size_arg)
Definition color.cc:41
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:376
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2341
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:96
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:95
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:94