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"
32
33namespace yaze {
34namespace editor {
35
36namespace {
37
38struct UsageChip {
39 const char* icon = "";
40 std::string value;
41 ImVec4 color;
42};
43
44void DrawInlineInspectButton(const std::function<void()>& callback) {
45 if (!callback) {
46 return;
47 }
48
49 const char* label = ICON_MD_OPEN_IN_NEW " Inspect";
50 const ImGuiStyle& style = ImGui::GetStyle();
51 const float button_width =
52 ImGui::CalcTextSize(label).x + style.FramePadding.x * 2.0f;
53 const float next_x =
54 ImGui::GetItemRectMax().x + style.ItemSpacing.x + button_width;
55 const float line_right =
56 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
57 if (next_x <= line_right) {
58 ImGui::SameLine();
59 }
60 if (ImGui::SmallButton(label)) {
61 callback();
62 }
63}
64
65void DrawUsageChips(std::initializer_list<UsageChip> chips) {
66 if (chips.size() == 0) {
67 return;
68 }
69
70 const int columns = ImGui::GetContentRegionAvail().x < 330.0f ? 2 : 4;
71 constexpr ImGuiTableFlags kFlags =
72 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
73 if (!ImGui::BeginTable("##ObjectSelectorUsageChips", columns, kFlags)) {
74 return;
75 }
76
77 for (const UsageChip& chip : chips) {
78 ImGui::TableNextColumn();
79 ImGui::TextColored(chip.color, "%s %s", chip.icon, chip.value.c_str());
80 }
81 ImGui::EndTable();
82}
83
84} // namespace
85
87 Rom* rom, DungeonCanvasViewer* canvas_viewer,
88 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor,
89 ToastManager* toast_manager)
90 : rom_(rom),
91 canvas_viewer_(canvas_viewer),
92 object_selector_(rom),
93 object_editor_(object_editor),
94 toast_manager_(toast_manager) {
95 // Wire up object selector callback
97 [this](const zelda3::RoomObject& obj) {
98 preview_object_ = obj;
100 if (canvas_viewer_) {
103 }
104
105 // Sync with backend editor if available
106 if (object_editor_) {
108 object_editor_->SetCurrentObjectType(obj.id_);
109 }
110 });
111}
112
119
120void ObjectSelectorContent::Draw(bool* p_open) {
121 (void)p_open;
123
124 const int max_objects = static_cast<int>(zelda3::kMaxTileObjects);
125 const int max_sprites = static_cast<int>(zelda3::kMaxTotalSprites);
126 const int max_doors = static_cast<int>(zelda3::kMaxDoors);
127
128 // Check if placement was blocked by ROM limits
129 if (canvas_viewer_) {
130 auto& coordinator =
132
133 auto& tile_handler = coordinator.tile_handler();
134 if (tile_handler.was_placement_blocked()) {
135 const auto reason = tile_handler.placement_block_reason();
136 tile_handler.clear_placement_blocked();
137 switch (reason) {
139 SetPlacementError(absl::StrFormat(
140 "Object limit reached (%d max) - placement blocked",
141 max_objects));
142 break;
144 SetPlacementError("Invalid room target - placement blocked");
145 break;
147 default:
148 SetPlacementError("Object placement blocked");
149 break;
150 }
151 }
152
153 auto& sprite_handler = coordinator.sprite_handler();
154 if (sprite_handler.was_placement_blocked()) {
155 const auto reason = sprite_handler.placement_block_reason();
156 sprite_handler.clear_placement_blocked();
157 switch (reason) {
159 SetPlacementError(absl::StrFormat(
160 "Sprite limit reached (%d max) - placement blocked",
161 max_sprites));
162 break;
164 SetPlacementError("Invalid room target - sprite placement blocked");
165 break;
167 default:
168 SetPlacementError("Sprite placement blocked");
169 break;
170 }
171 }
172
173 auto& door_handler = coordinator.door_handler();
174 if (door_handler.was_placement_blocked()) {
175 const auto reason = door_handler.placement_block_reason();
176 door_handler.clear_placement_blocked();
177 switch (reason) {
179 SetPlacementError(absl::StrFormat(
180 "Door limit reached (%d max) - placement blocked", max_doors));
181 break;
183 SetPlacementError("Invalid door position - must be near a wall");
184 break;
186 SetPlacementError("Invalid room target - door placement blocked");
187 break;
189 default:
190 SetPlacementError("Door placement blocked");
191 break;
192 }
193 }
194 }
195
196 float available_height = ImGui::GetContentRegionAvail().y;
197 float browser_height = std::max(240.0f, available_height);
198
200 ImGui::Spacing();
201 ImGui::BeginChild("ObjectBrowserRegion", ImVec2(0, browser_height), false);
203 ImGui::EndChild();
204}
205
209
211 // In agent mode, we might force tabs open or change layout
212 (void)enabled;
213}
214
215void ObjectSelectorContent::SetPlacementError(const std::string& message) {
216 // Avoid refreshing the timer for repeated identical errors; keeps the
217 // message stable during rapid blocked clicks.
218 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
219 double elapsed = ImGui::GetTime() - placement_error_time_;
220 if (elapsed < kPlacementErrorDuration) {
221 return;
222 }
223 }
224 last_placement_error_ = message;
225 placement_error_time_ = ImGui::GetTime();
226 if (toast_manager_) {
227 toast_manager_->Show(message, ToastType::kError, 4.0f);
228 }
229}
230
232 // Delegate to the DungeonObjectSelector component
234}
235
237 const auto& theme = AgentUI::GetTheme();
238 auto* viewer = ResolveCanvasViewer();
239 const auto snapshot = viewer != nullptr
241 viewer->object_interaction(), viewer->rooms(),
242 viewer->current_room_id())
244
245 ImGui::AlignTextToFramePadding();
246 ImGui::TextColored(theme.text_info, ICON_MD_CATEGORY " Object Selector");
247
248 if (!last_placement_error_.empty()) {
249 double elapsed = ImGui::GetTime() - placement_error_time_;
250 if (!toast_manager_ && elapsed < kPlacementErrorDuration) {
251 ImGui::SameLine();
252 ImGui::TextColored(theme.status_error, ICON_MD_WARNING " %s",
253 last_placement_error_.c_str());
254 } else if (elapsed >= kPlacementErrorDuration) {
255 last_placement_error_.clear();
256 }
257 }
258
259 ImGui::Separator();
260
261 bool is_placing = has_preview_object_ && canvas_viewer_ &&
263 if (!is_placing && has_preview_object_) {
264 has_preview_object_ = false;
265 }
266
267 if (is_placing) {
268 ImGui::TextColored(theme.status_warning,
269 ICON_MD_ADD_CIRCLE " Queued 0x%03X %s",
272 const char* cancel_label = ICON_MD_CANCEL " Cancel";
273 const float cancel_width = ImGui::CalcTextSize(cancel_label).x +
274 ImGui::GetStyle().FramePadding.x * 2.0f;
275 const float next_x = ImGui::GetItemRectMax().x +
276 ImGui::GetStyle().ItemSpacing.x + cancel_width;
277 const float line_right =
278 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
279 if (next_x <= line_right) {
280 ImGui::SameLine();
281 }
282 if (ImGui::SmallButton(cancel_label)) {
284 }
285 } else if (snapshot.kind == DungeonSelectionKind::ObjectSingle) {
286 ImGui::TextColored(theme.status_success,
287 ICON_MD_CHECK_CIRCLE " 1 object selected");
288 DrawInlineInspectButton(open_object_editor_callback_);
289 } else if (snapshot.kind == DungeonSelectionKind::ObjectMulti) {
290 ImGui::TextColored(theme.status_success,
291 ICON_MD_SELECT_ALL " %zu objects selected",
292 snapshot.count);
293 DrawInlineInspectButton(open_object_editor_callback_);
294 } else if (snapshot.kind == DungeonSelectionKind::Door ||
295 snapshot.kind == DungeonSelectionKind::Sprite ||
296 snapshot.kind == DungeonSelectionKind::Item) {
297 ImGui::TextColored(theme.status_success,
298 ICON_MD_MANAGE_SEARCH " 1 %s selected",
299 GetDungeonSelectionKindLabel(snapshot.kind));
300 DrawInlineInspectButton(open_object_editor_callback_);
301 } else if (snapshot.kind == DungeonSelectionKind::EntityMulti ||
302 snapshot.kind == DungeonSelectionKind::Mixed) {
303 ImGui::TextColored(theme.status_success, ICON_MD_SELECT_ALL " %s",
304 GetDungeonSelectionSummaryText(snapshot).c_str());
305 DrawInlineInspectButton(open_object_editor_callback_);
306 } else {
307 ImGui::TextColored(
308 theme.text_secondary_gray, ICON_MD_MOUSE
309 " Browse, filter, and click an object below to queue placement.");
310 }
311
312 auto* rooms = object_selector_.get_rooms();
313 if (rooms && current_room_id_ >= 0 &&
315 const auto& room = (*rooms)[current_room_id_];
316 size_t object_count = room.GetTileObjects().size();
317 size_t sprite_count = room.GetSprites().size();
318 size_t door_count = room.GetDoors().size();
319 int chest_count = 0;
320 for (const auto& obj : room.GetTileObjects()) {
323 chest_count++;
324 }
325 }
326
327 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
328 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
329 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
330 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
331
332 auto usage_color = [&](size_t count, int max_val,
333 bool exact_capacity_warning) -> ImVec4 {
334 if (exact_capacity_warning) {
335 return GetPlacementSummaryColor(theme, count, max_val,
336 theme.text_secondary_gray);
337 }
338 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
339 if (ratio >= 1.0f) {
340 return theme.status_error;
341 }
342 if (ratio >= 0.75f) {
343 return theme.status_warning;
344 }
345 return theme.text_secondary_gray;
346 };
347
348 zelda3::DungeonValidator validator;
349 auto result = validator.ValidateRoom(room);
350
351 ImGui::Spacing();
352 DrawUsageChips(
353 {{ICON_MD_WIDGETS, absl::StrFormat("%zu/%d", object_count, kMaxObjects),
354 usage_color(object_count, kMaxObjects, true)},
356 absl::StrFormat("%zu/%d", sprite_count, kMaxSprites),
357 usage_color(sprite_count, kMaxSprites, true)},
358 {ICON_MD_DOOR_FRONT, absl::StrFormat("%zu/%d", door_count, kMaxDoors),
359 usage_color(door_count, kMaxDoors, true)},
361 absl::StrFormat("%d/%d", chest_count, kMaxChests),
362 usage_color(chest_count, kMaxChests, false)}});
363
364 if (!result.errors.empty() || !result.warnings.empty()) {
365 ImGui::TextColored(
366 result.errors.empty() ? theme.status_warning : theme.status_error,
367 tr("%s %zu issue%s"),
368 result.errors.empty() ? ICON_MD_WARNING : ICON_MD_ERROR,
369 result.errors.size() + result.warnings.size(),
370 (result.errors.size() + result.warnings.size()) == 1 ? "" : "s");
371 if (ImGui::IsItemHovered()) {
372 ImGui::BeginTooltip();
373 for (const auto& err : result.errors) {
374 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
375 err.c_str());
376 }
377 for (const auto& warn : result.warnings) {
378 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
379 warn.c_str());
380 }
381 ImGui::EndTooltip();
382 }
383 }
384 } else {
385 ImGui::Spacing();
386 ImGui::TextColored(theme.text_secondary_gray,
387 ICON_MD_INFO " Room data unavailable");
388 }
389}
390
398
399} // namespace editor
400} // 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
bool UsesRoomObjectStream(const RoomObject &object)
constexpr size_t kMaxDoors
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
constexpr bool IsStatefulChestObjectId(int object_id)
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites