yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_selection.cc
Go to the documentation of this file.
1#include "object_selection.h"
2
3#include <algorithm>
4
5#include "absl/strings/str_format.h"
7#include "imgui/imgui.h"
8#include "util/log.h"
9
10namespace yaze::editor {
11
12// ============================================================================
13// Selection Operations
14// ============================================================================
15
17 switch (mode) {
19 // Replace entire selection with single object
20 selected_indices_.clear();
21 selected_indices_.insert(index);
22 break;
23
25 // Add to existing selection (Shift+click)
26 selected_indices_.insert(index);
27 break;
28
30 // Toggle object in selection (Ctrl+click)
31 if (selected_indices_.count(index)) {
32 selected_indices_.erase(index);
33 } else {
34 selected_indices_.insert(index);
35 }
36 break;
37
39 // This shouldn't be used for single object selection
40 LOG_ERROR("ObjectSelection",
41 "Rectangle mode used for single object selection");
42 selected_indices_.insert(index);
43 break;
44 }
45
47}
48
50 int room_min_x, int room_min_y, int room_max_x, int room_max_y,
51 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
52 // Normalize rectangle bounds
53 int min_x = std::min(room_min_x, room_max_x);
54 int max_x = std::max(room_min_x, room_max_x);
55 int min_y = std::min(room_min_y, room_max_y);
56 int max_y = std::max(room_min_y, room_max_y);
57
58 // For Single mode, clear previous selection first
59 if (mode == SelectionMode::Single) {
60 selected_indices_.clear();
61 }
62
63 // Find all objects within rectangle
64 for (size_t i = 0; i < objects.size(); ++i) {
65 if (IsObjectInRectangle(objects[i], min_x, min_y, max_x, max_y)) {
66 if (mode == SelectionMode::Toggle) {
67 // Toggle each object
68 if (selected_indices_.count(i)) {
69 selected_indices_.erase(i);
70 } else {
71 selected_indices_.insert(i);
72 }
73 } else {
74 // Add or Replace mode - just add
75 selected_indices_.insert(i);
76 }
77 }
78 }
79
81}
82
83void ObjectSelection::SelectAll(size_t object_count) {
84 selected_indices_.clear();
85 for (size_t i = 0; i < object_count; ++i) {
86 selected_indices_.insert(i);
87 }
89}
90
91void ObjectSelection::SelectAll(const std::vector<zelda3::RoomObject>& objects) {
92 selected_indices_.clear();
93 for (size_t i = 0; i < objects.size(); ++i) {
94 // Only select objects that pass the layer filter
95 if (PassesLayerFilter(objects[i])) {
96 selected_indices_.insert(i);
97 }
98 }
100}
101
103 if (selected_indices_.empty()) {
104 return; // No change
105 }
106
107 selected_indices_.clear();
109}
110
111bool ObjectSelection::IsObjectSelected(size_t index) const {
112 return selected_indices_.count(index) > 0;
113}
114
115std::vector<size_t> ObjectSelection::GetSelectedIndices() const {
116 // Safely convert set to vector with bounds checking
117 std::vector<size_t> result;
118 result.reserve(selected_indices_.size());
119 for (size_t idx : selected_indices_) {
120 result.push_back(idx);
121 }
122 return result;
123}
124
125std::optional<size_t> ObjectSelection::GetPrimarySelection() const {
126 if (selected_indices_.empty()) {
127 return std::nullopt;
128 }
129 return *selected_indices_.begin(); // First element (lowest index)
130}
131
132// ============================================================================
133// Rectangle Selection State
134// ============================================================================
135
136void ObjectSelection::BeginRectangleSelection(int canvas_x, int canvas_y) {
138 rect_start_x_ = canvas_x;
139 rect_start_y_ = canvas_y;
140 rect_end_x_ = canvas_x;
141 rect_end_y_ = canvas_y;
142}
143
144void ObjectSelection::UpdateRectangleSelection(int canvas_x, int canvas_y) {
146 LOG_ERROR("ObjectSelection",
147 "UpdateRectangleSelection called when not active");
148 return;
149 }
150
151 rect_end_x_ = canvas_x;
152 rect_end_y_ = canvas_y;
153}
154
156 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
158 LOG_ERROR("ObjectSelection",
159 "EndRectangleSelection called when not active");
160 return;
161 }
162
163 // Convert canvas coordinates to room coordinates
164 auto [start_room_x, start_room_y] =
166 auto [end_room_x, end_room_y] =
168
169 // Select objects in rectangle
170 SelectObjectsInRect(start_room_x, start_room_y, end_room_x, end_room_y,
171 objects, mode);
172
174}
175
179
180std::tuple<int, int, int, int> ObjectSelection::GetRectangleSelectionBounds()
181 const {
182 int min_x = std::min(rect_start_x_, rect_end_x_);
183 int max_x = std::max(rect_start_x_, rect_end_x_);
184 int min_y = std::min(rect_start_y_, rect_end_y_);
185 int max_y = std::max(rect_start_y_, rect_end_y_);
186 return {min_x, min_y, max_x, max_y};
187}
188
189// ============================================================================
190// Visual Rendering
191// ============================================================================
192
194 gui::Canvas* canvas, const std::vector<zelda3::RoomObject>& objects,
195 std::function<std::pair<int, int>(const zelda3::RoomObject&)>
196 dimension_calculator) {
197 if (selected_indices_.empty() || !canvas) {
198 return;
199 }
200
201 ImDrawList* draw_list = ImGui::GetWindowDrawList();
202 ImVec2 canvas_pos = canvas->zero_point();
203 float scale = canvas->global_scale();
204
205 for (size_t index : selected_indices_) {
206 if (index >= objects.size()) {
207 continue;
208 }
209
210 const auto& object = objects[index];
211
212 // Calculate object position in canvas coordinates
213 auto [obj_x, obj_y] = RoomToCanvasCoordinates(object.x_, object.y_);
214
215 // Calculate object dimensions
216 int pixel_width, pixel_height;
217 if (dimension_calculator) {
218 auto dims = dimension_calculator(object);
219 pixel_width = dims.first;
220 pixel_height = dims.second;
221 } else {
222 // Fallback to old logic if no calculator provided
223 auto [tile_x, tile_y, tile_width, tile_height] = GetObjectBounds(object);
224 pixel_width = tile_width * 8;
225 pixel_height = tile_height * 8;
226 }
227
228 // Apply scale and canvas offset
229 ImVec2 obj_start(canvas_pos.x + obj_x * scale,
230 canvas_pos.y + obj_y * scale);
231 ImVec2 obj_end(obj_start.x + pixel_width * scale,
232 obj_start.y + pixel_height * scale);
233
234 // Expand selection box slightly for visibility
235 constexpr float margin = 2.0f;
236 obj_start.x -= margin;
237 obj_start.y -= margin;
238 obj_end.x += margin;
239 obj_end.y += margin;
240
241 // Get color based on object layer and type
242 ImVec4 base_color = GetLayerTypeColor(object);
243
244 // Draw pulsing animated border
245 float pulse =
246 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f);
247 ImVec4 pulsing_color = ImVec4(
248 base_color.x * pulse, base_color.y * pulse, base_color.z * pulse,
249 0.85f // High-contrast at 0.85f alpha
250 );
251 ImU32 border_color = ImGui::GetColorU32(pulsing_color);
252 draw_list->AddRect(obj_start, obj_end, border_color, 0.0f, 0, 2.5f);
253
254 // Draw corner handles with matching color
255 constexpr float handle_size = 6.0f;
256 ImVec4 handle_col = ImVec4(base_color.x, base_color.y, base_color.z, 0.95f);
257 ImU32 handle_color = ImGui::GetColorU32(handle_col);
258
259 // Top-left handle
260 draw_list->AddRectFilled(
261 ImVec2(obj_start.x - handle_size / 2, obj_start.y - handle_size / 2),
262 ImVec2(obj_start.x + handle_size / 2, obj_start.y + handle_size / 2),
263 handle_color);
264
265 // Top-right handle
266 draw_list->AddRectFilled(
267 ImVec2(obj_end.x - handle_size / 2, obj_start.y - handle_size / 2),
268 ImVec2(obj_end.x + handle_size / 2, obj_start.y + handle_size / 2),
269 handle_color);
270
271 // Bottom-left handle
272 draw_list->AddRectFilled(
273 ImVec2(obj_start.x - handle_size / 2, obj_end.y - handle_size / 2),
274 ImVec2(obj_start.x + handle_size / 2, obj_end.y + handle_size / 2),
275 handle_color);
276
277 // Bottom-right handle
278 draw_list->AddRectFilled(
279 ImVec2(obj_end.x - handle_size / 2, obj_end.y - handle_size / 2),
280 ImVec2(obj_end.x + handle_size / 2, obj_end.y + handle_size / 2),
281 handle_color);
282 }
283}
284
286 // Layer-based primary hue with type-based saturation variation
287 int layer = object.GetLayerValue();
288 int object_type = zelda3::GetObjectSubtype(object.id_);
289
290 // Layer colors (distinct hues for each layer)
291 // Layer 0 (BG1): Cyan/Teal
292 // Layer 1 (BG2): Orange/Amber
293 // Layer 2 (BG3): Magenta/Pink
294 ImVec4 base;
295 switch (layer) {
296 case 0: // BG1 - Cyan
297 base = ImVec4(0.0f, 0.9f, 1.0f, 1.0f);
298 break;
299 case 1: // BG2 - Orange
300 base = ImVec4(1.0f, 0.6f, 0.0f, 1.0f);
301 break;
302 case 2: // BG3 - Magenta
303 base = ImVec4(1.0f, 0.3f, 0.8f, 1.0f);
304 break;
305 default:
306 base = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); // Gray for unknown
307 break;
308 }
309
310 // Slightly shift color based on object type for additional differentiation
311 switch (object_type) {
312 case 1: // Type 1 (0x00-0xFF) - walls, floors - base color
313 break;
314 case 2: // Type 2 (0x100-0x1FF) - doors, interactive - slightly brighter
315 base.x = std::min(1.0f, base.x * 1.1f);
316 base.y = std::min(1.0f, base.y * 1.1f);
317 base.z = std::min(1.0f, base.z * 1.1f);
318 break;
319 case 3: // Type 3 (0xF00+) - special objects - slightly shifted
320 base.x = std::min(1.0f, base.x + 0.1f);
321 break;
322 }
323
324 return base;
325}
326
328 if (!rectangle_selection_active_ || !canvas) {
329 return;
330 }
331
332 const auto& theme = AgentUI::GetTheme();
333 ImDrawList* draw_list = ImGui::GetWindowDrawList();
334 ImVec2 canvas_pos = canvas->zero_point();
335 float scale = canvas->global_scale();
336
337 // Get normalized bounds
338 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
339
340 // Apply scale and canvas offset
341 ImVec2 box_start(canvas_pos.x + min_x * scale, canvas_pos.y + min_y * scale);
342 ImVec2 box_end(canvas_pos.x + max_x * scale, canvas_pos.y + max_y * scale);
343
344 // Draw selection box with theme accent color
345 // Border: High-contrast at 0.85f alpha
346 ImU32 border_color = ImGui::ColorConvertFloat4ToU32(
347 ImVec4(theme.accent_color.x, theme.accent_color.y, theme.accent_color.z,
348 0.85f));
349 // Fill: Subtle at 0.15f alpha
350 ImU32 fill_color = ImGui::ColorConvertFloat4ToU32(
351 ImVec4(theme.accent_color.x, theme.accent_color.y, theme.accent_color.z,
352 0.15f));
353
354 draw_list->AddRectFilled(box_start, box_end, fill_color);
355 draw_list->AddRect(box_start, box_end, border_color, 0.0f, 0, 2.0f);
356}
357
358// ============================================================================
359// Utility Functions
360// ============================================================================
361
362std::pair<int, int> ObjectSelection::RoomToCanvasCoordinates(int room_x,
363 int room_y) {
364 // Dungeon tiles are 8x8 pixels
365 return {room_x * 8, room_y * 8};
366}
367
368std::pair<int, int> ObjectSelection::CanvasToRoomCoordinates(int canvas_x,
369 int canvas_y) {
370 // Convert pixels back to tiles (round down)
371 return {canvas_x / 8, canvas_y / 8};
372}
373
374std::tuple<int, int, int, int> ObjectSelection::GetObjectBounds(
375 const zelda3::RoomObject& object) {
376 // Use ObjectDimensionTable for accurate dimensions if loaded
377 auto& dim_table = zelda3::ObjectDimensionTable::Get();
378 if (dim_table.IsLoaded()) {
379 // GetHitTestBounds returns (x, y, width_tiles, height_tiles)
380 return dim_table.GetHitTestBounds(object);
381 }
382
383 // Fallback: Object dimensions based on size field
384 // Lower nibble = horizontal size, upper nibble = vertical size
385 // TODO(zelda3-hacking-expert): This fallback ignores SNES size helpers
386 // (1to16 vs 1to15or26/32 and diagonal +4 bases) plus fixed 4x4/super-square
387 // footprints and BothBG dual-layer objects. Align with the rules captured in
388 // docs/internal/agents/dungeon-object-rendering-spec.md so outlines match
389 // the real draw extents when the dimension table is unavailable.
390 int x = object.x_;
391 int y = object.y_;
392 int size_h = (object.size_ & 0x0F);
393 int size_v = (object.size_ >> 4) & 0x0F;
394
395 // Objects are typically (size+1) tiles wide/tall
396 int width = size_h + 1;
397 int height = size_v + 1;
398
399 return {x, y, width, height};
400}
401
402// ============================================================================
403// Private Helper Functions
404// ============================================================================
405
411
413 int min_x, int min_y, int max_x,
414 int max_y) const {
415 // Check layer filter first
416 if (!PassesLayerFilter(object)) {
417 return false;
418 }
419
420 // Get object bounds
421 auto [obj_x, obj_y, obj_width, obj_height] = GetObjectBounds(object);
422
423 // Check if object's bounding box intersects with selection rectangle
424 // Object is selected if ANY part of it is within the rectangle
425 int obj_min_x = obj_x;
426 int obj_max_x = obj_x + obj_width - 1;
427 int obj_min_y = obj_y;
428 int obj_max_y = obj_y + obj_height - 1;
429
430 // Rectangle intersection test
431 bool x_overlap = (obj_min_x <= max_x) && (obj_max_x >= min_x);
432 bool y_overlap = (obj_min_y <= max_y) && (obj_max_y >= min_y);
433
434 return x_overlap && y_overlap;
435}
436
438 // If no layer filter is active, all objects pass
440 return true;
441 }
442
443 // Mask mode: only allow BG2/Layer 1 objects (overlay content like platforms)
445 return object.GetLayerValue() == kLayer2; // Layer 1 = BG2 = overlay
446 }
447
448 // Check if the object's layer matches the filter
449 return object.GetLayerValue() == static_cast<uint8_t>(active_layer_filter_);
450}
451
452} // namespace yaze::editor
bool IsObjectInRectangle(const zelda3::RoomObject &object, int min_x, int min_y, int max_x, int max_y) const
std::tuple< int, int, int, int > GetRectangleSelectionBounds() const
Get rectangle selection bounds in canvas coordinates.
void UpdateRectangleSelection(int canvas_x, int canvas_y)
Update rectangle selection endpoint.
static std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y)
Convert room tile coordinates to canvas pixel coordinates.
std::function< void()> selection_changed_callback_
static constexpr int kMaskLayer
bool IsObjectSelected(size_t index) const
Check if an object is selected.
std::set< size_t > selected_indices_
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
static std::tuple< int, int, int, int > GetObjectBounds(const zelda3::RoomObject &object)
Calculate the bounding box of an object.
void SelectAll(size_t object_count)
Select all objects in the current room.
static std::pair< int, int > CanvasToRoomCoordinates(int canvas_x, int canvas_y)
Convert canvas pixel coordinates to room tile coordinates.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Complete rectangle selection operation.
void BeginRectangleSelection(int canvas_x, int canvas_y)
Begin a rectangle selection operation.
bool PassesLayerFilter(const zelda3::RoomObject &object) const
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::pair< int, int >(const zelda3::RoomObject &)> dimension_calculator)
Draw selection highlights for all selected objects.
ImVec4 GetLayerTypeColor(const zelda3::RoomObject &object) const
Get selection highlight color based on object layer and type.
static constexpr int kLayerAll
void CancelRectangleSelection()
Cancel rectangle selection without modifying selection.
std::optional< size_t > GetPrimarySelection() const
Get the primary selected object (first in selection)
void DrawRectangleSelectionBox(gui::Canvas *canvas)
Draw the active rectangle selection box.
void SelectObjectsInRect(int room_min_x, int room_min_y, int room_max_x, int room_max_y, const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single)
Select multiple objects within a rectangle.
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:150
auto global_scale() const
Definition canvas.h:494
auto zero_point() const
Definition canvas.h:443
static ObjectDimensionTable & Get()
#define LOG_ERROR(category, format,...)
Definition log.h:109
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
Definition agent_chat.cc:23
int GetObjectSubtype(int object_id)