yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_selector_content.cc
Go to the documentation of this file.
1// Related header
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <functional>
7#include <initializer_list>
8#include <memory>
9#include <string>
10#include <vector>
11#include "util/i18n/tr.h"
12
13// Third-party library headers
14#include "absl/strings/str_format.h"
17#include "imgui/imgui.h"
18
19// Project headers
24#include "app/gui/core/icons.h"
26#include "rom/rom.h"
31
32namespace yaze {
33namespace editor {
34
35namespace {
36
37struct UsageChip {
38 const char* icon = "";
39 std::string value;
40 ImVec4 color;
41};
42
43void DrawInlineInspectButton(const std::function<void()>& callback) {
44 if (!callback) {
45 return;
46 }
47
48 const char* label = ICON_MD_OPEN_IN_NEW " Inspect";
49 const ImGuiStyle& style = ImGui::GetStyle();
50 const float button_width =
51 ImGui::CalcTextSize(label).x + style.FramePadding.x * 2.0f;
52 const float next_x =
53 ImGui::GetItemRectMax().x + style.ItemSpacing.x + button_width;
54 const float line_right =
55 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
56 if (next_x <= line_right) {
57 ImGui::SameLine();
58 }
59 if (ImGui::SmallButton(label)) {
60 callback();
61 }
62}
63
64void DrawUsageChips(std::initializer_list<UsageChip> chips) {
65 if (chips.size() == 0) {
66 return;
67 }
68
69 const int columns = ImGui::GetContentRegionAvail().x < 330.0f ? 2 : 4;
70 constexpr ImGuiTableFlags kFlags =
71 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
72 if (!ImGui::BeginTable("##ObjectSelectorUsageChips", columns, kFlags)) {
73 return;
74 }
75
76 for (const UsageChip& chip : chips) {
77 ImGui::TableNextColumn();
78 ImGui::TextColored(chip.color, "%s %s", chip.icon, chip.value.c_str());
79 }
80 ImGui::EndTable();
81}
82
83} // namespace
84
86 Rom* rom, DungeonCanvasViewer* canvas_viewer,
87 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor,
88 ToastManager* toast_manager)
89 : rom_(rom),
90 canvas_viewer_(canvas_viewer),
91 object_selector_(rom),
92 object_editor_(object_editor),
93 toast_manager_(toast_manager) {
94 // Wire up object selector callback
96 [this](const zelda3::RoomObject& obj) {
97 preview_object_ = obj;
99 if (canvas_viewer_) {
102 }
103
104 // Sync with backend editor if available
105 if (object_editor_) {
107 object_editor_->SetCurrentObjectType(obj.id_);
108 }
109 });
110}
111
118
119void ObjectSelectorContent::Draw(bool* p_open) {
120 (void)p_open;
122
123 const int max_objects = static_cast<int>(zelda3::kMaxTileObjects);
124 const int max_sprites = static_cast<int>(zelda3::kMaxTotalSprites);
125 const int max_doors = static_cast<int>(zelda3::kMaxDoors);
126
127 // Check if placement was blocked by ROM limits
128 if (canvas_viewer_) {
129 auto& coordinator =
131
132 auto& tile_handler = coordinator.tile_handler();
133 if (tile_handler.was_placement_blocked()) {
134 const auto reason = tile_handler.placement_block_reason();
135 tile_handler.clear_placement_blocked();
136 switch (reason) {
138 SetPlacementError(absl::StrFormat(
139 "Object limit reached (%d max) - placement blocked",
140 max_objects));
141 break;
143 SetPlacementError("Invalid room target - placement blocked");
144 break;
146 default:
147 SetPlacementError("Object placement blocked");
148 break;
149 }
150 }
151
152 auto& sprite_handler = coordinator.sprite_handler();
153 if (sprite_handler.was_placement_blocked()) {
154 const auto reason = sprite_handler.placement_block_reason();
155 sprite_handler.clear_placement_blocked();
156 switch (reason) {
158 SetPlacementError(absl::StrFormat(
159 "Sprite limit reached (%d max) - placement blocked",
160 max_sprites));
161 break;
163 SetPlacementError("Invalid room target - sprite placement blocked");
164 break;
166 default:
167 SetPlacementError("Sprite placement blocked");
168 break;
169 }
170 }
171
172 auto& door_handler = coordinator.door_handler();
173 if (door_handler.was_placement_blocked()) {
174 const auto reason = door_handler.placement_block_reason();
175 door_handler.clear_placement_blocked();
176 switch (reason) {
178 SetPlacementError(absl::StrFormat(
179 "Door limit reached (%d max) - placement blocked", max_doors));
180 break;
182 SetPlacementError("Invalid door position - must be near a wall");
183 break;
185 SetPlacementError("Invalid room target - door placement blocked");
186 break;
188 default:
189 SetPlacementError("Door placement blocked");
190 break;
191 }
192 }
193 }
194
195 float available_height = ImGui::GetContentRegionAvail().y;
196 float browser_height = std::max(240.0f, available_height);
197
199 ImGui::Spacing();
200 ImGui::BeginChild("ObjectBrowserRegion", ImVec2(0, browser_height), false);
202 ImGui::EndChild();
203}
204
208
210 // In agent mode, we might force tabs open or change layout
211 (void)enabled;
212}
213
214void ObjectSelectorContent::SetPlacementError(const std::string& message) {
215 // Avoid refreshing the timer for repeated identical errors; keeps the
216 // message stable during rapid blocked clicks.
217 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
218 double elapsed = ImGui::GetTime() - placement_error_time_;
219 if (elapsed < kPlacementErrorDuration) {
220 return;
221 }
222 }
223 last_placement_error_ = message;
224 placement_error_time_ = ImGui::GetTime();
225 if (toast_manager_) {
226 toast_manager_->Show(message, ToastType::kError, 4.0f);
227 }
228}
229
231 // Delegate to the DungeonObjectSelector component
233}
234
236 const auto& theme = AgentUI::GetTheme();
237 auto* viewer = ResolveCanvasViewer();
238 const auto snapshot = viewer != nullptr
240 viewer->object_interaction(), viewer->rooms(),
241 viewer->current_room_id())
243
244 ImGui::AlignTextToFramePadding();
245 ImGui::TextColored(theme.text_info, ICON_MD_CATEGORY " Object Selector");
246
247 if (!last_placement_error_.empty()) {
248 double elapsed = ImGui::GetTime() - placement_error_time_;
249 if (!toast_manager_ && elapsed < kPlacementErrorDuration) {
250 ImGui::SameLine();
251 ImGui::TextColored(theme.status_error, ICON_MD_WARNING " %s",
252 last_placement_error_.c_str());
253 } else if (elapsed >= kPlacementErrorDuration) {
254 last_placement_error_.clear();
255 }
256 }
257
258 ImGui::Separator();
259
260 bool is_placing = has_preview_object_ && canvas_viewer_ &&
262 if (!is_placing && has_preview_object_) {
263 has_preview_object_ = false;
264 }
265
266 if (is_placing) {
267 ImGui::TextColored(theme.status_warning,
268 ICON_MD_ADD_CIRCLE " Queued 0x%03X %s",
271 const char* cancel_label = ICON_MD_CANCEL " Cancel";
272 const float cancel_width = ImGui::CalcTextSize(cancel_label).x +
273 ImGui::GetStyle().FramePadding.x * 2.0f;
274 const float next_x = ImGui::GetItemRectMax().x +
275 ImGui::GetStyle().ItemSpacing.x + cancel_width;
276 const float line_right =
277 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
278 if (next_x <= line_right) {
279 ImGui::SameLine();
280 }
281 if (ImGui::SmallButton(cancel_label)) {
283 }
284 } else if (snapshot.kind == DungeonSelectionKind::ObjectSingle) {
285 ImGui::TextColored(theme.status_success,
286 ICON_MD_CHECK_CIRCLE " 1 object selected");
287 DrawInlineInspectButton(open_object_editor_callback_);
288 } else if (snapshot.kind == DungeonSelectionKind::ObjectMulti) {
289 ImGui::TextColored(theme.status_success,
290 ICON_MD_SELECT_ALL " %zu objects selected",
291 snapshot.count);
292 DrawInlineInspectButton(open_object_editor_callback_);
293 } else if (snapshot.kind == DungeonSelectionKind::Door ||
294 snapshot.kind == DungeonSelectionKind::Sprite ||
295 snapshot.kind == DungeonSelectionKind::Item) {
296 ImGui::TextColored(theme.status_success,
297 ICON_MD_MANAGE_SEARCH " 1 %s selected",
298 GetDungeonSelectionKindLabel(snapshot.kind));
299 DrawInlineInspectButton(open_object_editor_callback_);
300 } else if (snapshot.kind == DungeonSelectionKind::EntityMulti ||
301 snapshot.kind == DungeonSelectionKind::Mixed) {
302 ImGui::TextColored(theme.status_success, ICON_MD_SELECT_ALL " %s",
303 GetDungeonSelectionSummaryText(snapshot).c_str());
304 DrawInlineInspectButton(open_object_editor_callback_);
305 } else {
306 ImGui::TextColored(
307 theme.text_secondary_gray, ICON_MD_MOUSE
308 " Browse, filter, and click an object below to queue placement.");
309 }
310
311 auto* rooms = object_selector_.get_rooms();
312 if (rooms && current_room_id_ >= 0 &&
314 const auto& room = (*rooms)[current_room_id_];
315 size_t object_count = room.GetTileObjects().size();
316 size_t sprite_count = room.GetSprites().size();
317 size_t door_count = room.GetDoors().size();
318 int chest_count = 0;
319 for (const auto& obj : room.GetTileObjects()) {
320 if (obj.id_ >= 0xF9 && obj.id_ <= 0xFD) {
321 chest_count++;
322 }
323 }
324
325 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
326 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
327 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
328 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
329
330 auto usage_color = [&](size_t count, int max_val,
331 bool exact_capacity_warning) -> ImVec4 {
332 if (exact_capacity_warning) {
333 return GetPlacementSummaryColor(theme, count, max_val,
334 theme.text_secondary_gray);
335 }
336 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
337 if (ratio >= 1.0f) {
338 return theme.status_error;
339 }
340 if (ratio >= 0.75f) {
341 return theme.status_warning;
342 }
343 return theme.text_secondary_gray;
344 };
345
346 zelda3::DungeonValidator validator;
347 auto result = validator.ValidateRoom(room);
348
349 ImGui::Spacing();
350 DrawUsageChips(
351 {{ICON_MD_WIDGETS, absl::StrFormat("%zu/%d", object_count, kMaxObjects),
352 usage_color(object_count, kMaxObjects, true)},
354 absl::StrFormat("%zu/%d", sprite_count, kMaxSprites),
355 usage_color(sprite_count, kMaxSprites, true)},
356 {ICON_MD_DOOR_FRONT, absl::StrFormat("%zu/%d", door_count, kMaxDoors),
357 usage_color(door_count, kMaxDoors, true)},
359 absl::StrFormat("%d/%d", chest_count, kMaxChests),
360 usage_color(chest_count, kMaxChests, false)}});
361
362 if (!result.errors.empty() || !result.warnings.empty()) {
363 ImGui::TextColored(
364 result.errors.empty() ? theme.status_warning : theme.status_error,
365 tr("%s %zu issue%s"),
366 result.errors.empty() ? ICON_MD_WARNING : ICON_MD_ERROR,
367 result.errors.size() + result.warnings.size(),
368 (result.errors.size() + result.warnings.size()) == 1 ? "" : "s");
369 if (ImGui::IsItemHovered()) {
370 ImGui::BeginTooltip();
371 for (const auto& err : result.errors) {
372 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
373 err.c_str());
374 }
375 for (const auto& warn : result.warnings) {
376 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
377 warn.c_str());
378 }
379 ImGui::EndTooltip();
380 }
381 }
382 } else {
383 ImGui::Spacing();
384 ImGui::TextColored(theme.text_secondary_gray,
385 ICON_MD_INFO " Room data unavailable");
386 }
387}
388
396
397} // namespace editor
398} // 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
DungeonObjectInteraction & object_interaction()
void SetPreviewObject(const zelda3::RoomObject &object)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SelectObject(int obj_id, int subtype=-1)
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
void SetPlacementError(const std::string &message)
std::function< DungeonCanvasViewer *()> canvas_viewer_provider_
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
ObjectSelectorContent(Rom *rom, DungeonCanvasViewer *canvas_viewer, std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr, ToastManager *toast_manager=nullptr)
void Draw(bool *p_open) override
Draw the panel content.
PlacementBlockReason placement_block_reason() const
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
ValidationResult ValidateRoom(const Room &room)
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_MANAGE_SEARCH
Definition icons.h:1172
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_MOUSE
Definition icons.h:1251
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
const AgentUITheme & GetTheme()
void DrawInlineInspectButton(const std::function< void()> &callback)
void DrawUsageChips(std::initializer_list< UsageChip > chips)
ImVec4 GetPlacementSummaryColor(const AgentUITheme &theme, size_t current_count, size_t max_count, const ImVec4 &normal_color)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
const char * GetDungeonSelectionKindLabel(DungeonSelectionKind kind)
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
constexpr size_t kMaxTileObjects
constexpr size_t kMaxDoors
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites