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"
9#include "imgui/imgui.h"
10#include "util/log.h"
12
13namespace yaze::editor {
14
15// ============================================================================
16// Selection Operations
17// ============================================================================
18
20 switch (mode) {
22 // Replace entire selection with single object
23 selected_indices_.clear();
24 selected_indices_.insert(index);
25 break;
26
28 // Add to existing selection (Shift+click)
29 selected_indices_.insert(index);
30 break;
31
33 // Toggle object in selection (Ctrl+click)
34 if (selected_indices_.count(index)) {
35 selected_indices_.erase(index);
36 } else {
37 selected_indices_.insert(index);
38 }
39 break;
40
42 // This shouldn't be used for single object selection
43 LOG_ERROR("ObjectSelection",
44 "Rectangle mode used for single object selection");
45 selected_indices_.insert(index);
46 break;
47 }
48
50}
51
53 int room_min_x, int room_min_y, int room_max_x, int room_max_y,
54 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
55 // Normalize rectangle bounds
56 int min_x = std::min(room_min_x, room_max_x);
57 int max_x = std::max(room_min_x, room_max_x);
58 int min_y = std::min(room_min_y, room_max_y);
59 int max_y = std::max(room_min_y, room_max_y);
60
61 // For Single mode, clear previous selection first
62 if (mode == SelectionMode::Single) {
63 selected_indices_.clear();
64 }
65
66 // Find all objects within rectangle
67 for (size_t i = 0; i < objects.size(); ++i) {
68 if (IsObjectInRectangle(objects[i], min_x, min_y, max_x, max_y)) {
69 if (mode == SelectionMode::Toggle) {
70 // Toggle each object
71 if (selected_indices_.count(i)) {
72 selected_indices_.erase(i);
73 } else {
74 selected_indices_.insert(i);
75 }
76 } else {
77 // Add or Replace mode - just add
78 selected_indices_.insert(i);
79 }
80 }
81 }
82
84}
85
86void ObjectSelection::SelectAll(size_t object_count) {
87 selected_indices_.clear();
88 for (size_t i = 0; i < object_count; ++i) {
89 selected_indices_.insert(i);
90 }
92}
93
95 const std::vector<zelda3::RoomObject>& objects) {
96 selected_indices_.clear();
97 for (size_t i = 0; i < objects.size(); ++i) {
98 // Only select objects that pass the layer filter
99 if (PassesLayerFilter(objects[i])) {
100 selected_indices_.insert(i);
101 }
102 }
104}
105
107 if (selected_indices_.empty()) {
108 return; // No change
109 }
110
111 selected_indices_.clear();
113}
114
115bool ObjectSelection::IsObjectSelected(size_t index) const {
116 return selected_indices_.count(index) > 0;
117}
118
119std::vector<size_t> ObjectSelection::GetSelectedIndices() const {
120 // Safely convert set to vector with bounds checking
121 std::vector<size_t> result;
122 result.reserve(selected_indices_.size());
123 for (size_t idx : selected_indices_) {
124 result.push_back(idx);
125 }
126 return result;
127}
128
129std::optional<size_t> ObjectSelection::GetPrimarySelection() const {
130 if (selected_indices_.empty()) {
131 return std::nullopt;
132 }
133 return *selected_indices_.begin(); // First element (lowest index)
134}
135
136// ============================================================================
137// Rectangle Selection State
138// ============================================================================
139
140void ObjectSelection::BeginRectangleSelection(int canvas_x, int canvas_y) {
142 rect_start_x_ = canvas_x;
143 rect_start_y_ = canvas_y;
144 rect_end_x_ = canvas_x;
145 rect_end_y_ = canvas_y;
146}
147
148void ObjectSelection::UpdateRectangleSelection(int canvas_x, int canvas_y) {
150 LOG_ERROR("ObjectSelection",
151 "UpdateRectangleSelection called when not active");
152 return;
153 }
154
155 rect_end_x_ = canvas_x;
156 rect_end_y_ = canvas_y;
157}
158
160 const std::vector<zelda3::RoomObject>& objects, SelectionMode mode) {
162 LOG_ERROR("ObjectSelection",
163 "EndRectangleSelection called when not active");
164 return;
165 }
166
167 // Convert canvas coordinates to room coordinates
168 auto [start_room_x, start_room_y] =
170 auto [end_room_x, end_room_y] =
172
173 // Select objects in rectangle
174 SelectObjectsInRect(start_room_x, start_room_y, end_room_x, end_room_y,
175 objects, mode);
176
178}
179
183
184std::tuple<int, int, int, int> ObjectSelection::GetRectangleSelectionBounds()
185 const {
186 int min_x = std::min(rect_start_x_, rect_end_x_);
187 int max_x = std::max(rect_start_x_, rect_end_x_);
188 int min_y = std::min(rect_start_y_, rect_end_y_);
189 int max_y = std::max(rect_start_y_, rect_end_y_);
190 return {min_x, min_y, max_x, max_y};
191}
192
193bool ObjectSelection::IsRectangleLargeEnough(int min_pixels) const {
195 return false;
196 }
197
198 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
199 const int width = std::abs(max_x - min_x);
200 const int height = std::abs(max_y - min_y);
201 return (width >= min_pixels) || (height >= min_pixels);
202}
203
204// ============================================================================
205// Visual Rendering
206// ============================================================================
207
209 gui::Canvas* canvas, const std::vector<zelda3::RoomObject>& objects,
210 std::function<std::tuple<int, int, int, int>(const zelda3::RoomObject&)>
211 bounds_calculator) {
212 if (selected_indices_.empty() || !canvas) {
213 return;
214 }
215
216 ImDrawList* draw_list = ImGui::GetWindowDrawList();
217 const DungeonCanvasTransform transform(
218 canvas->zero_point(), canvas->scrolling(), canvas->global_scale());
219 const float scale = transform.scale();
220
221 for (size_t index : selected_indices_) {
222 if (index >= objects.size()) {
223 continue;
224 }
225
226 const auto& object = objects[index];
227
228 // Calculate object position in canvas coordinates
229 auto [obj_x, obj_y] = RoomToCanvasCoordinates(object.x_, object.y_);
230
231 int offset_x = 0;
232 int offset_y = 0;
233 int pixel_width = 0;
234 int pixel_height = 0;
235 if (bounds_calculator) {
236 auto [dx, dy, w, h] = bounds_calculator(object);
237 offset_x = dx;
238 offset_y = dy;
239 pixel_width = w;
240 pixel_height = h;
241 } else {
242 // Fallback to old logic if no calculator provided
243 auto [tile_x, tile_y, tile_width, tile_height] = GetObjectBounds(object);
244 offset_x = (tile_x - object.x_) * 8;
245 offset_y = (tile_y - object.y_) * 8;
246 pixel_width = tile_width * 8;
247 pixel_height = tile_height * 8;
248 }
249
250 obj_x += offset_x;
251 obj_y += offset_y;
252
253 ImVec2 obj_start = transform.RoomPixelsToScreen(
254 ImVec2(static_cast<float>(obj_x), static_cast<float>(obj_y)));
255 const ImVec2 obj_size = transform.RoomSizeToScreen(ImVec2(
256 static_cast<float>(pixel_width), static_cast<float>(pixel_height)));
257 ImVec2 obj_end(obj_start.x + obj_size.x, obj_start.y + obj_size.y);
258
259 // Expand selection box slightly for visibility
260 constexpr float margin = 2.0f;
261 obj_start.x -= margin;
262 obj_start.y -= margin;
263 obj_end.x += margin;
264 obj_end.y += margin;
265
266 // Get color based on theme
267 const auto& theme = AgentUI::GetTheme();
268
269 // Draw pulsing animated border
270 float pulse =
271 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 6.0f);
272 ImVec4 pulsing_color = theme.selection_primary;
273 pulsing_color.w = 0.4f + 0.4f * pulse;
274
275 ImU32 border_color = ImGui::GetColorU32(theme.selection_primary);
276 ImU32 pulse_color_u32 = ImGui::GetColorU32(pulsing_color);
277
278 draw_list->AddRect(obj_start, obj_end, border_color, 0.0f, 0, 2.0f);
279 draw_list->AddRect(obj_start, obj_end, pulse_color_u32, 0.0f, 0, 4.0f);
280
281 // Draw corner handles with theme handle color
282 float handle_size = 6.0f * std::max(1.0f, scale * 0.5f);
283 ImU32 handle_color = ImGui::GetColorU32(theme.selection_handle);
284 ImU32 handle_outline = ImGui::GetColorU32(ImVec4(0, 0, 0, 0.8f));
285
286 auto draw_handle = [&](ImVec2 pos) {
287 draw_list->AddRectFilled(
288 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
289 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
290 handle_color, 2.0f);
291 draw_list->AddRect(
292 ImVec2(pos.x - handle_size / 2, pos.y - handle_size / 2),
293 ImVec2(pos.x + handle_size / 2, pos.y + handle_size / 2),
294 handle_outline, 2.0f, 0, 1.0f);
295 };
296
297 draw_handle(obj_start);
298 draw_handle(ImVec2(obj_end.x, obj_start.y));
299 draw_handle(ImVec2(obj_start.x, obj_end.y));
300 draw_handle(obj_end);
301 }
302}
303
305 const zelda3::RoomObject& object) const {
306 const auto& theme = AgentUI::GetTheme();
307 int layer = object.GetLayerValue();
308
309 switch (layer) {
310 case 0:
311 return theme.dungeon_outline_layer0;
312 case 1:
313 return theme.dungeon_outline_layer1;
314 case 2:
315 return theme.dungeon_outline_layer2;
316 default:
317 return theme.status_inactive;
318 }
319}
320
322 if (!rectangle_selection_active_ || !canvas) {
323 return;
324 }
325
326 const auto& theme = AgentUI::GetTheme();
327 ImDrawList* draw_list = ImGui::GetWindowDrawList();
328 const DungeonCanvasTransform transform(
329 canvas->zero_point(), canvas->scrolling(), canvas->global_scale());
330
331 // Get normalized bounds
332 auto [min_x, min_y, max_x, max_y] = GetRectangleSelectionBounds();
333
334 const ImVec2 box_start = transform.RoomPixelsToScreen(
335 ImVec2(static_cast<float>(min_x), static_cast<float>(min_y)));
336 const ImVec2 box_end = transform.RoomPixelsToScreen(
337 ImVec2(static_cast<float>(max_x), static_cast<float>(max_y)));
338
339 // Draw selection box with theme selection color
340 // Border: High-contrast at 0.85f alpha
341 ImU32 border_color = ImGui::ColorConvertFloat4ToU32(
342 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y,
343 theme.selection_secondary.z, 0.85f));
344 // Fill: Subtle at 0.15f alpha
345 ImU32 fill_color = ImGui::ColorConvertFloat4ToU32(
346 ImVec4(theme.selection_secondary.x, theme.selection_secondary.y,
347 theme.selection_secondary.z, 0.15f));
348
349 draw_list->AddRectFilled(box_start, box_end, fill_color);
350 draw_list->AddRect(box_start, box_end, border_color, 0.0f, 0, 2.0f);
351}
352
353// ============================================================================
354// Utility Functions
355// ============================================================================
356
357std::pair<int, int> ObjectSelection::RoomToCanvasCoordinates(int room_x,
358 int room_y) {
359 // Dungeon tiles are 8x8 pixels
360 return {room_x * 8, room_y * 8};
361}
362
363std::pair<int, int> ObjectSelection::CanvasToRoomCoordinates(int canvas_x,
364 int canvas_y) {
365 // Convert pixels back to tiles (round down)
366 return {canvas_x / 8, canvas_y / 8};
367}
368
369std::tuple<int, int, int, int> ObjectSelection::GetObjectBounds(
370 const zelda3::RoomObject& object) {
372}
373
374// ============================================================================
375// Private Helper Functions
376// ============================================================================
377
383
385 int min_x, int min_y, int max_x,
386 int max_y) const {
387 // Check layer filter first
388 if (!PassesLayerFilter(object)) {
389 return false;
390 }
391
392 // Get object bounds
393 auto [obj_x, obj_y, obj_width, obj_height] = GetObjectBounds(object);
394
395 // Check if object's bounding box intersects with selection rectangle
396 // Object is selected if ANY part of it is within the rectangle
397 int obj_min_x = obj_x;
398 int obj_max_x = obj_x + obj_width - 1;
399 int obj_min_y = obj_y;
400 int obj_max_y = obj_y + obj_height - 1;
401
402 // Rectangle intersection test
403 bool x_overlap = (obj_min_x <= max_x) && (obj_max_x >= min_x);
404 bool y_overlap = (obj_min_y <= max_y) && (obj_max_y >= min_y);
405
406 return x_overlap && y_overlap;
407}
408
410 const zelda3::RoomObject& object) const {
411 // If no layer filter is active, all objects pass
413 return true;
414 }
415
416 // Mask mode: only allow BG2/Layer 1 objects (overlay content like platforms)
418 return object.GetLayerValue() == kLayer2; // Layer 1 = BG2 = overlay
419 }
420
421 // Check if the object's layer matches the filter
422 return object.GetLayerValue() == static_cast<uint8_t>(active_layer_filter_);
423}
424
425} // namespace yaze::editor
ImVec2 RoomSizeToScreen(ImVec2 room_size) const
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.
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::tuple< int, int, int, int >(const zelda3::RoomObject &)> bounds_calculator)
Draw selection highlights for all selected objects.
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.
bool IsRectangleLargeEnough(int min_pixels) const
Check if rectangle selection exceeds a minimum pixel size.
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
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:64
auto global_scale() const
Definition canvas.h:397
auto zero_point() const
Definition canvas.h:348
auto scrolling() const
Definition canvas.h:350
static DimensionService & Get()
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
#define LOG_ERROR(category, format,...)
Definition log.h:109
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.