yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_tile_editor_panel.cc
Go to the documentation of this file.
2
3#include "absl/strings/str_format.h"
5#include "imgui/imgui.h"
7
8namespace yaze {
9namespace editor {
10
12 Rom* rom)
13 : renderer_(renderer), rom_(rom) {
14 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
15}
16
18 int16_t object_id, int room_id,
19 std::array<zelda3::Room, 0x128>* rooms) {
20 current_object_id_ = object_id;
21 current_room_id_ = room_id;
22 rooms_ = rooms;
25 preview_dirty_ = true;
26 atlas_dirty_ = true;
27 is_open_ = true;
28
29 if (!rooms_ || current_room_id_ < 0 ||
30 current_room_id_ >= static_cast<int>(rooms_->size())) {
31 return;
32 }
33
34 auto& room = (*rooms_)[current_room_id_];
35 auto layout_or = tile_editor_->CaptureObjectLayout(
36 object_id, room, current_palette_group_);
37 if (layout_or.ok()) {
38 current_layout_ = std::move(layout_or.value());
39 }
40}
41
43 int width, int height, const std::string& filename,
44 int16_t object_id, int room_id,
45 std::array<zelda3::Room, 0x128>* rooms) {
46 current_object_id_ = object_id;
47 current_room_id_ = room_id;
48 rooms_ = rooms;
51 preview_dirty_ = true;
52 atlas_dirty_ = true;
53 is_open_ = true;
54 is_new_object_ = true;
55
57 width, height, object_id, filename);
58}
59
67
72
73void ObjectTileEditorPanel::Draw(bool* p_open) {
74 if (!is_open_ || current_layout_.cells.empty()) return;
75
76 std::string title;
77 if (is_new_object_) {
78 title = absl::StrFormat(
79 ICON_MD_ADD_BOX " New Object (%dx%d) - %s###ObjTileEditor",
82 } else {
83 title = absl::StrFormat(
84 ICON_MD_GRID_ON " Object 0x%03X - %s###ObjTileEditor",
87 }
88
89 ImGui::SetNextWindowSize(ImVec2(550, 500), ImGuiCond_FirstUseEver);
90 if (!ImGui::Begin(title.c_str(), &is_open_)) {
91 ImGui::End();
92 return;
93 }
94
95 if (!is_open_) {
96 Close();
97 ImGui::End();
98 return;
99 }
100
101 // Two-column layout: tile grid + source sheet
102 if (ImGui::BeginTable("##TileEditorLayout", 2,
103 ImGuiTableFlags_Resizable |
104 ImGuiTableFlags_BordersInnerV)) {
105 ImGui::TableSetupColumn("Tile Grid", ImGuiTableColumnFlags_WidthFixed,
106 280.0f);
107 ImGui::TableSetupColumn("Source Sheet", ImGuiTableColumnFlags_WidthStretch);
108
109 ImGui::TableNextRow();
110 ImGui::TableNextColumn();
111 DrawTileGrid();
112
113 ImGui::TableNextColumn();
115
116 ImGui::EndTable();
117 }
118
119 ImGui::Separator();
121 ImGui::Separator();
123
125
126 // Shared tile data confirmation modal
128 ImGui::OpenPopup("Shared Tile Data");
129 show_shared_confirm_ = false;
130 }
131 if (ImGui::BeginPopupModal("Shared Tile Data", nullptr,
132 ImGuiWindowFlags_AlwaysAutoResize)) {
133 ImGui::Text("This tile data is shared by %d objects.", shared_object_count_);
134 ImGui::Text("Changes will affect all of them.");
135 ImGui::Spacing();
136 if (ImGui::Button("Apply Anyway", ImVec2(120, 0))) {
137 ApplyChanges(/*confirm_shared=*/false);
138 ImGui::CloseCurrentPopup();
139 }
140 ImGui::SameLine();
141 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
142 ImGui::CloseCurrentPopup();
143 }
144 ImGui::EndPopup();
145 }
146
147 ImGui::End();
148}
149
151 if (!rooms_ || current_room_id_ < 0) return;
152 auto& room = (*rooms_)[current_room_id_];
153
154 auto status = tile_editor_->RenderLayoutToBitmap(
156 room.get_gfx_buffer().data(), current_palette_group_);
157 if (status.ok()) {
159 preview_dirty_ = false;
160 }
161}
162
164 if (!rooms_ || current_room_id_ < 0) return;
165 auto& room = (*rooms_)[current_room_id_];
166
167 auto status = tile_editor_->BuildTile8Atlas(
168 tile8_atlas_bmp_, room.get_gfx_buffer().data(),
170 if (status.ok()) {
172 atlas_dirty_ = false;
173 }
174}
175
177 if (preview_dirty_) {
179 }
180
181 ImGui::Text("Object Tiles (%dx%d)", current_layout_.bounds_width,
183
184 constexpr float kScale = 4.0f;
185 float grid_width = current_layout_.bounds_width * 8 * kScale;
186 float grid_height = current_layout_.bounds_height * 8 * kScale;
187
188 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
189 ImVec2 canvas_size(grid_width, grid_height);
190
191 // Draw the preview bitmap as background
193 ImGui::Image(
194 (ImTextureID)(intptr_t)object_preview_bmp_.texture(),
195 canvas_size);
196 } else {
197 ImGui::Dummy(canvas_size);
198 }
199
200 ImDrawList* draw_list = ImGui::GetWindowDrawList();
201
202 // Draw 8px grid overlay
203 for (int gx = 0; gx <= current_layout_.bounds_width; ++gx) {
204 float line_x = canvas_pos.x + gx * 8 * kScale;
205 draw_list->AddLine(ImVec2(line_x, canvas_pos.y),
206 ImVec2(line_x, canvas_pos.y + grid_height),
207 IM_COL32(128, 128, 128, 80));
208 }
209 for (int gy = 0; gy <= current_layout_.bounds_height; ++gy) {
210 float line_y = canvas_pos.y + gy * 8 * kScale;
211 draw_list->AddLine(ImVec2(canvas_pos.x, line_y),
212 ImVec2(canvas_pos.x + grid_width, line_y),
213 IM_COL32(128, 128, 128, 80));
214 }
215
216 // Highlight selected cell
217 if (selected_cell_index_ >= 0 &&
218 selected_cell_index_ < static_cast<int>(current_layout_.cells.size())) {
219 const auto& cell = current_layout_.cells[selected_cell_index_];
220 ImVec2 cell_min(canvas_pos.x + cell.rel_x * 8 * kScale,
221 canvas_pos.y + cell.rel_y * 8 * kScale);
222 ImVec2 cell_max(cell_min.x + 8 * kScale, cell_min.y + 8 * kScale);
223 draw_list->AddRect(cell_min, cell_max, IM_COL32(255, 255, 0, 255), 0, 0,
224 2.0f);
225 }
226
227 // Handle clicks on the grid
228 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
229 ImVec2 mouse = ImGui::GetMousePos();
230 int click_tile_x = static_cast<int>((mouse.x - canvas_pos.x) / (8 * kScale));
231 int click_tile_y = static_cast<int>((mouse.y - canvas_pos.y) / (8 * kScale));
232
233 // Find the cell at this position
234 for (int idx = 0; idx < static_cast<int>(current_layout_.cells.size());
235 ++idx) {
236 if (current_layout_.cells[idx].rel_x == click_tile_x &&
237 current_layout_.cells[idx].rel_y == click_tile_y) {
239 break;
240 }
241 }
242 }
243
244 // Show cell count
245 int modified_count = 0;
246 for (const auto& cell : current_layout_.cells) {
247 if (cell.modified) ++modified_count;
248 }
249 ImGui::Text("%zu tiles, %d modified", current_layout_.cells.size(),
250 modified_count);
251}
252
254 if (atlas_dirty_) {
256 }
257
258 ImGui::Text("Source Tiles (Palette %d)", source_palette_);
259
260 // Palette selector
261 ImGui::SameLine();
262 ImGui::SetNextItemWidth(80);
263 if (ImGui::SliderInt("##SrcPal", &source_palette_, 0, 7)) {
264 atlas_dirty_ = true;
265 }
266
267 constexpr float kAtlasScale = 2.0f;
268 float display_width = zelda3::ObjectTileEditor::kAtlasWidthPx * kAtlasScale;
269 float display_height =
271
272 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
273 ImVec2 atlas_size(display_width, display_height);
274
275 // Scrollable child for the atlas
276 ImGui::BeginChild("##AtlasScroll", ImVec2(display_width + 16, 300), true,
277 ImGuiWindowFlags_HorizontalScrollbar);
278
279 atlas_pos = ImGui::GetCursorScreenPos();
280
282 ImGui::Image(
283 (ImTextureID)(intptr_t)tile8_atlas_bmp_.texture(),
284 atlas_size);
285 } else {
286 ImGui::Dummy(atlas_size);
287 }
288
289 ImDrawList* draw_list = ImGui::GetWindowDrawList();
290
291 // Draw 8px grid
292 for (int gx = 0; gx <= zelda3::ObjectTileEditor::kAtlasTilesPerRow; ++gx) {
293 float line_x = atlas_pos.x + gx * 8 * kAtlasScale;
294 draw_list->AddLine(ImVec2(line_x, atlas_pos.y),
295 ImVec2(line_x, atlas_pos.y + display_height),
296 IM_COL32(64, 64, 64, 60));
297 }
298 for (int gy = 0; gy <= zelda3::ObjectTileEditor::kAtlasTileRows; ++gy) {
299 float line_y = atlas_pos.y + gy * 8 * kAtlasScale;
300 draw_list->AddLine(ImVec2(atlas_pos.x, line_y),
301 ImVec2(atlas_pos.x + display_width, line_y),
302 IM_COL32(64, 64, 64, 60));
303 }
304
305 // Highlight selected source tile
306 if (selected_source_tile_ >= 0) {
307 int src_col =
309 int src_row =
311 ImVec2 sel_min(atlas_pos.x + src_col * 8 * kAtlasScale,
312 atlas_pos.y + src_row * 8 * kAtlasScale);
313 ImVec2 sel_max(sel_min.x + 8 * kAtlasScale, sel_min.y + 8 * kAtlasScale);
314 draw_list->AddRect(sel_min, sel_max, IM_COL32(0, 255, 255, 255), 0, 0,
315 2.0f);
316 }
317
318 // Handle clicks on the atlas
319 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
320 ImVec2 mouse = ImGui::GetMousePos();
321 int click_col =
322 static_cast<int>((mouse.x - atlas_pos.x) / (8 * kAtlasScale));
323 int click_row =
324 static_cast<int>((mouse.y - atlas_pos.y) / (8 * kAtlasScale));
325
326 if (click_col >= 0 &&
328 click_row >= 0 &&
330 int tile_id = click_row * zelda3::ObjectTileEditor::kAtlasTilesPerRow +
331 click_col;
332 selected_source_tile_ = tile_id;
333
334 // If a cell is selected, replace its tile
335 if (selected_cell_index_ >= 0 &&
337 static_cast<int>(current_layout_.cells.size())) {
339 cell.tile_info.id_ = static_cast<uint16_t>(tile_id);
340 cell.tile_info.palette_ = static_cast<uint8_t>(source_palette_);
341 cell.modified = true;
342 preview_dirty_ = true;
343 }
344 }
345 }
346
347 ImGui::EndChild();
348
349 if (selected_source_tile_ >= 0) {
350 ImGui::Text("Tile: 0x%03X", selected_source_tile_);
351 }
352}
353
355 if (selected_cell_index_ < 0 ||
356 selected_cell_index_ >= static_cast<int>(current_layout_.cells.size())) {
357 ImGui::TextDisabled("Select a tile cell to edit properties");
358 return;
359 }
360
362 ImGui::Text("Cell (%d, %d)", cell.rel_x, cell.rel_y);
363 ImGui::SameLine();
364
365 // Tile ID
366 int tile_id = cell.tile_info.id_;
367 ImGui::SetNextItemWidth(80);
368 if (ImGui::InputInt("ID", &tile_id, 1, 16)) {
369 cell.tile_info.id_ = static_cast<uint16_t>(tile_id & 0x3FF);
370 cell.modified = true;
371 preview_dirty_ = true;
372 }
373 ImGui::SameLine();
374
375 // Palette
376 int pal = cell.tile_info.palette_;
377 ImGui::SetNextItemWidth(60);
378 if (ImGui::SliderInt("Pal", &pal, 0, 7)) {
379 cell.tile_info.palette_ = static_cast<uint8_t>(pal);
380 cell.modified = true;
381 preview_dirty_ = true;
382 }
383 ImGui::SameLine();
384
385 // Flip flags
386 if (ImGui::Checkbox("H", &cell.tile_info.horizontal_mirror_)) {
387 cell.modified = true;
388 preview_dirty_ = true;
389 }
390 ImGui::SameLine();
391 if (ImGui::Checkbox("V", &cell.tile_info.vertical_mirror_)) {
392 cell.modified = true;
393 preview_dirty_ = true;
394 }
395 ImGui::SameLine();
396 if (ImGui::Checkbox("Pri", &cell.tile_info.over_)) {
397 cell.modified = true;
398 preview_dirty_ = true;
399 }
400}
401
402void ObjectTileEditorPanel::ApplyChanges(bool confirm_shared) {
403 // Check for shared tile data and ask for confirmation
404 if (confirm_shared && current_layout_.tile_data_address >= 0 &&
406 int shared_count =
407 tile_editor_->CountObjectsSharingTileData(current_object_id_);
408 if (shared_count > 1) {
409 shared_object_count_ = shared_count;
411 return;
412 }
413 }
414
415 auto status = tile_editor_->WriteBack(current_layout_);
416 if (status.ok()) {
417 // Re-render room after applying changes
418 if (rooms_ && current_room_id_ >= 0 &&
419 current_room_id_ < static_cast<int>(rooms_->size())) {
420 auto& room = (*rooms_)[current_room_id_];
421 room.MarkObjectsDirty();
422 room.RenderRoomGraphics();
423 }
424 // Update original words to match current state
425 for (auto& cell : current_layout_.cells) {
426 if (cell.modified) {
427 cell.original_word = gfx::TileInfoToWord(cell.tile_info);
428 cell.modified = false;
429 }
430 }
431
432 // Fire creation callback on first save of a new object
436 is_new_object_ = false;
437 }
438 }
439}
440
442 int modified_count = 0;
443 for (const auto& cell : current_layout_.cells) {
444 if (cell.modified) ++modified_count;
445 }
446
447 bool has_mods = modified_count > 0;
448
449 if (has_mods) {
450 ImGui::Text("%d tile(s) modified", modified_count);
451 ImGui::SameLine();
452 }
453
454 // Shared tile data warning
456 int shared_count =
457 tile_editor_->CountObjectsSharingTileData(current_object_id_);
458 if (shared_count > 1) {
459 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f),
460 ICON_MD_WARNING " Shared by %d objects", shared_count);
461 ImGui::SameLine();
462 }
463 }
464
465 // Apply button
466 if (!has_mods) ImGui::BeginDisabled();
467 if (ImGui::Button(ICON_MD_SAVE " Apply")) {
468 ApplyChanges();
469 }
470 if (!has_mods) ImGui::EndDisabled();
471
472 ImGui::SameLine();
473
474 // Revert button
475 if (!has_mods) ImGui::BeginDisabled();
476 if (ImGui::Button(ICON_MD_UNDO " Revert")) {
478 preview_dirty_ = true;
479 }
480 if (!has_mods) ImGui::EndDisabled();
481
482 ImGui::SameLine();
483
484 if (ImGui::Button(ICON_MD_CLOSE " Close")) {
485 Close();
486 }
487}
488
490 // Only handle shortcuts when this window is focused
491 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) return;
492 if (current_layout_.cells.empty()) return;
493
494 int cell_count = static_cast<int>(current_layout_.cells.size());
495
496 // Arrow keys: navigate selected cell by spatial position
497 auto find_neighbor = [&](int dx, int dy) -> int {
498 if (selected_cell_index_ < 0) return 0;
499 const auto& cur = current_layout_.cells[selected_cell_index_];
500 int target_x = cur.rel_x + dx;
501 int target_y = cur.rel_y + dy;
502 for (int i = 0; i < cell_count; ++i) {
503 if (current_layout_.cells[i].rel_x == target_x &&
504 current_layout_.cells[i].rel_y == target_y) {
505 return i;
506 }
507 }
509 };
510
511 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false)) {
512 selected_cell_index_ = find_neighbor(-1, 0);
513 }
514 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow, false)) {
515 selected_cell_index_ = find_neighbor(1, 0);
516 }
517 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
518 selected_cell_index_ = find_neighbor(0, -1);
519 }
520 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
521 selected_cell_index_ = find_neighbor(0, 1);
522 }
523
524 // Number keys 0-7: set palette on selected cell
525 if (selected_cell_index_ >= 0 && selected_cell_index_ < cell_count) {
527 for (int key = 0; key <= 7; ++key) {
528 if (ImGui::IsKeyPressed(
529 static_cast<ImGuiKey>(ImGuiKey_0 + key), false)) {
530 cell.tile_info.palette_ = static_cast<uint8_t>(key);
531 cell.modified = true;
532 preview_dirty_ = true;
533 }
534 }
535
536 // H: toggle horizontal flip
537 if (ImGui::IsKeyPressed(ImGuiKey_H, false)) {
538 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
539 cell.modified = true;
540 preview_dirty_ = true;
541 }
542
543 // V: toggle vertical flip
544 if (ImGui::IsKeyPressed(ImGuiKey_V, false)) {
545 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
546 cell.modified = true;
547 preview_dirty_ = true;
548 }
549
550 // P: toggle priority
551 if (ImGui::IsKeyPressed(ImGuiKey_P, false)) {
552 cell.tile_info.over_ = !cell.tile_info.over_;
553 cell.modified = true;
554 preview_dirty_ = true;
555 }
556 }
557
558 // Escape: deselect or close
559 if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
560 if (selected_cell_index_ >= 0) {
562 } else {
563 Close();
564 }
565 }
566
567 // Tab: cycle to next cell
568 if (ImGui::IsKeyPressed(ImGuiKey_Tab, false)) {
569 if (cell_count > 0) {
570 selected_cell_index_ = (selected_cell_index_ + 1) % cell_count;
571 }
572 }
573}
574
575} // namespace editor
576} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
void OpenForNewObject(int width, int height, const std::string &filename, int16_t object_id, int room_id, std::array< zelda3::Room, 0x128 > *rooms)
void ApplyChanges(bool confirm_shared=true)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
ObjectTileEditorPanel(gfx::IRenderer *renderer, Rom *rom)
std::unique_ptr< zelda3::ObjectTileEditor > tile_editor_
std::function< void(int, const std::string &) on_object_created_)
std::array< zelda3::Room, 0x128 > * rooms_
void Draw(bool *p_open) override
Draw the panel content.
void OpenForObject(int16_t object_id, int room_id, std::array< zelda3::Room, 0x128 > *rooms)
const uint8_t * data() const
Definition bitmap.h:377
TextureHandle texture() const
Definition bitmap.h:380
bool is_active() const
Definition bitmap.h:384
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Definition bitmap.cc:297
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
static constexpr int kAtlasTilesPerRow
static constexpr int kAtlasTileRows
static constexpr int kAtlasHeightPx
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_ADD_BOX
Definition icons.h:90
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_UNDO
Definition icons.h:2039
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
std::string GetObjectName(int object_id)
Represents a group of palettes.
static ObjectTileLayout CreateEmpty(int width, int height, int16_t object_id, const std::string &filename)