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 : renderer_(renderer), rom_(rom) {
13 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
14}
15
16void ObjectTileEditorPanel::OpenForObject(int16_t object_id, int room_id,
17 DungeonRoomStore* rooms) {
18 current_object_id_ = object_id;
19 current_room_id_ = room_id;
20 rooms_ = rooms;
23 preview_dirty_ = true;
24 atlas_dirty_ = true;
25 is_open_ = true;
26
27 if (!rooms_ || current_room_id_ < 0 ||
28 current_room_id_ >= static_cast<int>(rooms_->size())) {
29 return;
30 }
31
32 auto& room = (*rooms_)[current_room_id_];
33 auto layout_or = tile_editor_->CaptureObjectLayout(object_id, room,
35 if (layout_or.ok()) {
36 current_layout_ = std::move(layout_or.value());
37 }
38}
39
40void ObjectTileEditorPanel::OpenForNewObject(int width, int height,
41 const std::string& filename,
42 int16_t object_id, int room_id,
43 DungeonRoomStore* rooms) {
44 current_object_id_ = object_id;
45 current_room_id_ = room_id;
46 rooms_ = rooms;
49 preview_dirty_ = true;
50 atlas_dirty_ = true;
51 is_open_ = true;
52 is_new_object_ = true;
53
55 zelda3::ObjectTileLayout::CreateEmpty(width, height, object_id, filename);
56}
57
65
70
71void ObjectTileEditorPanel::Draw(bool* p_open) {
72 if (!is_open_ || current_layout_.cells.empty())
73 return;
74
75 std::string title;
76 if (is_new_object_) {
77 title = absl::StrFormat(
78 ICON_MD_ADD_BOX " New Object (%dx%d) - %s###ObjTileEditor",
81 } else {
82 title = absl::StrFormat(
83 ICON_MD_GRID_ON " Object 0x%03X - %s###ObjTileEditor",
85 }
86
87 ImGui::SetNextWindowSize(ImVec2(550, 500), ImGuiCond_FirstUseEver);
88 if (!ImGui::Begin(title.c_str(), &is_open_)) {
89 ImGui::End();
90 return;
91 }
92
93 if (!is_open_) {
94 Close();
95 ImGui::End();
96 return;
97 }
98
99 // Two-column layout: tile grid + source sheet
100 if (ImGui::BeginTable(
101 "##TileEditorLayout", 2,
102 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
103 ImGui::TableSetupColumn("Tile Grid", ImGuiTableColumnFlags_WidthFixed,
104 280.0f);
105 ImGui::TableSetupColumn("Source Sheet", ImGuiTableColumnFlags_WidthStretch);
106
107 ImGui::TableNextRow();
108 ImGui::TableNextColumn();
109 DrawTileGrid();
110
111 ImGui::TableNextColumn();
113
114 ImGui::EndTable();
115 }
116
117 ImGui::Separator();
119 ImGui::Separator();
121
123
124 // Shared tile data confirmation modal
126 ImGui::OpenPopup("Shared Tile Data");
127 show_shared_confirm_ = false;
128 }
129 if (ImGui::BeginPopupModal("Shared Tile Data", nullptr,
130 ImGuiWindowFlags_AlwaysAutoResize)) {
131 ImGui::Text("This tile data is shared by %d objects.",
133 ImGui::Text("Changes will affect all of them.");
134 ImGui::Spacing();
135 if (ImGui::Button("Apply Anyway", ImVec2(120, 0))) {
136 ApplyChanges(/*confirm_shared=*/false);
137 ImGui::CloseCurrentPopup();
138 }
139 ImGui::SameLine();
140 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
141 ImGui::CloseCurrentPopup();
142 }
143 ImGui::EndPopup();
144 }
145
146 ImGui::End();
147}
148
150 if (!rooms_ || current_room_id_ < 0)
151 return;
152 auto& room = (*rooms_)[current_room_id_];
153
154 auto status = tile_editor_->RenderLayoutToBitmap(
155 current_layout_, object_preview_bmp_, room.get_gfx_buffer().data(),
157 if (status.ok()) {
159 preview_dirty_ = false;
160 }
161}
162
164 if (!rooms_ || current_room_id_ < 0)
165 return;
166 auto& room = (*rooms_)[current_room_id_];
167
168 auto status = tile_editor_->BuildTile8Atlas(
169 tile8_atlas_bmp_, room.get_gfx_buffer().data(), current_palette_group_,
171 if (status.ok()) {
173 atlas_dirty_ = false;
174 }
175}
176
178 if (preview_dirty_) {
180 }
181
182 ImGui::Text("Object Tiles (%dx%d)", current_layout_.bounds_width,
184
185 constexpr float kScale = 4.0f;
186 float grid_width = current_layout_.bounds_width * 8 * kScale;
187 float grid_height = current_layout_.bounds_height * 8 * kScale;
188
189 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
190 ImVec2 canvas_size(grid_width, grid_height);
191
192 // Draw the preview bitmap as background
194 ImGui::Image((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 =
231 static_cast<int>((mouse.x - canvas_pos.x) / (8 * kScale));
232 int click_tile_y =
233 static_cast<int>((mouse.y - canvas_pos.y) / (8 * kScale));
234
235 // Find the cell at this position
236 for (int idx = 0; idx < static_cast<int>(current_layout_.cells.size());
237 ++idx) {
238 if (current_layout_.cells[idx].rel_x == click_tile_x &&
239 current_layout_.cells[idx].rel_y == click_tile_y) {
241 break;
242 }
243 }
244 }
245
246 // Show cell count
247 int modified_count = 0;
248 for (const auto& cell : current_layout_.cells) {
249 if (cell.modified)
250 ++modified_count;
251 }
252 ImGui::Text("%zu tiles, %d modified", current_layout_.cells.size(),
253 modified_count);
254}
255
257 if (atlas_dirty_) {
259 }
260
261 ImGui::Text("Source Tiles (Palette %d)", source_palette_);
262
263 // Palette selector
264 ImGui::SameLine();
265 ImGui::SetNextItemWidth(80);
266 if (ImGui::SliderInt("##SrcPal", &source_palette_, 0, 7)) {
267 atlas_dirty_ = true;
268 }
269
270 constexpr float kAtlasScale = 2.0f;
271 float display_width = zelda3::ObjectTileEditor::kAtlasWidthPx * kAtlasScale;
272 float display_height = zelda3::ObjectTileEditor::kAtlasHeightPx * kAtlasScale;
273
274 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
275 ImVec2 atlas_size(display_width, display_height);
276
277 // Scrollable child for the atlas
278 ImGui::BeginChild("##AtlasScroll", ImVec2(display_width + 16, 300), true,
279 ImGuiWindowFlags_HorizontalScrollbar);
280
281 atlas_pos = ImGui::GetCursorScreenPos();
282
284 ImGui::Image((ImTextureID)(intptr_t)tile8_atlas_bmp_.texture(), 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 =
331 click_row * zelda3::ObjectTileEditor::kAtlasTilesPerRow + 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)
445 ++modified_count;
446 }
447
448 bool has_mods = modified_count > 0;
449
450 if (has_mods) {
451 ImGui::Text("%d tile(s) modified", modified_count);
452 ImGui::SameLine();
453 }
454
455 // Shared tile data warning
457 int shared_count =
458 tile_editor_->CountObjectsSharingTileData(current_object_id_);
459 if (shared_count > 1) {
460 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f),
461 ICON_MD_WARNING " Shared by %d objects", shared_count);
462 ImGui::SameLine();
463 }
464 }
465
466 // Apply button
467 if (!has_mods)
468 ImGui::BeginDisabled();
469 if (ImGui::Button(ICON_MD_SAVE " Apply")) {
470 ApplyChanges();
471 }
472 if (!has_mods)
473 ImGui::EndDisabled();
474
475 ImGui::SameLine();
476
477 // Revert button
478 if (!has_mods)
479 ImGui::BeginDisabled();
480 if (ImGui::Button(ICON_MD_UNDO " Revert")) {
482 preview_dirty_ = true;
483 }
484 if (!has_mods)
485 ImGui::EndDisabled();
486
487 ImGui::SameLine();
488
489 if (ImGui::Button(ICON_MD_CLOSE " Close")) {
490 Close();
491 }
492}
493
495 // Only handle shortcuts when this window is focused
496 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
497 return;
498 if (current_layout_.cells.empty())
499 return;
500
501 int cell_count = static_cast<int>(current_layout_.cells.size());
502
503 // Arrow keys: navigate selected cell by spatial position
504 auto find_neighbor = [&](int dx, int dy) -> int {
505 if (selected_cell_index_ < 0)
506 return 0;
507 const auto& cur = current_layout_.cells[selected_cell_index_];
508 int target_x = cur.rel_x + dx;
509 int target_y = cur.rel_y + dy;
510 for (int i = 0; i < cell_count; ++i) {
511 if (current_layout_.cells[i].rel_x == target_x &&
512 current_layout_.cells[i].rel_y == target_y) {
513 return i;
514 }
515 }
517 };
518
519 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false)) {
520 selected_cell_index_ = find_neighbor(-1, 0);
521 }
522 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow, false)) {
523 selected_cell_index_ = find_neighbor(1, 0);
524 }
525 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
526 selected_cell_index_ = find_neighbor(0, -1);
527 }
528 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
529 selected_cell_index_ = find_neighbor(0, 1);
530 }
531
532 // Number keys 0-7: set palette on selected cell
533 if (selected_cell_index_ >= 0 && selected_cell_index_ < cell_count) {
535 for (int key = 0; key <= 7; ++key) {
536 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_0 + key), false)) {
537 cell.tile_info.palette_ = static_cast<uint8_t>(key);
538 cell.modified = true;
539 preview_dirty_ = true;
540 }
541 }
542
543 // H: toggle horizontal flip
544 if (ImGui::IsKeyPressed(ImGuiKey_H, false)) {
545 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
546 cell.modified = true;
547 preview_dirty_ = true;
548 }
549
550 // V: toggle vertical flip
551 if (ImGui::IsKeyPressed(ImGuiKey_V, false)) {
552 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
553 cell.modified = true;
554 preview_dirty_ = true;
555 }
556
557 // P: toggle priority
558 if (ImGui::IsKeyPressed(ImGuiKey_P, false)) {
559 cell.tile_info.over_ = !cell.tile_info.over_;
560 cell.modified = true;
561 preview_dirty_ = true;
562 }
563 }
564
565 // Escape: deselect or close
566 if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
567 if (selected_cell_index_ >= 0) {
569 } else {
570 Close();
571 }
572 }
573
574 // Tab: cycle to next cell
575 if (ImGui::IsKeyPressed(ImGuiKey_Tab, false)) {
576 if (cell_count > 0) {
577 selected_cell_index_ = (selected_cell_index_ + 1) % cell_count;
578 }
579 }
580}
581
582} // namespace editor
583} // 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 ApplyChanges(bool confirm_shared=true)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
ObjectTileEditorPanel(gfx::IRenderer *renderer, Rom *rom)
void OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
std::unique_ptr< zelda3::ObjectTileEditor > tile_editor_
std::function< void(int, const std::string &) on_object_created_)
void Draw(bool *p_open) override
Draw the panel content.
void OpenForNewObject(int width, int height, const std::string &filename, int16_t object_id, int room_id, DungeonRoomStore *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)