yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_matrix_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_ROOM_MATRIX_PANEL_H_
2#define YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_ROOM_MATRIX_PANEL_H_
3
4#include <array>
5#include <cctype>
6#include <cmath>
7#include <cstdint>
8#include <functional>
9#include <optional>
10#include <string>
11#include <unordered_map>
12
16#include "app/gui/core/icons.h"
17#include "imgui/imgui.h"
18#include "zelda3/dungeon/room.h"
21
22namespace yaze {
23namespace editor {
24
40 public:
48 DungeonRoomMatrixPanel(int* current_room_id, ImVector<int>* active_rooms,
49 std::function<void(int)> on_room_selected,
50 std::function<void(int, int)> on_room_swap = nullptr,
51 std::array<zelda3::Room, 0x128>* rooms = nullptr)
52 : current_room_id_(current_room_id),
53 active_rooms_(active_rooms),
54 rooms_(rooms),
55 on_room_selected_(std::move(on_room_selected)),
56 on_room_swap_(std::move(on_room_swap)) {}
57
58 // ==========================================================================
59 // EditorPanel Identity
60 // ==========================================================================
61
62 std::string GetId() const override { return "dungeon.room_matrix"; }
63 std::string GetDisplayName() const override { return "Room Matrix"; }
64 std::string GetIcon() const override { return ICON_MD_GRID_VIEW; }
65 std::string GetEditorCategory() const override { return "Dungeon"; }
66 int GetPriority() const override { return 30; }
67
69 std::function<void(int, RoomSelectionIntent)> callback) {
70 on_room_intent_ = std::move(callback);
71 }
72
73 // ==========================================================================
74 // EditorPanel Drawing
75 // ==========================================================================
76
77 void Draw(bool* p_open) override {
79 return;
80
81 const auto& theme = AgentUI::GetTheme();
82
83 // 16 wide x 19 tall = 304 cells (296 rooms + 8 empty)
84 constexpr int kRoomsPerRow = 16;
85 constexpr int kRoomsPerCol = 19;
86 constexpr int kTotalRooms = 0x128; // 296 rooms (0x00-0x127)
87 constexpr float kCellSpacing = 1.0f;
88
89 // Responsive cell size based on available panel width
90 float panel_width = ImGui::GetContentRegionAvail().x;
91 // Calculate cell size to fit 16 cells with spacing in available width
92 float cell_size = std::max(
93 12.0f,
94 std::min(24.0f, (panel_width - kCellSpacing * (kRoomsPerRow - 1)) /
95 kRoomsPerRow));
96
97 ImDrawList* draw_list = ImGui::GetWindowDrawList();
98 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
99
100 int room_index = 0;
101 for (int row = 0; row < kRoomsPerCol; row++) {
102 for (int col = 0; col < kRoomsPerRow; col++) {
103 int room_id = room_index;
104 bool is_valid_room = (room_id < kTotalRooms);
105
106 ImVec2 cell_min =
107 ImVec2(canvas_pos.x + col * (cell_size + kCellSpacing),
108 canvas_pos.y + row * (cell_size + kCellSpacing));
109 ImVec2 cell_max =
110 ImVec2(cell_min.x + cell_size, cell_min.y + cell_size);
111
112 if (is_valid_room) {
113 // Get color based on room palette if available, else use algorithmic
114 ImU32 bg_color = GetRoomColor(room_id, theme);
115
116 bool is_current = (*current_room_id_ == room_id);
117 bool is_open = false;
118 for (int i = 0; i < active_rooms_->Size; i++) {
119 if ((*active_rooms_)[i] == room_id) {
120 is_open = true;
121 break;
122 }
123 }
124
125 // Draw cell background
126 draw_list->AddRectFilled(cell_min, cell_max, bg_color);
127
128 // Draw outline based on state using theme colors
129 if (is_current) {
130 // Add glow effect for current room (outer glow layers)
131 ImVec4 glow_color = theme.dungeon_selection_primary;
132 glow_color.w = 0.3f; // 30% opacity outer glow
133 ImVec2 glow_min(cell_min.x - 2, cell_min.y - 2);
134 ImVec2 glow_max(cell_max.x + 2, cell_max.y + 2);
135 draw_list->AddRect(glow_min, glow_max,
136 ImGui::ColorConvertFloat4ToU32(glow_color), 0.0f,
137 0, 3.0f);
138
139 // Inner bright border
140 ImU32 sel_color =
141 ImGui::ColorConvertFloat4ToU32(theme.dungeon_selection_primary);
142 draw_list->AddRect(cell_min, cell_max, sel_color, 0.0f, 0, 2.5f);
143 } else if (is_open) {
144 ImU32 open_color = ImGui::ColorConvertFloat4ToU32(
145 theme.dungeon_grid_cell_selected);
146 draw_list->AddRect(cell_min, cell_max, open_color, 0.0f, 0, 2.0f);
147 } else {
148 ImU32 border_color =
149 ImGui::ColorConvertFloat4ToU32(theme.dungeon_grid_cell_border);
150 draw_list->AddRect(cell_min, cell_max, border_color, 0.0f, 0, 1.0f);
151 }
152
153 // Draw room ID (only if cell is large enough)
154 if (cell_size >= 18.0f) {
155 char label[8];
156 snprintf(label, sizeof(label), "%02X", room_id);
157 ImVec2 text_size = ImGui::CalcTextSize(label);
158 ImVec2 text_pos =
159 ImVec2(cell_min.x + (cell_size - text_size.x) * 0.5f,
160 cell_min.y + (cell_size - text_size.y) * 0.5f);
161 ImU32 text_color =
162 ImGui::ColorConvertFloat4ToU32(theme.dungeon_grid_text);
163 draw_list->AddText(text_pos, text_color, label);
164 }
165
166 // Handle clicks
167 ImGui::SetCursorScreenPos(cell_min);
168 char btn_id[32];
169 snprintf(btn_id, sizeof(btn_id), "##room%d", room_id);
170 ImGui::InvisibleButton(btn_id, ImVec2(cell_size, cell_size));
171
172 if (ImGui::IsItemClicked()) {
173 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
174 // Double-click: open as standalone panel
175 if (on_room_intent_) {
177 } else if (on_room_selected_) {
178 on_room_selected_(room_id);
179 }
180 } else if (on_room_selected_) {
181 on_room_selected_(room_id);
182 }
183 }
184
185 if (ImGui::BeginPopupContextItem()) {
186 const bool can_swap =
188 *current_room_id_ < kTotalRooms && *current_room_id_ != room_id;
189
190 std::string open_label =
191 is_open ? "Focus Room" : "Open in Workbench";
192 if (ImGui::MenuItem(open_label.c_str())) {
193 if (on_room_intent_) {
194 on_room_intent_(room_id,
196 } else if (on_room_selected_) {
197 on_room_selected_(room_id);
198 }
199 }
200
201 if (ImGui::MenuItem("Open as Panel")) {
202 if (on_room_intent_) {
204 } else if (on_room_selected_) {
205 on_room_selected_(room_id);
206 }
207 }
208
209 if (ImGui::MenuItem("Swap With Current Room", nullptr, false,
210 can_swap)) {
212 }
213
214 ImGui::Separator();
215
216 char id_buf[16];
217 snprintf(id_buf, sizeof(id_buf), "0x%02X", room_id);
218 if (ImGui::MenuItem("Copy Room ID")) {
219 ImGui::SetClipboardText(id_buf);
220 }
221
222 const std::string& room_label = zelda3::GetRoomLabel(room_id);
223 if (ImGui::MenuItem("Copy Room Name")) {
224 ImGui::SetClipboardText(room_label.c_str());
225 }
226
227 ImGui::EndPopup();
228 }
229
230 // Tooltip with room info and thumbnail preview
231 if (ImGui::IsItemHovered()) {
232 ImGui::BeginTooltip();
233 // Use unified ResourceLabelProvider for room names
234 ImGui::Text("%s", zelda3::GetRoomLabel(room_id).c_str());
235
236 if (rooms_ && (*rooms_)[room_id].IsLoaded()) {
237 // Show palette info
238 ImGui::TextDisabled("Palette: %d | Blockset: %d",
239 (*rooms_)[room_id].palette(),
240 (*rooms_)[room_id].blockset());
241
242 // Show thumbnail preview of the room
243 auto& room = (*rooms_)[room_id];
244 zelda3::RoomLayerManager layer_mgr;
245 layer_mgr.ApplyLayerMerging(room.layer_merging());
246 auto& preview_bitmap = room.GetCompositeBitmap(layer_mgr);
247 if (preview_bitmap.is_active() && preview_bitmap.texture() != 0) {
248 ImGui::Separator();
249 // Render at thumbnail size (80x80 from 512x512)
250 constexpr float kThumbnailSize = 80.0f;
251 ImGui::Image((ImTextureID)(intptr_t)preview_bitmap.texture(),
252 ImVec2(kThumbnailSize, kThumbnailSize));
253 }
254 }
255
256 ImGui::TextDisabled("Click to %s", is_open ? "focus" : "open");
257 ImGui::EndTooltip();
258 }
259 } else {
260 // Empty cell
261 draw_list->AddRectFilled(
262 cell_min, cell_max,
263 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker));
264 }
265
266 room_index++;
267 }
268 }
269
270 // Advance cursor past the grid
271 ImGui::Dummy(ImVec2(kRoomsPerRow * (cell_size + kCellSpacing),
272 kRoomsPerCol * (cell_size + kCellSpacing)));
273 }
274
275 void SetRooms(std::array<zelda3::Room, 0x128>* rooms) { rooms_ = rooms; }
276
277 private:
281 ImU32 GetRoomColor(int room_id, const AgentUITheme& theme) {
282 auto sample_dominant_color =
283 [](gfx::Bitmap& bitmap) -> std::optional<ImU32> {
284 if (!bitmap.is_active() || bitmap.width() <= 0 || bitmap.height() <= 0 ||
285 bitmap.data() == nullptr || bitmap.surface() == nullptr ||
286 bitmap.surface()->format == nullptr ||
287 bitmap.surface()->format->palette == nullptr) {
288 return std::nullopt;
289 }
290
291 constexpr int kSampleStep = 16;
292 std::array<uint32_t, 256> histogram{};
293 SDL_Palette* palette = bitmap.surface()->format->palette;
294 const uint8_t* pixels = bitmap.data();
295 const int width = bitmap.width();
296 const int height = bitmap.height();
297
298 for (int y = 0; y < height; y += kSampleStep) {
299 for (int x = 0; x < width; x += kSampleStep) {
300 const uint8_t idx = pixels[(y * width) + x];
301 if (idx == 255) {
302 continue;
303 }
304 histogram[idx]++;
305 }
306 }
307
308 uint32_t best_count = 0;
309 uint8_t best_index = 0;
310 for (int i = 0; i < palette->ncolors && i < 256; ++i) {
311 if (histogram[static_cast<size_t>(i)] > best_count) {
312 best_count = histogram[static_cast<size_t>(i)];
313 best_index = static_cast<uint8_t>(i);
314 }
315 }
316
317 if (best_count == 0) {
318 return std::nullopt;
319 }
320 const SDL_Color& c = palette->colors[best_index];
321 return IM_COL32(c.r, c.g, c.b, 255);
322 };
323
324 auto soften_color = [&](ImU32 color, float blend = 0.32f) -> ImU32 {
325 ImVec4 src = ImGui::ColorConvertU32ToFloat4(color);
326 const ImVec4 bg = theme.panel_bg_darker;
327 src.x = (src.x * (1.0f - blend)) + (bg.x * blend);
328 src.y = (src.y * (1.0f - blend)) + (bg.y * blend);
329 src.z = (src.z * (1.0f - blend)) + (bg.z * blend);
330 src.w = 1.0f;
331 return ImGui::ColorConvertFloat4ToU32(src);
332 };
333
334 // If room data is available and loaded, sample the actual room bitmap and
335 // choose its most frequent indexed color.
336 if (rooms_ && (*rooms_)[room_id].IsLoaded()) {
337 auto& room = (*rooms_)[room_id];
338 zelda3::RoomLayerManager layer_mgr;
339 layer_mgr.ApplyLayerMerging(room.layer_merging());
340 auto& composite = room.GetCompositeBitmap(layer_mgr);
341 if (auto composite_color = sample_dominant_color(composite);
342 composite_color.has_value()) {
343 return soften_color(composite_color.value());
344 }
345
346 if (auto bg1_color = sample_dominant_color(room.bg1_buffer().bitmap());
347 bg1_color.has_value()) {
348 return soften_color(bg1_color.value());
349 }
350 }
351
352 // Fallback: neutral deterministic color buckets (no rainbow hue wheel).
353 const auto clamp01 = [](float v) {
354 return (v < 0.0f) ? 0.0f : (v > 1.0f ? 1.0f : v);
355 };
356
357 const ImVec4 dark = theme.panel_bg_darker;
358 const ImVec4 mid = theme.panel_bg_color;
359 const float group_mix =
360 0.16f + (static_cast<float>((room_id >> 4) & 0x03) * 0.08f);
361 const float step = static_cast<float>(room_id & 0x07) * 0.0125f;
362
363 ImVec4 fallback;
364 fallback.x = clamp01(dark.x + (mid.x - dark.x) * group_mix + step);
365 fallback.y = clamp01(dark.y + (mid.y - dark.y) * group_mix + step);
366 fallback.z = clamp01(dark.z + (mid.z - dark.z) * group_mix + step);
367 fallback.w = 1.0f;
368 return ImGui::ColorConvertFloat4ToU32(fallback);
369 }
370
371 int* current_room_id_ = nullptr;
372 ImVector<int>* active_rooms_ = nullptr;
373 std::array<zelda3::Room, 0x128>* rooms_ = nullptr;
374 std::function<void(int)> on_room_selected_;
375 std::function<void(int, int)> on_room_swap_;
376 std::function<void(int, RoomSelectionIntent)> on_room_intent_;
377};
378
379} // namespace editor
380} // namespace yaze
381
382#endif // YAZE_APP_EDITOR_DUNGEON_PANELS_DUNGEON_ROOM_MATRIX_PANEL_H_
EditorPanel for displaying a visual 16x19 grid of all dungeon rooms.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
int GetPriority() const override
Get display priority for menu ordering.
std::function< void(int, RoomSelectionIntent)> on_room_intent_
ImU32 GetRoomColor(int room_id, const AgentUITheme &theme)
Get color for a room from dominant preview color, with fallback.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
DungeonRoomMatrixPanel(int *current_room_id, ImVector< int > *active_rooms, std::function< void(int)> on_room_selected, std::function< void(int, int)> on_room_swap=nullptr, std::array< zelda3::Room, 0x128 > *rooms=nullptr)
Construct a room matrix panel.
std::string GetId() const override
Unique identifier for this panel.
std::array< zelda3::Room, 0x128 > * rooms_
void SetRoomIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
void Draw(bool *p_open) override
Draw the panel content.
void SetRooms(std::array< zelda3::Room, 0x128 > *rooms)
std::string GetIcon() const override
Material Design icon for this panel.
std::function< void(int, int)> on_room_swap_
Base interface for all logical panel components.
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
RoomLayerManager - Manages layer visibility and compositing.
void ApplyLayerMerging(const LayerMergeType &merge_type)
#define ICON_MD_GRID_VIEW
Definition icons.h:897
const AgentUITheme & GetTheme()
RoomSelectionIntent
Intent for room selection in the dungeon editor.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19